Nginx源碼分析:核心模塊剖析及常見問題

Nginx 在解析完請求行和請求頭以後,一共定義了十一個階段,分別介紹以下php

HTTP 模塊工做原理

HTTP 處理的十一個階段定義前端

 

typedef enum { nginx

        NGX_HTTP_POST_READ_PHASE = 0, // 讀取請求內容階段   git

        NGX_HTTP_SERVER_REWRITE_PHASE, // Server請求地址重寫階段   github

        NGX_HTTP_FIND_CONFIG_PHASE, // 配置查找階段 apache

        NGX_HTTP_REWRITE_PHASE, // Location請求地址重寫階段 後端

        NGX_HTTP_POST_REWRITE_PHASE, // 請求地址重寫提交階段   數組

        NGX_HTTP_PREACCESS_PHASE, // 訪問權限檢查準備階段   緩存

        NGX_HTTP_ACCESS_PHASE, // 訪問權限檢查階段 tomcat

        NGX_HTTP_POST_ACCESS_PHASE, // 訪問權限檢查提交階段   

        NGX_HTTP_TRY_FILES_PHASE, // 配置項try_files處理階段 

        NGX_HTTP_CONTENT_PHASE, // 內容產生階段       

        NGX_HTTP_LOG_PHASE // 日誌模塊處理階段 

} ngx_http_phases ;

 

一、讀取請求內容階段

這個階段沒有默認的 handler,主要用來讀取請求體,並對請求體作相應的處理

 Server 請求地址重寫階段。這個階段主要是處理全局的 ( server block) 的 rewrite 規則。

 

二、配置查找階段

這個階段主要是經過 uri 來查找對應的 location。而後將 uri 和 location 的數據關聯起來。這個階段主要處理邏輯在 checker 函數中,不能掛載自定義的 handler。

 

三、Location 請求地址重寫階段

這個主要處理 location block 的 rewrite。

 

四、請求地址重寫提交階段

post rewrite,這個主要是進行一些校驗以及收尾工做,以便於交給後面的模塊。這個 phase 不能掛載自定義 handler。

 

五、訪問權限檢查準備階段

好比流控這種類型的 access 就放在這個 phase,也就是說它主要是進行一些比較粗粒度的 access。

 

六、訪問權限檢查階段

這個好比存取控制,權限驗證就放在這個 phase,通常來講處理動做是交給下面的模塊作的.這個主要是作一些細粒度的 access。

 

七、Location 請求地址重寫階段

這個主要處理 location block 的 rewrite。

 

八、訪問權限檢查提交階段

通常來講當上面的access模塊獲得access_code以後就會由這個模塊根據access_code來進行操做 這個phase不能掛載自定義handler

 

九、配置項 try_files 處理階段

try_file 模塊,也就是對應配置文件中的 try_files 指令。 這個 phase 不能掛載自定義 handler。按順序檢查文件是否存在,返回第一個找到的文件。結尾的斜線表示爲文件夾 -$uri/。若是全部的文件都找不到,會進行一個內部重定向到最後一個參數。

 

十、內容產生階段

內容處理模塊,產生文件內容,若是是 php,去調用 phpcgi,若是是代理,就轉發給相應的後端服務器

 

十一、日誌模塊處理階段

日誌處理模塊,是每一個請求最後必定會執行的。用於打印訪問日誌。

 

自定義的 handler 有時候能夠掛載在不一樣的 phase,均可以正常運行。自定義的 handler,若是依賴某一個 phase 的結果,則必須掛載在該 phase 後面的 phase 上。自定義的 handler 須要遵照 nginx 對不一樣 phase 的功能劃分,但不是必需的。除去 4 個不能掛載的 phase 和 log phase,還有以下 6 個 phase 能夠掛載

 

  1. NGX_HTTP_POST_READ_PHASE

  2. NGX_HTTP_SERVER_REWRITE_PHASE

  3. NGX_HTTP_REWRITE_PHASE

  4. NGX_HTTP_PREACCESS_PHASE

  5. NGX_HTTP_ACCESS_PHASE

  6. NGX_HTTP_CONTENT_PHASE

 

