一張腦圖說清 Nginx 的主流程


一張腦圖說清 Nginx 的主流程

這個腦圖在 nginx-1.14.0-research 上。這是我在研究nginx的http模塊的時候畫的。基本上把 Nginx 主流程(特別是 HTTP 的部分)的關鍵函數和關鍵設置畫了下來,了解了這個腦圖,就對整個 Nginx 的主流程有了定性的了解了。

Nginx 的啟動過程分為兩個部分,一個部分是讀取配置文件,做配置文件中配置的一些事情(比如監聽端口等)。第二個部分是形成 Master-Worker 的多進程模型。這兩個過程就是 Nginx 代碼中最重要的兩個函數:ngx_init_cyclengx_master_process_cycle

ngx_init_cycle

ngx_init_cycle 是 Nginx 中最重要的函數,沒有之一。我們可以想想,如果我們寫一個和 Nginx 一樣的 Web 服務,我們會怎么做?我們大致的思路一定是解析配置文件,把配置文件存入到一個數據結構中,然后根據數據結構,進行端口監聽。是的,差不多,Nginx 就是這么一個流程。不過 Nginx 里面有個模塊的概念,所有的功能都是用模塊的方式進行加載的。

Nginx 的模塊

Nginx 的模塊分為幾類,這幾類分別為 Core,Event,Conf,Http,Mail。看名字就知道 Core 模塊是最重要的。模塊是什么意思呢?它包含一堆命令(cmd)和命令對應的處理函數(cmd->handler),我們根據配置文件中的配置(token)就知道這個配置是屬於哪個模塊的哪個命令,然后調用命令對應的處理函數來處理或者設置我們的服務。

這幾類模塊中,Core 模塊是 Nginx 啟動的時候一定會加載的,其他的模塊,只有在解析配置的時候,遇到了這個模塊的命令,才會加載對應的模塊。
這個也是體現了 Nginx 按需加載的理念。(昨天還和小組成員討論,如果我們寫的話,可能就會先把所有模塊都加載,然后根據配置文件進行匹配,這樣可能 Nginx 的啟動過程和進程資源就變大了)。

模塊的另一個問題是我這個 Nginx 最多有哪些模塊的能力呢?這個是編譯的時候就決定了,Nginx 的編譯過程可以參考這篇文章 。我們可以不用管./configure 的時候的具體內容,但是我們最關注的就是 objs/ngx_modules.c 這個編譯出來的文件,里面有個ngx_modules全局變量,這個變量里面就存放了我們這次編譯的 Nginx 最多可以支持的模塊。

模塊的結構是我們需要關注的另外一個問題。 Nginx 中模塊的結構叫做ngx_module_s(你或許會看到ngx_module_t,其實就是struct ngx_moudle_s的縮寫)

里面有個結構*ctx,對於不同的模塊類型,這個ctx指向的結構是不一樣的,我們這里最主要是研究 HTTP 類型的模塊,所以我們就記得 HTTP 模塊指向的結構是ngx_http_module_t

主流程

了解了 Nginx 的模塊概念,我們再回到ngx_init_cycle函數

這個函數里面做了幾個事情:

  • ngx_cycle_modules,它本質就是把objs/ngx_modules.c里面的全局變量拷貝到cycle這個全局變量里面
  • 調用了每個 Core 類型模塊的create_conf方法
  • ngx_conf_parse 解析配置文件,調用每個Core 類型模塊的init_conf方法
  • 調用了每個 Core 類型模塊的init_conf方法
  • ngx_open_listening_sockets 打開配置文件中設置的監聽端口和IP
  • ngx_init_modules 調用每個加載模塊的init_module方法

create_conf是創建一些模塊需要初始化的結構,但是這個結構里面並沒有具體的值。init_conf是往這些初始化結構里面填寫配置文件中解析出來的信息。

其中的ngx_conf_parse是真正解析配置文件的。

在代碼ngx_open_listening_sockets里面我們看到熟悉的bind,listen的命令。所以 Nginx 是如何多個進程同時監聽一個80端口的?本質是啟動了一個master進程,在ngx_init_cycle里面監聽了端口,然后在ngx_master_process_cycle里面 fork 出來多個 worker 子進程。

ngx_conf_parse

這個函數是非常非常重要的。

它的邏輯,就是這兩步,首先使用函數ngx_conf_read_token先循環逐行逐字符查找,看匹配的字符,獲取出cmd, 然后去所有的模塊查找對應的cmd,調用那個查找后的cmd->set方法。用Http模塊舉例子,我們的配置文件中一定有且只有一個關鍵字叫http

http{

}

先解析這個配置的時候發現了http這個關鍵字,然后去各個模塊匹配,發現ngx_http_module這個模塊包含了http命令。它對應的set方法是ngx_http_block。這個方法就是http模塊非常重要的方法了。當然,這里順帶提一下,event模塊也有類似的方法,ngx_events_block。它具體做的事情就是解析

event epoll

這樣的命令,並創建出事件驅動的模型。

ngx_http_block

這個函數是http模塊加載的時候最重要的函數,首先,它會遍歷modules.c中的所有 http 模塊,還記得上文說的,HTTP 模塊結構ngx_module_s中的**ctx 指向的是 ngx_http_module

第一步,模塊對三個層級的回調

它內部有這個 HTTP 模塊定義的,在各個層級(http,server,location)所需要加載回調的方法。

