nginx共享內存機制實例分析

1. 使用示例

nginx聲明共享內存的指令為:

proxy_cache_path?/users/mike/nginx-cache?levels=1:2?keys_zone=one:10m?max_size=10g?inactive=60m?use_temp_path=off;

這里只是聲明的一個名稱為one,最大可用內存為10g的共享內存。這里面各個參數的含義如下:

  • /users/mike/nginx-cache:這是一個路徑參數,指定了將共享內存所緩存的文件的存儲位置。這里為什么會生成文件的原因在于,對于上游服務發出的響應,是可以將其生成一個文件存儲在nginx上的,后續如果有同樣的請求,就可以直接讀取該文件或者讀取共享內存中的緩存以響應客戶端;

  • levels:在linux操作系統中,如果所有文件都放在一個文件夾中,那么當文件數量非常多的時候,可能一個磁盤驅動就無法讀取這么多文件了,如果放置在多個文件夾中,那么就能夠利用多個驅動并且讀取的優點。這里的levels參數指定的就是如何生成文件夾。假設nginx為上游服務的某個響應數據生成的文件名為e0bd86606797639426a92306b1b98ad9,那么對于上面的levels=1:2,其就會從文件名的最后開始取值,先取1位(也即9)作為一級子目錄名,然后取2位(也即ad)作為二級子目錄名;

  • keys_zone:該參數指定了當前共享內存的名稱,這里為one,后面的10m表示當前共享內存用于存儲key的內存大小為10m;

  • max_size:該參數指定了當前共享內存可用的最大內存;

  • inactive:該參數指定了當前共享內存的最長存活時間,如果在這段時間內都沒有任何請求訪問該內存數據,那么其就會被lru算法淘汰掉;

  • 該參數設為use_temp_path,用以控制生成的文件是否先存儲到臨時文件夾,之后再移動到指定目錄

2. 工作原理

共享內存的管理工作主要分為如下圖所示的幾個部分:

nginx共享內存機制實例分析

可以看到,其主要分為初始化、共享內存的管理、共享內存的加載和共享內存的使用等幾個方面。在初始化的過程中,首先會解析proxy_cache_path指令,然后分別啟動cache manager和cache loader進程;這里cache manager進程主要是進行共享內存的管理的,其主要是通過lru算法清除過期數據,或者當資源緊張時強制刪除部分未被引用的內存數據;而cache loader進程的主要工作是在nginx啟動之后,讀取文件存儲目錄中已有的文件,將其加載到共享內存中;而共享內存的使用主要是在處理請求完成之后對響應數據的緩存,這一部分的內容將在后面的文章中進行講解,本文主要講解前面三部分的工作原理。

按照上面的劃分,共享內存的管理主要可以分為三個部分(共享內存的使用將在后面進行講解)。如下是這三個部分的處理流程的示意圖:

nginx共享內存機制實例分析

從上面的流程圖中可以看出,在主流程中,主要進行了解析proxy_cache_path指令、啟動cache manager進程和啟動cache loader進程的工作。而在cache manager進程中,主要工作則分為兩部分:1. 檢查隊列尾部元素是否過期,如果過期并且引用數為0,則刪除該元素和該元素對應的文件;2. 檢查當前共享內存是否資源緊張,如果資源緊張,則刪除所有引用數為0的元素及其文件,無論其是否過期。在cache loader進程的處理流程中,主要是通過遞歸的方式遍歷存儲文件的目錄及其子目錄中的文件,然后將這些文件加載到共享內存中。需要注意的是,cache manager進程在每次遍歷完所有的共享內存塊之后會進入下一次循環,而cache loader進程在nginx啟動之后60s的時刻執行一次,然后就會退出該進程。

3. 源碼解讀

3.1 proxy_cache_path指令解析

對于nginx各個指令的解析,其都會在相應的模塊中定義一個ngx_command_t結構體,該結構體中有一個set方法指定了解析當前指令所使用的方法。如下是proxy_cache_path所對應的ngx_command_t結構體的定義:

static?ngx_command_t?ngx_http_proxy_commands[]?=?{ ?{?ngx_string("proxy_cache_path"),?//?指定了當前指令的名稱 ??//?指定了當前指令的使用位置,即http模塊,并且指定了當前模塊的參數個數,這里是必須大于等于2 ???ngx_http_main_conf|ngx_conf_2more, ??//?指定了set()方法所指向的方法 ???ngx_http_file_cache_set_slot, ???ngx_http_main_conf_offset, ???offsetof(ngx_http_proxy_main_conf_t,?caches), ???&ngx_http_proxy_module?} }

可以看到,該指令所使用的解析方法是ngx_http_file_cache_set_slot(),這里我們直接閱讀該方法的源碼:

char?*ngx_http_file_cache_set_slot(ngx_conf_t?*cf,?ngx_command_t?*cmd,?void?*conf) { ??char?*confp?=?conf;  ??off_t??????????max_size; ??u_char?????????*last,?*p; ??time_t?????????inactive; ??ssize_t?????????size; ??ngx_str_t????????s,?name,?*value; ??ngx_int_t????????loader_files,?manager_files; ??ngx_msec_t???????loader_sleep,?manager_sleep,?loader_threshold, ??????????????manager_threshold; ??ngx_uint_t???????i,?n,?use_temp_path; ??ngx_array_t??????*caches; ??ngx_http_file_cache_t?*cache,?**ce;  ??cache?=?ngx_pcalloc(cf->pool,?sizeof(ngx_http_file_cache_t)); ??if?(cache?==?null)?{ ????return?ngx_conf_error; ??}  ??cache->path?=?ngx_pcalloc(cf->pool,?sizeof(ngx_path_t)); ??if?(cache->path?==?null)?{ ????return?ngx_conf_error; ??}  ??//?初始化各個屬性的默認值 ??use_temp_path?=?1;  ??inactive?=?600;  ??loader_files?=?100; ??loader_sleep?=?50; ??loader_threshold?=?200;  ??manager_files?=?100; ??manager_sleep?=?50; ??manager_threshold?=?200;  ??name.len?=?0; ??size?=?0; ??max_size?=?ngx_max_off_t_value;  ??//?示例配置:proxy_cache_path?/users/mike/nginx-cache?levels=1:2?keys_zone=one:10m?max_size=10g?inactive=60m?use_temp_path=off;  ??//?這里的cf->args->elts中存儲了解析proxy_cache_path指令時,其包含的各個token項, ??//?所謂的token項,指的就是使用空格分隔的字符片段 ??value?=?cf->args->elts;  ??//?value[1]就是配置的第一個參數,也即cache文件會保存的根路徑 ??cache->path->name?=?value[1];  ??if?(cache->path->name.data[cache->path->name.len?-?1]?==?'/')?{ ????cache->path->name.len--; ??}  ??if?(ngx_conf_full_name(cf->cycle,?&cache->path->name,?0)?!=?ngx_ok)?{ ????return?ngx_conf_error; ??}  ??//?從第三個參數開始進行解析 ??for?(i?=?2;?i?args->nelts;?i++)?{  ????//?如果第三個參數是以"levels="開頭,則解析levels子參數 ????if?(ngx_strncmp(value[i].data,?"levels=",?7)?==?0)?{  ??????p?=?value[i].data?+?7;?//?計算開始解析的其實位置 ??????last?=?value[i].data?+?value[i].len;??//?計算最后一個字符的位置  ??????//?開始解析1:2 ??????for?(n?=?0;?n??'0'?&&?*p?path->level[n]?=?*p++?-?'0'; ??????????cache->path->len?+=?cache->path->level[n]?+?1;  ??????????if?(p?==?last)?{ ????????????break; ??????????}  ??????????//?如果當前字符是冒號,則繼續下一個字符的解析; ??????????//?這里的ngx_max_path_level值為3,也就是說levels參數后最多接3級子目錄 ??????????if?(*p++?==?':'?&&?n?path->len??8191)?{ ??????????continue; ????????} ??????}  ??????ngx_conf_log_error(ngx_log_emerg,?cf,?0, ????????????????"invalid?keys?zone?size?"%v"",?&value[i]); ??????return?ngx_conf_error; ????}  ????//?如果參數是以"inactive="開頭,則解析inactive參數。該參數的形式如inactive=60m, ????//?表示緩存的文件在多長時間沒有訪問之后將會過期 ????if?(ngx_strncmp(value[i].data,?"inactive=",?9)?==?0)?{  ??????s.len?=?value[i].len?-?9; ??????s.data?=?value[i].data?+?9;  ??????//?對時間進行解析,最終將轉換為以秒為單位的時間長度 ??????inactive?=?ngx_parse_time(&s,?1); ??????if?(inactive?==?(time_t)?ngx_error)?{ ????????ngx_conf_log_error(ngx_log_emerg,?cf,?0, ??????????????????"invalid?inactive?value?"%v"",?&value[i]); ????????return?ngx_conf_error; ??????}  ??????continue; ????}  ????//?如果參數是以"max_size="開頭,則解析max_size參數。該參數的形式如max_size=10g, ????//?表示當前緩存能夠使用的最大內存空間 ????if?(ngx_strncmp(value[i].data,?"max_size=",?9)?==?0)?{  ??????s.len?=?value[i].len?-?9; ??????s.data?=?value[i].data?+?9;  ??????//?對解析得到的值進行轉換,最終將以字節數為單位 ??????max_size?=?ngx_parse_offset(&s); ??????if?(max_size?name); ????return?ngx_conf_error; ??}  ??//?這里的cache->path->manager和cache->path->loader的值為兩個函數,需要注意的是, ??//?在nginx啟動之后,會啟動兩個單獨的進程,一個cache?manager,一個cache?loader,其中cache?manager ??//?將會在一個循環中不斷的為每個共享內存執行cache->path->manager所指定的方法, ??//?從而實現對緩存進行清理。而另一個進程cache?loader則會在nginx啟動之后60s的時候只執行一次, ??//?執行的方法就是cache->path->loader所指定的方法, ??//?該方法的主要作用是加載已經存在的文件數據到當前的共享內存中 ??cache->path->manager?=?ngx_http_file_cache_manager; ??cache->path->loader?=?ngx_http_file_cache_loader; ??cache->path->data?=?cache; ??cache->path->conf_file?=?cf->conf_file->file.name.data; ??cache->path->line?=?cf->conf_file->line; ??cache->loader_files?=?loader_files; ??cache->loader_sleep?=?loader_sleep; ??cache->loader_threshold?=?loader_threshold; ??cache->manager_files?=?manager_files; ??cache->manager_sleep?=?manager_sleep; ??cache->manager_threshold?=?manager_threshold;  ??//?將當前的path添加到cycle中,后續會對這些path進行檢查,如果path不存在,則會創建相應的路徑 ??if?(ngx_add_path(cf,?&cache->path)?!=?ngx_ok)?{ ????return?ngx_conf_error; ??}  ??//?把當前共享內存添加到cf->cycle->shared_memory所指定的共享內存列表中 ??cache->shm_zone?=?ngx_shared_memory_add(cf,?&name,?size,?cmd->post); ??if?(cache->shm_zone?==?null)?{ ????return?ngx_conf_error; ??}  ??if?(cache->shm_zone->data)?{ ????ngx_conf_log_error(ngx_log_emerg,?cf,?0, ??????????????"duplicate?zone?"%v"",?&name); ????return?ngx_conf_error; ??}   ??//?這里指定了每個共享內存的初始化方法,該方法在master進程啟動的時候會被執行 ??cache->shm_zone->init?=?ngx_http_file_cache_init; ??cache->shm_zone->data?=?cache;  ??cache->use_temp_path?=?use_temp_path;  ??cache->inactive?=?inactive; ??cache->max_size?=?max_size;  ??caches?=?(ngx_array_t?*)?(confp?+?cmd->offset);  ??ce?=?ngx_array_push(caches); ??if?(ce?==?null)?{ ????return?ngx_conf_error; ??}  ??*ce?=?cache;  ??return?ngx_conf_ok; }

?從上面的代碼可以看出,在proxy_cache_path方法中,主要是初始化了一個ngx_http_file_cache_t結構體。而該結構體中的各個屬性,則是通過解析proxy_cache_path的各個參數來進行的。

3.2 cache manager與cache loader進程啟動

nginx程序的入口方法是nginx.c的main()方法,如果開啟了master-worker進程模式,那么最后就會進入ngx_master_process_cycle()方法,該方法首先會啟動worker進程,以接收客戶端的請求;然后會分別啟動cache manager和cache loader進程;最后進入一個無限循環中,以處理用戶在命令行向nginx發送的指令。如下是cache manager和cache loader進程啟動的源碼:

void ngx_master_process_cycle(ngx_cycle_t?*cycle) { ??... ??? ??//?獲取核心模塊的配置 ??ccf?=?(ngx_core_conf_t?*)?ngx_get_conf(cycle->conf_ctx,?ngx_core_module);  ??//?啟動各個worker進程 ??ngx_start_worker_processes(cycle,?ccf->worker_processes,?ngx_process_respawn); ??//?啟動cache進程 ??ngx_start_cache_manager_processes(cycle,?0); ? ??... }

?對于cache manager和cache loader進程的啟動,可以看到,其主要是在ngx_start_cache_manager_processes()方法中,如下是該方法的源碼:

static?void?ngx_start_cache_manager_processes(ngx_cycle_t?*cycle,?ngx_uint_t?respawn)?{ ??ngx_uint_t????i,?manager,?loader; ??ngx_path_t???**path; ??ngx_channel_t??ch;  ??manager?=?0; ??loader?=?0;  ??path?=?ngx_cycle->paths.elts; ??for?(i?=?0;?i?paths.nelts;?i++)?{  ????//?查找是否有任何一個path指定了manager為1 ????if?(path[i]->manager)?{ ??????manager?=?1; ????}  ????//?查找是否有任何一個path指定了loader為1 ????if?(path[i]->loader)?{ ??????loader?=?1; ????} ??}  ??//?如果沒有任何一個path的manager指定為1,則直接返回 ??if?(manager?==?0)?{ ????return; ??}  ??//?創建一個進程以執行ngx_cache_manager_process_cycle()方法中所執行的循環,需要注意的是, ??//?在回調ngx_cache_manager_process_cycle方法時,這里傳入的第二個參數是ngx_cache_manager_ctx ??ngx_spawn_process(cycle,?ngx_cache_manager_process_cycle, ???????????&ngx_cache_manager_ctx,?"cache?manager?process", ???????????respawn???ngx_process_just_respawn?:?ngx_process_respawn);  ??ngx_memzero(&ch,?sizeof(ngx_channel_t));  ??//?創建一個ch結構體,以將當前進程的創建消息廣播出去 ??ch.command?=?ngx_cmd_open_channel; ??ch.pid?=?ngx_processes[ngx_process_slot].pid; ??ch.slot?=?ngx_process_slot; ??ch.fd?=?ngx_processes[ngx_process_slot].channel[0];  ??//?廣播cache?manager?process進程被創建的消息 ??ngx_pass_open_channel(cycle,?&ch);  ??if?(loader?==?0)?{ ????return; ??}  ??//?創建一個進程以執行ngx_cache_manager_process_cycle()所指定的流程,需要注意的是, ??//?在回調ngx_cache_manager_process_cycle方法時,這里傳入的第二個參數是ngx_cache_loader_ctx ??ngx_spawn_process(cycle,?ngx_cache_manager_process_cycle, ???????????&ngx_cache_loader_ctx,?"cache?loader?process", ???????????respawn???ngx_process_just_spawn?:?ngx_process_norespawn);  ??//?創建一個ch結構體,以將當前進程的創建消息廣播出去 ??ch.command?=?ngx_cmd_open_channel; ??ch.pid?=?ngx_processes[ngx_process_slot].pid; ??ch.slot?=?ngx_process_slot; ??ch.fd?=?ngx_processes[ngx_process_slot].channel[0];  ??//?廣播cache?loader?process進程被創建的消息 ??ngx_pass_open_channel(cycle,?&ch); }

上面的代碼其實比較簡單,首先檢查是否有任何一個路徑指定了使用cache manager或者cache loader,如果有,則啟動對應的繼承,否則是不會創建cache manager和cache loader進程的。而啟動這兩個進程所使用的方法都是:

//?啟動cache?manager進程 ngx_spawn_process(cycle,?ngx_cache_manager_process_cycle, ?????????&ngx_cache_manager_ctx,?"cache?manager?process", ?????????respawn???ngx_process_just_respawn?:?ngx_process_respawn);  //?啟動cache?loader進程 ngx_spawn_process(cycle,?ngx_cache_manager_process_cycle, ???????????&ngx_cache_loader_ctx,?"cache?loader?process", ???????????respawn???ngx_process_just_spawn?:?ngx_process_norespawn);

這里的ngx_spawn_process()方法的作用主要是創建一個新的進程,該進程創建之后就會執行第二個參數所指定的方法,并且執行該方法時傳入的參數是這里第三個參數所指定的結構體對象。觀察上面兩個啟動進程的方式,其在新進程創建之后所執行的方法都是ngx_cache_manager_process_cycle(),只不過調用該方法時傳入的參數不一樣,一個是ngx_cache_manager_ctx,另一個則是ngx_cache_loader_ctx。這里我們首先看一下這兩個結構體的定義:

//?這里的ngx_cache_manager_process_handler指定了當前cache?manager進程將會執行的方法, //?cache?manager?process則指定了該進程的名稱,而最后的0表示當前進程在啟動之后間隔多長時間才會執行 //?ngx_cache_manager_process_handler()方法,這里是立即執行 static?ngx_cache_manager_ctx_t?ngx_cache_manager_ctx?=?{ ??ngx_cache_manager_process_handler,?"cache?manager?process",?0 };  //?這里的ngx_cache_loader_process_handler指定了當前cache?loader進程將會執行的方法, //?其會在cache?loader進程啟動后60秒之后才會執行ngx_cache_loader_process_handler()方法 static?ngx_cache_manager_ctx_t?ngx_cache_loader_ctx?=?{ ??ngx_cache_loader_process_handler,?"cache?loader?process",?60000 };

可以看到,這兩個結構體主要是分別定義了cache manager和cache loader兩個進程的不同行為。下面我們來看一下ngx_cache_manager_process_cycle()方法是如何調用這兩個方法的:

static?void?ngx_cache_manager_process_cycle(ngx_cycle_t?*cycle,?void?*data)?{ ??ngx_cache_manager_ctx_t?*ctx?=?data;  ??void?????*ident[4]; ??ngx_event_t??ev;  ??ngx_process?=?ngx_process_helper;  ??//?當前進程主要是用于處理cache?manager和cache?loader工作的,因而其不需要進行socket的監聽,因而這里需要將其關閉 ??ngx_close_listening_sockets(cycle);  ??/*?set?a?moderate?number?of?connections?for?a?helper?process.?*/ ??cycle->connection_n?=?512;  ??//?對當前的進程進行初始化,主要是設置一些參數屬性,并且在最后為當前進行設置監聽channel[1]句柄的事件,從而接收master進程的消息 ??ngx_worker_process_init(cycle,?-1);  ??ngx_memzero(&ev,?sizeof(ngx_event_t)); ??//?對于cache?manager,這里的handler指向的是ngx_cache_manager_process_handler()方法, ??//?對于cache?loader,這里的handler指向的是ngx_cache_loader_process_handler()方法 ??ev.handler?=?ctx->handler; ??ev.data?=?ident; ??ev.log?=?cycle->log; ??ident[3]?=?(void?*)?-1;  ??//?cache模塊不需要使用共享鎖 ??ngx_use_accept_mutex?=?0;  ??ngx_setproctitle(ctx->name);  ??//?把當前事件添加到事件隊列中,事件的延遲時間為ctx->delay,對于cache?manager,該值為0, ??//?而對于cache?loader,該值為60s。 ??//?需要注意的是,在當前事件的處理方法中,ngx_cache_manager_process_handler()如果處理完了當前事件, ??//?會將當前事件再次添加到事件隊列中,從而實現定時處理的功能;而對于 ??//?ngx_cache_loader_process_handler()方法,其處理完一次之后,并不會將當前事件 ??//?再次添加到事件隊列中,因而相當于當前事件只會執行一次,然后cache?loader進程就會退出 ??ngx_add_timer(&ev,?ctx->delay);  ??for?(?;;?)?{  ????//?如果master將當前進程標記為terminate或者quit狀態,則退出進程 ????if?(ngx_terminate?||?ngx_quit)?{ ??????ngx_log_error(ngx_log_notice,?cycle->log,?0,?"exiting"); ??????exit(0); ????}  ????//?如果master進程發出了reopen消息,則重新打開所有的緩存文件 ????if?(ngx_reopen)?{ ??????ngx_reopen?=?0; ??????ngx_log_error(ngx_log_notice,?cycle->log,?0,?"reopening?logs"); ??????ngx_reopen_files(cycle,?-1); ????}  ????//?執行事件隊列中的事件 ????ngx_process_events_and_timers(cycle); ??} }

上面的代碼中,首先創建了一個事件對象,ev.handler = ctx->handler;指定了該事件所需要處理的邏輯,也即上面兩個結構體中的第一個參數所對應的方法;然后將該事件添加到事件隊列中,即ngx_add_timer(&ev, ctx->delay);,需要注意的是,這里的第二個參數就是上面兩個結構體中所指定的第三個參數,也就是說這里是以事件的延遲時間的方式來控制hander()方法的執行時間的;最后,在一個無限for循環中,通過ngx_process_events_and_timers()方法來不斷檢查事件隊列的事件,并且處理事件。

3.3 cache manager進程處理邏輯

對于cache manager處理的流程,通過上面的講解可以看出,其是在其所定義的cache manager結構體中的ngx_cache_manager_process_handler()方法中進行的。如下是該方法的源碼:

static?void?ngx_cache_manager_process_handler(ngx_event_t?*ev)?{ ??ngx_uint_t??i; ??ngx_msec_t??next,?n; ??ngx_path_t?**path;  ??next?=?60?*?60?*?1000;  ??path?=?ngx_cycle-&gt;paths.elts; ??for?(i?=?0;?i?paths.nelts;?i++)?{  ????//?這里的manager方法指向的是ngx_http_file_cache_manager()方法 ????if?(path[i]-&gt;manager)?{ ??????n?=?path[i]-&gt;manager(path[i]-&gt;data);  ??????next?=?(n?<p>這里首先會獲取所有的路徑定義,然后檢查其manager()方法是否為空,如果不會空,則調用該方法。這里的manager()方法所指向的實際方法就是在前面3.1節中對proxy_cache_path指令進行解析中進行定義的,也即cache-&gt;path-&gt;manager = ngx_http_file_cache_manager;,也就是說該方法是管理cache的主要方法。在調用完了管理方法之后,接下來會繼續將當前的事件添加到事件隊列中,以進行下一次cache管理循環。如下是ngx_http_file_cache_manager()方法的源碼:</p><pre class="brush:bash;">static?ngx_msec_t?ngx_http_file_cache_manager(void?*data)?{ ??//?這里的ngx_http_file_cache_t結構體是解析proxy_cache_path配置項得到的 ??ngx_http_file_cache_t?*cache?=?data;  ??off_t????size; ??time_t???wait; ??ngx_msec_t?elapsed,?next; ??ngx_uint_t?count,?watermark;  ??cache-&gt;last?=?ngx_current_msec; ??cache-&gt;files?=?0;  ??//?這里的ngx_http_file_cache_expire()方法在一個無限循環中,不斷檢查緩存隊列尾部是否有過期的 ??//?共享內存,如果存在,則將其以及其所對應的文件進行刪除 ??next?=?(ngx_msec_t)?ngx_http_file_cache_expire(cache)?*?1000;  ??//?next是ngx_http_file_cache_expire()方法的返回值,該方法只有在兩種情況下才會返回0: ??//?1.?當刪除的文件個數超過了manager_files指定的文件個數時; ??//?2.?當刪除各個文件的總耗時超過了manager_threshold所指定的總時長時; ??//?如果next為0,則說明完成了一個批次的緩存清理工作,此時是需要休眠一段時間然后再進行下一次的清理工作, ??//?這個休眠的時長就是manager_sleep所指定的值。也就是說這里的next的值實際上就是下一次 ??//?執行緩存清理工作的等待時長 ??if?(next?==?0)?{ ????next?=?cache-&gt;manager_sleep; ????goto?done; ??}  ??for?(?;;?)?{ ????ngx_shmtx_lock(&amp;cache-&gt;shpool-&gt;mutex);  ????//?這里的size指的是當前緩存所使用的總大小 ????//?count指定了當前緩存中的文件個數 ????//?watermark則表示水位,其為總共能夠存儲的文件個數的7/8 ????size?=?cache-&gt;sh-&gt;size; ????count?=?cache-&gt;sh-&gt;count; ????watermark?=?cache-&gt;sh-&gt;watermark;  ????ngx_shmtx_unlock(&amp;cache-&gt;shpool-&gt;mutex);  ????ngx_log_debug3(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ????????????"http?file?cache?size:?%o?c:%ui?w:%i", ????????????size,?count,?(ngx_int_t)?watermark);  ????//?如果當前的緩存所使用的內存大小小于能夠使用的最大大小并且緩存文件個數小于水位, ????//?說明還可以繼續存儲緩存文件,則跳出循環 ????if?(size?max_size?&amp;&amp;?count??0)?{ ??????next?=?(ngx_msec_t)?wait?*?1000; ??????break; ????}  ????//?如果當前nginx已經退出或者終止,則跳出循環 ????if?(ngx_quit?||?ngx_terminate)?{ ??????break; ????}  ????//?如果當前刪除的文件個數超過了manager_files所指定的個數,則跳出循環, ????//?并且指定距離下次清理工作所需要休眠的時間 ????if?(++cache-&gt;files?&gt;=?cache-&gt;manager_files)?{ ??????next?=?cache-&gt;manager_sleep; ??????break; ????}  ????ngx_time_update();  ????elapsed?=?ngx_abs((ngx_msec_int_t)?(ngx_current_msec?-?cache-&gt;last));  ????//?如果當前刪除動作的耗時超過了manager_threshold所指定的時長,則跳出循環, ????//?并且指定距離下次清理工作所需要休眠的時間 ????if?(elapsed?&gt;=?cache-&gt;manager_threshold)?{ ??????next?=?cache-&gt;manager_sleep; ??????break; ????} ??}  done:  ??elapsed?=?ngx_abs((ngx_msec_int_t)?(ngx_current_msec?-?cache-&gt;last));  ??ngx_log_debug3(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ??????????"http?file?cache?manager:?%ui?e:%m?n:%m", ??????????cache-&gt;files,?elapsed,?next);  ??return?next; }

在ngx_http_file_cache_manager()方法中,首先會進入ngx_http_file_cache_expire()方法,該方法的主要作用是檢查當前共享內存隊列尾部的元素是否過期,如果過期,則根據其引用次數和是否正在被刪除而判斷是否需要將該元素以及該元素對應的磁盤文件進行刪除。在進行這個檢查之后,然后會進入一個無限for循環,這里循環的主要目的是檢查當前的共享內存是否資源比較緊張,也即是否所使用的內存超過了max_size定義的最大內存,或者是當前所緩存的文件總數超過了總文件數的7/8。如果這兩個條件有一個達到了,就會嘗試進行強制清除緩存文件,所謂的強制清除就是刪除當前共享內存中所有被引用數為0的元素及其對應的磁盤文件。這里我們首先閱讀ngx_http_file_cache_expire()方法:

static?time_t?ngx_http_file_cache_expire(ngx_http_file_cache_t?*cache)?{ ??u_char???????????*name,?*p; ??size_t????????????len; ??time_t????????????now,?wait; ??ngx_path_t?????????*path; ??ngx_msec_t??????????elapsed; ??ngx_queue_t?????????*q; ??ngx_http_file_cache_node_t?*fcn; ??u_char????????????key[2?*?ngx_http_cache_key_len];  ??ngx_log_debug0(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ??????????"http?file?cache?expire");  ??path?=?cache-&gt;path; ??len?=?path-&gt;name.len?+?1?+?path-&gt;len?+?2?*?ngx_http_cache_key_len;  ??name?=?ngx_alloc(len?+?1,?ngx_cycle-&gt;log); ??if?(name?==?null)?{ ????return?10; ??}  ??ngx_memcpy(name,?path-&gt;name.data,?path-&gt;name.len);  ??now?=?ngx_time();  ??ngx_shmtx_lock(&amp;cache-&gt;shpool-&gt;mutex);  ??for?(?;;?)?{  ????//?如果當前nginx已經退出了,或者終止了,則跳出當前循環 ????if?(ngx_quit?||?ngx_terminate)?{ ??????wait?=?1; ??????break; ????}  ????//?如果當前的共享內存隊列為空的,則跳出當前循環 ????if?(ngx_queue_empty(&amp;cache-&gt;sh-&gt;queue))?{ ??????wait?=?10; ??????break; ????}  ????//?獲取隊列的最后一個元素 ????q?=?ngx_queue_last(&amp;cache-&gt;sh-&gt;queue);  ????//?獲取隊列的節點 ????fcn?=?ngx_queue_data(q,?ngx_http_file_cache_node_t,?queue);  ????//?計算節點的過期時間距離當前時間的時長 ????wait?=?fcn-&gt;expire?-?now;  ????//?如果當前節點沒有過期,則退出當前循環 ????if?(wait?&gt;?0)?{ ??????wait?=?wait?&gt;?10???10?:?wait; ??????break; ????}  ????ngx_log_debug6(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ????????????"http?file?cache?expire:?#%d?%d?%02xd%02xd%02xd%02xd", ????????????fcn-&gt;count,?fcn-&gt;exists, ????????????fcn-&gt;key[0],?fcn-&gt;key[1],?fcn-&gt;key[2],?fcn-&gt;key[3]);  ????//?這里的count表示當前的節點被引用的次數,如果其引用次數為0,則直接刪除該節點 ????if?(fcn-&gt;count?==?0)?{ ??????//?這里的主要動作是將當前的節點從隊列中移除,并且刪除該節點對應的文件 ??????ngx_http_file_cache_delete(cache,?q,?name); ??????goto?next; ????}  ????//?如果當前節點正在被刪除,那么當前進程就可以不用對其進行處理 ????if?(fcn-&gt;deleting)?{ ??????wait?=?1; ??????break; ????}  ????//?走到這里,說明當前節點已經過期了,但是引用數大于0,并且沒有進程正在刪除該節點 ????//?這里計算的是該節點進行hex計算后文件的名稱 ????p?=?ngx_hex_dump(key,?(u_char?*)?&amp;fcn-&gt;node.key,?sizeof(ngx_rbtree_key_t)); ????len?=?ngx_http_cache_key_len?-?sizeof(ngx_rbtree_key_t); ????(void)?ngx_hex_dump(p,?fcn-&gt;key,?len);  ????//?由于當前節點在時間上已經過期了,但是有請求正在引用該節點,并且沒有進程正在刪除該節點, ????//?說明該節點應該被保留,因而這里嘗試將該節點從隊列尾部刪除,并且為其重新計算下次的過期時間, ????//?然后將其插入到隊列頭部 ????ngx_queue_remove(q); ????fcn-&gt;expire?=?ngx_time()?+?cache-&gt;inactive; ????ngx_queue_insert_head(&amp;cache-&gt;sh-&gt;queue,?&amp;fcn-&gt;queue);  ????ngx_log_error(ngx_log_alert,?ngx_cycle-&gt;log,?0, ???????????"ignore?long?locked?inactive?cache?entry?%*s,?count:%d", ???????????(size_t)?2?*?ngx_http_cache_key_len,?key,?fcn-&gt;count);  next:??//?這里是隊列中的最后一個節點被刪除,并且對應的文件也被刪除之后才會執行的邏輯  ????//?這里的cache-&gt;files記錄了當前已經處理的節點數,manager_files的含義在于, ????//?在進行lru算法強制清除文件時,最多會清除該參數所指定的文件個數,默認為100。 ????//?因而這里如果cache-&gt;files如果大于等于manager_files,則跳出循環 ????if?(++cache-&gt;files?&gt;=?cache-&gt;manager_files)?{ ??????wait?=?0; ??????break; ????}  ????//?更新當前nginx緩存的時間 ????ngx_time_update();  ????//?elapsed等于當前刪除動作的總耗時 ????elapsed?=?ngx_abs((ngx_msec_int_t)?(ngx_current_msec?-?cache-&gt;last));  ????//?如果總耗時超過了manager_threshold所指定的值,則跳出當前循環 ????if?(elapsed?&gt;=?cache-&gt;manager_threshold)?{ ??????wait?=?0; ??????break; ????} ??}  ??//?釋放當前的鎖 ??ngx_shmtx_unlock(&amp;cache-&gt;shpool-&gt;mutex);  ??ngx_free(name);  ??return?wait; }

可以看到,這里的主要處理邏輯是首先會火嘴隊列尾部的元素,根據lru算法,隊列尾部的元素是最有可能過期的元素,因而只需要檢查該元素即可。然后檢查該元素是否過期,如果沒有過期,則退出當前方法,否則檢查當前元素是否引用數為0,也就是說如果當前元素已經過期,并且引用數為0,則直接刪除該元素及其對應的磁盤文件。如果當前元素引用數不為0,則會檢查其是否正在被刪除,需要注意的是,如果一個元素正在被刪除,那么刪除進程是會將其引用數置為1的,以防止其他的進程也進行刪除操作。如果其正在被刪除,則當前進程不會處理該元素,如果沒有被刪除,則當前進程會嘗試將該元素從隊列尾部移動到隊列頭部,這么做的主要原因在于,雖然元素已經過期,但是其引用數不為0,并且沒有進程正在刪除該元素,那么說明該元素還是一個活躍元素,因而需要將其移動到隊列頭部。

下面我們來看一下,當資源比較緊張時,cache manager是如何強制清除元素的,如下是ngx_http_file_cache_forced_expire()方法的源碼:

static?time_t?ngx_http_file_cache_forced_expire(ngx_http_file_cache_t?*cache)?{ ??u_char???????????*name; ??size_t????????????len; ??time_t????????????wait; ??ngx_uint_t??????????tries; ??ngx_path_t?????????*path; ??ngx_queue_t?????????*q; ??ngx_http_file_cache_node_t?*fcn;  ??ngx_log_debug0(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ??????????"http?file?cache?forced?expire");  ??path?=?cache-&gt;path; ??len?=?path-&gt;name.len?+?1?+?path-&gt;len?+?2?*?ngx_http_cache_key_len;  ??name?=?ngx_alloc(len?+?1,?ngx_cycle-&gt;log); ??if?(name?==?null)?{ ????return?10; ??}  ??ngx_memcpy(name,?path-&gt;name.data,?path-&gt;name.len);  ??wait?=?10; ??tries?=?20;  ??ngx_shmtx_lock(&amp;cache-&gt;shpool-&gt;mutex);  ??//?不斷遍歷隊列中的每個節點 ??for?(q?=?ngx_queue_last(&amp;cache-&gt;sh-&gt;queue); ?????q?!=?ngx_queue_sentinel(&amp;cache-&gt;sh-&gt;queue); ?????q?=?ngx_queue_prev(q)) ??{ ????//?獲取當前節點的數據 ????fcn?=?ngx_queue_data(q,?ngx_http_file_cache_node_t,?queue);  ????ngx_log_debug6(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ?????????"http?file?cache?forced?expire:?#%d?%d?%02xd%02xd%02xd%02xd", ?????????fcn-&gt;count,?fcn-&gt;exists, ?????????fcn-&gt;key[0],?fcn-&gt;key[1],?fcn-&gt;key[2],?fcn-&gt;key[3]);  ????//?如果當前節點的引用數為0,則直接刪除該節點 ????if?(fcn-&gt;count?==?0)?{ ??????ngx_http_file_cache_delete(cache,?q,?name); ??????wait?=?0;  ????}?else?{ ??????//?進行下一個節點的嘗試,如果有連續的20個節點的引用數都大于0,則會跳出當前循環 ??????if?(--tries)?{ ????????continue; ??????}  ??????wait?=?1; ????}  ????break; ??}  ??ngx_shmtx_unlock(&amp;cache-&gt;shpool-&gt;mutex);  ??ngx_free(name);  ??return?wait; }

可以看到,這里的處理邏輯比較簡單,主要是從隊列尾部開始往前依次檢查隊列中的元素的引用次數是否為0,如果為0,則直接刪除,然后檢查下一個元素。如果不為0,則檢查下一個元素,如此往復。這里需要注意的是,如果檢查總共有20次元素正在被引用過程中,則跳出當前循環。

3.4 cache loader進程處理邏輯

前面已經講到,cache loader的主要處理流程在ngx_cache_loader_process_handler()方法中,如下是該方法的主要處理邏輯:

static?void?ngx_cache_loader_process_handler(ngx_event_t?*ev) { ??ngx_uint_t???i; ??ngx_path_t??**path; ??ngx_cycle_t??*cycle;  ??cycle?=?(ngx_cycle_t?*)?ngx_cycle;  ??path?=?cycle-&gt;paths.elts; ??for?(i?=?0;?i?paths.nelts;?i++)?{  ????if?(ngx_terminate?||?ngx_quit)?{ ??????break; ????}  ????//?這里的loader方法指向的是ngx_http_file_cache_loader()方法 ????if?(path[i]-&gt;loader)?{ ??????path[i]-&gt;loader(path[i]-&gt;data); ??????ngx_time_update(); ????} ??}  ??//?加載完成后退出當前流程 ??exit(0); }

這里cache loader與cache manager的處理主流程是非常相似的,主要是通過調用各個路徑的loader()方法進行數據加載的,而loader()方法的具體實現方法也是在proxy_cache_path配置項解析的時候定義的,具體的定義如下(在3.1節最后一部分):

cache->path->loader = ngx_http_file_cache_loader;

這里我們繼續閱讀ngx_http_file_cache_loader()方法的源碼:

static?void?ngx_http_file_cache_loader(void?*data)?{ ??ngx_http_file_cache_t?*cache?=?data;  ??ngx_tree_ctx_t?tree;  ??//?如果已經加載完成或者正在加載,則直接返回 ??if?(!cache-&gt;sh-&gt;cold?||?cache-&gt;sh-&gt;loading)?{ ????return; ??}  ??//?嘗試加鎖 ??if?(!ngx_atomic_cmp_set(&amp;cache-&gt;sh-&gt;loading,?0,?ngx_pid))?{ ????return; ??}  ??ngx_log_debug0(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ??????????"http?file?cache?loader");  ??//?這里的tree就是加載的一個主要流程對象,加載的過程是通過遞歸的方式進行的 ??tree.init_handler?=?null; ??//?封裝了加載單個文件的操作 ??tree.file_handler?=?ngx_http_file_cache_manage_file; ??//?在加載一個目錄之前的操作,這里主要是檢查當前目錄有沒有操作權限 ??tree.pre_tree_handler?=?ngx_http_file_cache_manage_directory; ??//?在加載一個目錄之后的操作,這里實際上是一個空方法 ??tree.post_tree_handler?=?ngx_http_file_cache_noop; ?//?這里主要是處理特殊文件,即既不是文件也不是文件夾的文件,這里主要是刪除了該文件 ??tree.spec_handler?=?ngx_http_file_cache_delete_file; ??tree.data?=?cache; ??tree.alloc?=?0; ??tree.log?=?ngx_cycle-&gt;log;  ??cache-&gt;last?=?ngx_current_msec; ??cache-&gt;files?=?0;  ??//?開始通過遞歸的方式遍歷指定目錄下的所有文件,然后按照上面定義的方法對其進行處理,也即加載到共享內存中 ??if?(ngx_walk_tree(&amp;tree,?&amp;cache-&gt;path-&gt;name)?==?ngx_abort)?{ ????cache-&gt;sh-&gt;loading?=?0; ????return; ??}  ??//?標記加載狀態 ??cache-&gt;sh-&gt;cold?=?0; ??cache-&gt;sh-&gt;loading?=?0;  ??ngx_log_error(ngx_log_notice,?ngx_cycle-&gt;log,?0, ?????????"http?file?cache:?%v?%.3fm,?bsize:?%uz", ?????????&amp;cache-&gt;path-&gt;name, ?????????((double)?cache-&gt;sh-&gt;size?*?cache-&gt;bsize)?/?(1024?*?1024), ?????????cache-&gt;bsize); }

在加載過程中,首先將目標加載目錄封裝到一個ngx_tree_ctx_t結構體中,并且為其指定加載文件所使用的方法。最終的加載邏輯主要是在ngx_walk_tree()方法中進行的,而整個加載過程也是通過遞歸來實現的。如下是ngx_walk_tree()方法的實現原理:

ngx_int_t?ngx_walk_tree(ngx_tree_ctx_t?*ctx,?ngx_str_t?*tree)?{ ??void????*data,?*prev; ??u_char???*p,?*name; ??size_t???len; ??ngx_int_t??rc; ??ngx_err_t??err; ??ngx_str_t??file,?buf; ??ngx_dir_t??dir;  ??ngx_str_null(&amp;buf);  ??ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ??????????"walk?tree?"%v"",?tree);  ??//?打開目標目錄 ??if?(ngx_open_dir(tree,?&amp;dir)?==?ngx_error)?{ ????ngx_log_error(ngx_log_crit,?ctx-&gt;log,?ngx_errno, ???????????ngx_open_dir_n?"?"%s"?failed",?tree-&gt;data); ????return?ngx_error; ??}  ??prev?=?ctx-&gt;data;  ??//?這里傳入的alloc是0,因而不會進入當前分支 ??if?(ctx-&gt;alloc)?{ ????data?=?ngx_alloc(ctx-&gt;alloc,?ctx-&gt;log); ????if?(data?==?null)?{ ??????goto?failed; ????}  ????if?(ctx-&gt;init_handler(data,?prev)?==?ngx_abort)?{ ??????goto?failed; ????}  ????ctx-&gt;data?=?data;  ??}?else?{ ????data?=?null; ??}  ??for?(?;;?)?{  ????ngx_set_errno(0);  ????//?讀取當前子目錄中的內容 ????if?(ngx_read_dir(&amp;dir)?==?ngx_error)?{ ??????err?=?ngx_errno;  ??????if?(err?==?ngx_enomorefiles)?{ ????????rc?=?ngx_ok;  ??????}?else?{ ????????ngx_log_error(ngx_log_crit,?ctx-&gt;log,?err, ???????????????ngx_read_dir_n?"?"%s"?failed",?tree-&gt;data); ????????rc?=?ngx_error; ??????}  ??????goto?done; ????}  ????len?=?ngx_de_namelen(&amp;dir); ????name?=?ngx_de_name(&amp;dir);  ????ngx_log_debug2(ngx_log_debug_core,?ctx-&gt;log,?0, ???????????"tree?name?%uz:"%s"",?len,?name);  ????//?如果當前讀取到的是.,則表示其為當前目錄,跳過該目錄 ????if?(len?==?1?&amp;&amp;?name[0]?==?'.')?{ ??????continue; ????}  ????//?如果當前讀取到的是..,則表示其為返回上一級目錄的標識,跳過該目錄 ????if?(len?==?2?&amp;&amp;?name[0]?==?'.'?&amp;&amp;?name[1]?==?'.')?{ ??????continue; ????}  ????file.len?=?tree-&gt;len?+?1?+?len;  ????//?更新可用的緩存大小 ????if?(file.len?+?ngx_dir_mask_len?&gt;?buf.len)?{  ??????if?(buf.len)?{ ????????ngx_free(buf.data); ??????}  ??????buf.len?=?tree-&gt;len?+?1?+?len?+?ngx_dir_mask_len;  ??????buf.data?=?ngx_alloc(buf.len?+?1,?ctx-&gt;log); ??????if?(buf.data?==?null)?{ ????????goto?failed; ??????} ????}  ????p?=?ngx_cpymem(buf.data,?tree-&gt;data,?tree-&gt;len); ????*p++?=?'/'; ????ngx_memcpy(p,?name,?len?+?1);  ????file.data?=?buf.data;  ????ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ????????????"tree?path?"%s"",?file.data);  ????if?(!dir.valid_info)?{ ??????if?(ngx_de_info(file.data,?&amp;dir)?==?ngx_file_error)?{ ????????ngx_log_error(ngx_log_crit,?ctx-&gt;log,?ngx_errno, ???????????????ngx_de_info_n?"?"%s"?failed",?file.data); ????????continue; ??????} ????}  ????//?如果當前讀取到的是一個文件,則調用ctx-&gt;file_handler()加載該文件的內容 ????if?(ngx_de_is_file(&amp;dir))?{  ??????ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ??????????????"tree?file?"%s"",?file.data);  ??????//?設置文件的相關屬性 ??????ctx-&gt;size?=?ngx_de_size(&amp;dir); ??????ctx-&gt;fs_size?=?ngx_de_fs_size(&amp;dir); ??????ctx-&gt;access?=?ngx_de_access(&amp;dir); ??????ctx-&gt;mtime?=?ngx_de_mtime(&amp;dir);  ??????if?(ctx-&gt;file_handler(ctx,?&amp;file)?==?ngx_abort)?{ ????????goto?failed; ??????}  ?????//?如果當前讀取到的是一個目錄,則首先調用設置的pre_tree_handler()方法,然后調用 ?????//?ngx_walk_tree()方法,遞歸的讀取子目錄,最后調用設置的post_tree_handler()方法 ????}?else?if?(ngx_de_is_dir(&amp;dir))?{  ??????ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ??????????????"tree?enter?dir?"%s"",?file.data);  ??????ctx-&gt;access?=?ngx_de_access(&amp;dir); ??????ctx-&gt;mtime?=?ngx_de_mtime(&amp;dir);  ??????//?應用讀取目錄的前置邏輯 ??????rc?=?ctx-&gt;pre_tree_handler(ctx,?&amp;file);  ??????if?(rc?==?ngx_abort)?{ ????????goto?failed; ??????}  ??????if?(rc?==?ngx_declined)?{ ????????ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ????????????????"tree?skip?dir?"%s"",?file.data); ????????continue; ??????}  ??????//?遞歸的讀取當前目錄 ??????if?(ngx_walk_tree(ctx,?&amp;file)?==?ngx_abort)?{ ????????goto?failed; ??????}  ??????ctx-&gt;access?=?ngx_de_access(&amp;dir); ??????ctx-&gt;mtime?=?ngx_de_mtime(&amp;dir);  ??????//?應用讀取目錄的后置邏輯 ??????if?(ctx-&gt;post_tree_handler(ctx,?&amp;file)?==?ngx_abort)?{ ????????goto?failed; ??????}  ????}?else?{  ??????ngx_log_debug1(ngx_log_debug_core,?ctx-&gt;log,?0, ??????????????"tree?special?"%s"",?file.data);  ??????if?(ctx-&gt;spec_handler(ctx,?&amp;file)?==?ngx_abort)?{ ????????goto?failed; ??????} ????} ??}  failed:  ??rc?=?ngx_abort;  done:  ??if?(buf.len)?{ ????ngx_free(buf.data); ??}  ??if?(data)?{ ????ngx_free(data); ????ctx-&gt;data?=?prev; ??}  ??if?(ngx_close_dir(&amp;dir)?==?ngx_error)?{ ????ngx_log_error(ngx_log_crit,?ctx-&gt;log,?ngx_errno, ???????????ngx_close_dir_n?"?"%s"?failed",?tree-&gt;data); ??}  ??return?rc; }

從上面的處理流程可以看出,真正的加載文件的邏輯在ngx_http_file_cache_manage_file()方法中,如下是該方法的源碼:

static?ngx_int_t?ngx_http_file_cache_manage_file(ngx_tree_ctx_t?*ctx,?ngx_str_t?*path)?{ ??ngx_msec_t???????elapsed; ??ngx_http_file_cache_t?*cache;  ??cache?=?ctx-&gt;data;  ??//?將文件添加到共享內存中 ??if?(ngx_http_file_cache_add_file(ctx,?path)?!=?ngx_ok)?{ ????(void)?ngx_http_file_cache_delete_file(ctx,?path); ??}  ??//?如果加載的文件個數超過了loader_files指定的個數,則休眠一段時間 ??if?(++cache-&gt;files?&gt;=?cache-&gt;loader_files)?{ ????ngx_http_file_cache_loader_sleep(cache);  ??}?else?{ ????//?更新當前緩存的時間 ????ngx_time_update();  ????//?計算當前加載炒作的耗時 ????elapsed?=?ngx_abs((ngx_msec_int_t)?(ngx_current_msec?-?cache-&gt;last));  ????ngx_log_debug1(ngx_log_debug_http,?ngx_cycle-&gt;log,?0, ????????????"http?file?cache?loader?time?elapsed:?%m",?elapsed);  ????//?如果加載操作耗時超過了loader_threshold所指定的時間,則休眠指定的時間 ????if?(elapsed?&gt;=?cache-&gt;loader_threshold)?{ ??????ngx_http_file_cache_loader_sleep(cache); ????} ??}  ??return?(ngx_quit?||?ngx_terminate)???ngx_abort?:?ngx_ok; }

這里的加載邏輯整體而言比較簡單,主要過程就是將該文件加載到共享內存中,并且會判斷加載的文件數量是否超限,如果超限了,則會休眠指定的時長;另外,也會判斷加載文件的總耗時是否超過了指定時長,如果超過了,也會休眠指定的時長。

? 版權聲明
THE END
喜歡就支持一下吧
點贊12 分享