不少功能掛載在這 6 個 phase,均可以實現。掛載在越前面,咱們的性能會越好,掛載在後面,咱們的自由度會更大。好比說,若是咱們實如今 NGX_HTTP_POST_READ_PHASE 階段,咱們就不能用 location if 這些後面階段實現的指令來組合實現一些更復雜的功能。推薦使用:

 

  • NGX_HTTP_PREACCESS_PHASE

  • NGX_HTTP_ACCESS_PHASE

  • NGX_HTTP_CONTENT_PHASE

 

Nginx 在解析 HTTP 的配置時,會將多個 phase handler 數組,合成爲一個數組。handler 會在 postconfigure 階段初始化。實際調用過程當中執行:

 

void ngx_http_core_run_phases(ngx_http_request_t *r) { 

        ngx_int_t rc; 

        ngx_http_phase_handler_t *ph; 

        ngx_http_core_main_conf_t *cmcf;   

        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);   

        ph = cmcf->phase_engine.handlers;   

        while (ph[r->phase_handler].checker) {   

                rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);   

               if (rc == NGX_OK) { return; } 

         }

 }

 

實現經過調用 checker,再在 checker 中調用對應的 handler,實現對各個階段的調用。如下是不一樣的 phase,對應不一樣的 checker 函數

 

  • NGX_HTTP_POST_READ_PHASE ngx_http_core_generic_phase

  • NGX_HTTP_SERVER_REWRITE_PHASE ngx_http_core_rewrite_phase

  • NGX_HTTP_FIND_CONFIG_PHASE ngx_http_core_find_config_phase

  • NGX_HTTP_REWRITE_PHASE ngx_http_core_rewrite_phase

  • NGX_HTTP_POST_REWRITE_PHASE ngx_http_core_post_rewrite_phase

  • NGX_HTTP_PREACCESS_PHASE ngx_http_core_generic_phase

  • NGX_HTTP_ACCESS_PHASE ngx_http_core_access_phase

  • NGX_HTTP_POST_ACCESS_PHASE ngx_http_core_post_access_phase

  • NGX_HTTP_TRY_FILES_PHASE ngx_http_core_try_files_phase

  • NGX_HTTP_CONTENT_PHASE ngx_http_core_content_phase

  • NGX_HTTP_LOG_PHASE ngx_http_core_generic_phase

 

通常而言 handler 返回:

 

  • NGX_OK : 表示該階段已經處理完成,須要轉入下一個階段

  • NG_DECLINED : 表示須要轉入本階段的下一個handler繼續處理

  • NGX_AGAIN, NGX_DONE : 表示須要等待某個事件發生才能繼續處理(好比等待網絡IO),此時Nginx爲了避免阻塞其餘請求的處理,必須中斷當前請求的執行鏈,等待事件發生以後繼續執行該handler

  • NGX_ERROR:表示發生了錯誤,須要結束該請求。

 

checker 函數根據 handler 函數的不一樣返回值,給上一層的 ngx_http_core_run_phases 函數返回 NGX_AGAIN 或者 NGX_OK,若是指望上一層繼續執行後面的 phase 則須要確保 checker 函數不是返回 NGX_OK,不一樣 checker 函數對 handler 函數的返回值處理還不太同樣,開發模塊時須要確保相應階段的 checker 函數對返回值的處理在你的預期以內。

 

創建鏈接

在前面 event poll 初始化的時候,添加了事件方法爲 ngx_event_accept 在接受完 accept 請求後,最後一步將會調用 ls->handler(c) 該 hander 爲 ngx_http_init_connection,這個過程分爲:

  1. 初始化ngx_http_connection_t結構

  2. 設置可讀回調方法爲:ngx_http_wait_request_handler

  3. 設置可寫回調方法爲空:ngx_http_empty_handler

  4. 若是可讀已經就緒,則執行ngx_http_wait_request_handler

  5. 將可讀事件加入到定時器中以監控鏈接是否超時

  6. 將可讀事件添加到epoll中

 

第一次可讀事件處理

這時執行的是 ngx_http_wait_request_handler 這個步驟分爲:

  1. 判斷請求是否超時

  2. 建立接受緩衝區

  3. 建立並初始化ngx_http_request_t

  4. 更新ngx_http_request_t請求開始時間

  5. 調用ngx_http_process_request_line

 

接收請求行

這個步驟由 ngx_http_process_request_line 完成,因爲一次調用未必能所有讀取完成,因此,設置了:rev→handler = ngx_http_process_request_line; Top of Form,這個過程分爲:

  1. 判斷請求是否超時

  2. 讀取請求頭

  3. 解析請求行

  4. 處理請求uri

  5. 設置讀hander爲:ngx_http_process_request_headers;

  6. 處理請求頭

 

處理 HTTP 請求

處理完請求頭,最終調用 ngx_http_process_request:

  1. 因爲請求頭部已經接收完,因此刪除定時器 if (c->read->timer_set) { ngx_del_timer(c->read); }

  2. 因爲再也不接收請求行和頭部,讀寫回調所有設置爲:

    c->read->handler = ngx_http_request_handler; 

    c->write->handler = ngx_http_request_handler;

  3. 將 request 的 read_event_hander 設置爲何都不作:r->read_event_handler = ngx_http_block_reading;

  4. ngx_http_handler

    1. 根據internal標誌判斷是否要從定向

    2. 設置請求的寫事件hander爲ngx_http_core_run_phases,執行ngx_http_core_run_phases方法。r->write_event_handler = ngx_http_core_run_phases; ngx_http_core_run_phases(r)。這個過程執行十一個階段的hander而且調用checker,上面已經介紹過了。

  5. 調用ngx_http_run_posted_requests

     

處理子請求

ngx_http_run_posted_requests 方法根據鏈表順序調用了 write_event_handler

 

處理 HTTP 包體

該工具方法爲 ngx_http_read_client_request_body 執行過程以下:

  1. 原始請求計數器+1: r→main→count++

  2. 執行各模塊子請求的post_handler

  3. 接受ngx_http_request_body_t

  4. 假如一次性沒有接受完,則設置request的read_event_handler爲:ngx_http_read_client_request_body_handler

 

放棄處理 HTTP 包體

ngx_http_discard_request_body該方法只讀取上游body數據,但不保存。

 

發送 HTTP 響應

分別調用 head 和 body 的責任鏈:

 

ngx_int_t ngx_http_send_header(ngx_http_request_t *r) { 

        if (r->header_sent) { 

                ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, "header already sent"); 

                return NGX_ERROR; 

        }   

        if (r->err_status) { 

                r->headers_out.status = r->err_status; 

                r->headers_out.status_line.len = 0; 

        }   

        return ngx_http_top_header_filter(r);

}

 

ngx_int_t ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in) { 

        ngx_int_t rc; 

        ngx_connection_t *c;   

       c = r->connection;   

       ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,"http output filter \"%V?%V\"", &r->uri, &r->args);   

       rc = ngx_http_top_body_filter(r, in);   

       if (rc == NGX_ERROR) { /* NGX_ERROR may be returned by any filter */ 

               c->error = 1; 

       }   

      return rc;

}

 

這個也爲自定義 HTTP 過濾模塊的開發提供了可能。

 

結束 HTTP 請求

ngx_http_finalize_request 用於結束 HTTP 請求

 

整個 HTTP 請求的流程圖:

 

Upstream

upstream的工做過程:(以ngx memcached模塊爲例,此時上游服務器爲memcached):

 

1.模塊配置初始化的時候,設置配置的handler

 

2.http請求在ngx_http_core_find_config_phase階段,調用了:

ngx_http_update_location_config,將配置的handler設置爲:r->content_handler

(這裏爲:ngx_http_memcached_handler)

 

3.ngx_http_core_content_phase階段ngx_http_memcached_handler接管了請求

 

4.ngx_http_memcached_handler初始化:

a)u->input_filter_init = ngx_http_memcached_filter_init;

    u->input_filter = ngx_http_memcached_filter;

    u->input_filter_ctx = ctx;

 

b)ngx_http_upstream_create

c)ngx_http_upstream_init

d)ngx_http_upstream_init_request初始化請求

 

5.ngx_http_upstream_connect:

設置鏈接的讀寫回調函數:

    c->write->handler = ngx_http_upstream_handler;

    c->read->handler = ngx_http_upstream_handler;

 

    u->write_event_handler = ngx_http_upstream_send_request_handler;

    u->read_event_handler = ngx_http_upstream_process_header;

6.ngx_http_upstream_send_request_handler發送請求給上游服務器

 

7.ngx_http_upstream_process_header處理上游返回的頭:

若是subrequest_in_memory:

u->read_event_handler = ngx_http_upstream_process_body_in_memory

不然:

ngx_http_upstream_send_response:

 u->read_event_handler = ngx_http_upstream_process_non_buffered_upstream;

 r->write_event_handler = ngx_http_upstream_process_non_buffered_downstream;

 

 8.若是上游內容較多繼續調用ngx_http_upstream_process_non_buffered_upstream

 

 9.ngx_http_upstream_process_non_buffered_downstream發送數據給下游。

 

 注意:假以下游速度過慢,能夠經過u->buffering來控制是否緩存或者臨時文件保存上游的結果數據。

 

原理圖以下:

 

Nginx 鎖機制 & 原子性保證

 

原子性

ngx 的 atomic 的代碼以下:

 

static ngx_inline ngx_atomic_uint_tngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,ngx_atomic_uint_t set) {

        u_char res;

        __asm__ volatile (NGX_SMP_LOCK」 cmpxchgl %3, %1;」」 sete %0; 」 : 「=a」 (res) : 「m」 (*lock), 「a」 (old), 「r」 (set) : 「cc」, 「memory」);

        return res ; 

}

 

工做原理:

  • 在多核環境下,NGX_SMP_LOCK 其實就是一條 lock 指令,用於鎖住總線。

  • cmpxchgl 會保證指令同步執行。

  • sete 根據 cmpxchgl 執行的結果(eflags 中的 zf 標誌位)來設置 res 的值。

 

其中假如 cmpxchgl 執行完以後,時間片輪轉,這個時候 eflags 中的值會在堆棧中保持,這是 cpu task 切換機制所保證的,因此,等時間片切換回來再次執行 sete 的時候,也不會致使併發問題。

 

至於信號量,互斥鎖,最終還得依賴原子性的保證,具體鎖實現能夠有興趣本身再去閱讀源代碼。

 

Nginx cpu 親和度設置

 

例 1:worker_processes 4 ;  worker_cpu_affinity 0001 0010 0100 1000 ; 該配置實現將每一個CPU綁定到一個worker進程上。

例 2:worker_processes 2 ;  worker_cpu_affinity 0101 1010 ; 該配置實現將 1,3 號 CPU 綁定到worker1上,2,4 號 CPU 綁定到 worker2 上。

 

ngx 代碼實現

 

void ngx_setaffinity(uint64_t cpu_affinity, ngx_log_t *log) { 

        cpuset_t mask; 

        ngx_uint_t i;   

        ngx_log_error(NGX_LOG_NOTICE, log, 0, "cpuset_setaffinity(0x%08Xl)", cpu_affinity);   

        CPU_ZERO(&mask); 

        i = 0; 

        do { 

                 if (cpu_affinity & 1) { 

                        CPU_SET(i, &mask); 

                } 

                i++; 

                cpu_affinity >>= 1; 

        } 

        while (cpu_affinity) ;  

        if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cpuset_t), &mask) == -1)  { 

                ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "cpuset_setaffinity() failed"); 

        } 

}

 

經過位運算而後獲得 cpu 號存入 mask ,最終調用 cpuset_setaffinity 讓 cpu 和當前線程掛鉤。

 

 

Nginx 模塊開發方法

nginx 自定義模塊能夠分紅幾種模塊:

 

 

  • ngx http過濾模塊。能夠參考:https://github.com/lingqi1818/beacon/blob/master/ngx_http_beacon_module.c。原理主要改變了 ngx_http_output_header_filter_pt 和 ngx_http_output_body_filter_pt 的指針

  • ngx http模塊。能夠參考:http://blog.csdn.net/poechant/article/details/7627828

 

Nginx 實戰

靜態服務器

用於替換apache相同的靜態資源處理功能,目前世界上大多數網站都已經採用了nginx服務器。

 

靜態資源合併

例如:http://static.helijia.com/js/a.js,js/b.js,js/c.js,這塊 tengine 已經實現,無需本身開發模塊。

 

這樣作的好處是,可讓客戶端減小與服務端的請求次數,一次請求獲取多個靜態資源文件內容。

 

動態更新CDN版本號,結合 inotify

 

單機多個 tomcat 進程負載分發

不少時候,在單機部署多個jetty/tomcat這樣的服務時,能夠藉助nginx來作負載均衡:

上圖中的normandy即爲部署在jetty/tomcat中的登錄服務。

 

前端 abtest 種 cookie:

例如:1 if ngx.var.cookie_login_test == nil then

  2    local r = math.random(100);

  3    ngx.log(ngx.ERR,r)

  4    if r >=1 and r <=90 then

  5    ngx.header["Set-Cookie"]={"login_test:0;expires=Thu, 31 Dec 2115 23:59:59 GMT;max-age=3153600000;domain=xxx   ;path=/"}

  6    else

  7    ngx.header["Set-Cookie"]={"login_test:1;expires=Thu, 31 Dec 2115 23:59:59 GMT;max-age=3153600000;domain=xxx    ;path=/"}

  8    end

  9 end

 

經過上面代碼返回不一樣機率的 login_test 值,讓客戶端的登錄頁面產生不一樣的內容。

 

後端 abtest 根據規則分發請求

結合upstream模塊的開發,可讓後ngx根據不一樣的cookie或者請求數據,來發送給不一樣版本的後端服務器。

 

ngx_lua 腳本

目前有兩套解決方案:

  • 一套是淘寶寫的ngx_lua模塊,淘寶的模塊其實經過對ngx嵌入lua功能,加強了ngx_body_filter和ngx_head_fiter的過程,對於簡單的請求過濾處理能夠作到很是輕量級的開發。

  • 另外一套是OpenResty,OpenResty則功能更強大些,提供的能力也更爲複雜,具體使用哪一個能夠根據不一樣的需求場景本身作判斷並選擇。

 

Q & A

一、可否簡單對比下 Apache 和 Nginx?

  • 工做模式的不一樣。ngx 全部的請求都是異步,非阻塞的方式,而且採用了 epoll 模型。Apache 是線程池的模型。

  • Apache 依賴了不少三方庫,ngx 則是所有本身實現。

  • ngx 的設計所有是模塊化的,比較輕量級。

  • ngx 配置比較簡潔。

  • ngx 對機器的利用率較高,一切設計都是爲了節省內存和提升性能。

 

二、請問Nginx重加載配置是如何處理老配置和當前請求的?

ngx 是經過 SIGHUP 信號來 reload 配置的,這個過程由 master 進程負責。2.8處理 SIGHUP 信號,2.8.1 平滑升級,重啓 worker 進程。

 

if (ngx_new_binary) {

        ngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);

        ngx_start_cache_manager_processes(cycle, 0);

        ngx_noaccepting = 0;

        continue;

}

 

2.8.2不是平滑升級,須要從新讀取配置

...

    cycle = ngx_init_cycle(cycle);

 

    if (cycle == NULL) {

            cycle = (ngx_cycle_t *) ngx_cycle;

            continue;

    }

 

        ngx_cycle = cycle;

        ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,

        ngx_core_module);

        ngx_start_worker_processes(cycle, ccf->worker_processes,

        NGX_PROCESS_JUST_RESPAWN);

        ngx_start_cache_manager_processes(cycle, 1);

 

    /* allow new processes to start */

        ngx_msleep(100);

        live = 1;

        ngx_signal_worker_processes(cycle,

        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

}

 

這個過程會重啓工做線程。

 

三、使用線程池後,Nginx 的性能提高 9 倍是真的嗎?

我認爲線程池能夠從某種程度提高響應時間,畢竟單個線程處理多個請求,速度再快也是先來後到排序的。固然,這個提高也須要在編寫模塊的時候所有符合做者異步,非阻塞的思路,好比轉成 subrequest。

 

四、用 Nginx 自己的模塊來作 ddos 攻擊是否合適?若是不合適業界是否有成型的 Nginx 解決方案 or 其它,想參考一下。目前咱們的作法是用的後臺生成驗證碼的方式(app)

ngx 作 ddos 攻擊的話,就須要在 tcp 層來作了。不能直接編寫 ngx http 模塊的方法。不然失去了抗 ddos 攻擊的意義。

 

至於業界的方案這個我到不是很清楚,以前在阿里有個基於 Apache 的 humock,是在 tcp 層作的防攻擊。

 

五、Nginx 不是多進程併發處理麼?

ngx 的多個進程是指多個工做進程,分別來管理一份 epoll 事件池的。

經過這樣的方式能夠儘可能利用 cpu,並且 ngx 的所有設計思路就是圍繞異步,非阻塞。相似 Redis 的事件機制。

 

六、不是說靜態文件處理,Apache 比 Nginx 更有優點嗎?

以前我用 ab 作過性能測試,一個 1M 大小的頁面,性能 ngx 比 Apache 更強一些。不知道這個 Apache 比 ngx 更有優點的結論是如何的出來的。理論上 epoll 模型會比多線程模型開銷更小一些。

 

七、針對問題 5,是說每一個工做進程再搞個線程池?

每一個工做進程不須要線程池,epoll 就是個事件池子,等事件就緒以後,就能夠回調你以前註冊的回調函數

 

八、若是分配工做進程數和 cpu 核數匹配,由於工做進程的工做方式是異步的,不存在阻塞,因此再每一個工做進程再搞個線程池有意義麼?,由於 cpu 核都在 run,沒有空閒。

我以爲仍是有意義的,畢竟 CPU 不會給你那幾個進程獨佔,時間片是會切換的,線程池能夠提升吞吐率。固然這須要作下測試,另外須要看下使用場景。

 

九、針對問題3,使用線程池,是那塊使用呢?

Nginx 引入線程池是爲了解決由於某些長時間阻塞的調用致使性能降低的問題。能夠在編譯的時候加選項開啓線程池。

 

十、Nginx的高性能,高吞吐。最主要由什麼設計決定呢? 異步非阻塞?

異步非阻塞,絕對是一個核心點,另外 ngx 對任何使用內存的地方很是摳門,都是在框架上搞定了。而且能不使用就不使用,因此在你本身編寫模塊的時候,你都是從 pool 中拿到的內存。釋放也是 ngx 幫你搞定了。並且 ngx 全部的數據結構都是結合場景精心設計的。脫離 ngx 這個場景,你在其餘地方都會以爲很怪異。

相關文章
相關標籤/搜索