我們這里再附帶說一下 HTTP 的三個層級,這三個層級對應我們配置文件里面的三個不同的 Block 語句。

http {
  server {
    listen       80;
    location {
      root   html;
      index  index.html;
    }
  }
}

這三個層次里面的命令有可能會有重復,有沖突。比如,root這個命令,在 location 中可以有,在 server 中也可以有,如果賦值不一致的化,是上層覆蓋下層,還是下層覆蓋上層(當然大部分都是下層覆蓋上層)。這個就在具體的模塊定義的ngx_http_module結構中定義了 create_main_conf, create_srv_conf,...,merge_srv_conf等方法。這些方法的調用就是在ngx_http_block方法的第一步進行調用的。

第二步,設置連接回調和請求監聽回調

第二步是調用方法ngx_http_optimize_servers。它對配置文件中的所有listening的端口和IP進行監聽設置。記住,這里只是進行回調的設置,具體的listeningbinding操作不是在ngx_conf_parse中,而是在ngx_open_listening_sockets 中。

這個XMind中有標注(xxx時候回調)的分支就是只有在事件回調的時候會進行調用,不是在ngx_conf_parse的時候調用的。

我們再仔細看看這個腦圖中的流程,在conf_parse的時候,我實際上只對HTTP連接的時候設置了一個回調函數(ngx_http_init_connection)。在有HTTP連接上來的時候,才會設置讀請求的回調(ngx_http_wait_request_handler)。在這個回調,才是真正的解析 HTTP 請求的請求頭,請求體等。nginx 中著名的11階段就是在這個地方進行一個個步驟進行調用的。

這里說一下回調。nginx 是由各種各樣的回調組合起來的。回調就需要要求有一個事件驅動機制。在nginx中,這個事件驅動機制也是一個模塊,event 模塊。在編譯的時候,編譯程序會判斷你的系統支持哪些事件驅動,比如我的是centos,支持的是epoll,在配置文件配置

event epoll;

之后,就用這個epoll事件驅動監聽IO事件。其他模塊和事件驅動的交互就是通過ngx_add_event進行事件監聽和回調的。

http請求由於可能請求體或者返回體比較大,所以不一定會在一個事件中完成,為了整體的 nginx 高效,http 模塊在處理 http請求的時候,處理完成了一個event回調函數之后,如果沒有處理完成整個HTTP,就會在event中繼續注冊一個回調,然后把處理權和資源都交給事件驅動中心。等待事件驅動下一次觸發回調。

第三步,初始化定義 HTTP 的11個處理階段

HTTP請求在nginx中會經過11個處理階段和他們的checker方法:

  • NGX_HTTP_POST_READ_PHASE階段(ngx_http_core_generic_phase)
  • NGX_HTTP_SERVER_REWRITE_PHASE階段(ngx_http_rewrite_handler)
  • NGX_HTTP_FIND_CONFIG_PHASE階段(ngx_http_core_find_config_phase)
  • NGX_HTTP_REWRITE_PHASE階段(ngx_http_rewrite_handler)
  • 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_log_module中的ngx_http_log_handler)

就像把大象放冰箱需要幾步,處理 HTTP 需要11步,這11個步驟,有的是處理配置文件,有的是處理rewrite,有的是處理權限,有的是處理日志。所以,如果我們要自己開發一個http模塊,我們就需要定義我們這個http模塊處理http請求的這11個階段(當然並不是這11個階段都可以被自定義的,有的階段是不能被自定義模塊設置的)。然后當一個請求進來的時候,就按照順序把請求經過所有模塊的這11個階段。

這里所謂的經過這些11個階段本質上就是調用他們的 checker 方法。這些checker方法除了最后一個 NGX_HTTP_LOG_NGX_HTTP_LOG_PAHSE 是在 ngx_http_log_module 里面之外,其他的都是在 http 的 core 模塊中定義好了。

其他

這里其他的函數調用就沒有特別需要注意的了。

ngx_http_wait_request_handler

我們繼續跟着ngx_http_optimize_servers,ngx_http_init_listening,ngx_http_add_listening,ngx_http_init_connection 進入到處理http請求內容的函數里面。

這個函數先調用recv將http請求的數據獲取到,然后調用ngx_http_create_request創建了 HTTP 的請求結構體。

首先nginx處理的是HTTP的第一行,就是HTTP 1.0 GET, 從這一行,HTTP 會獲取到協議,方法等。接着再調用ngx_http_process_request_line一行一行處理請求頭。ngx_http_read_request_headerngx_http_process_request_headers。處理完成 http header 頭之后,ngx_http_process_request 再接着進行后續的處理

ngx_http_process_request 設置了讀和寫的handler,並且把監聽事件從epoll事件驅動隊列中拿出來(ngx_http_block_reading),就代表這個時候是阻塞做事件處理的事情。然后再調用ngx_http_core_run_phases來讓請求經過那11個階段。

其實並不是所有請求都需要讀取整個HTTP請求體。比如你只是獲取一個css文件,nginx在11個階段中的配置讀取階段就不會再繼續讀取HTTP的body了。但是如果是一個fastcgi請求,在http_fastcgi的模塊中,就會進入到NGX_HTTP_CONTENT_PHASE階段,在這個階段,它做的一個事情就是循環讀取讀緩存區的數據,直到讀取完畢,然后進行處理,再返回結構。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM