五分鐘學 NGINX - 詳解 Nginx 如何處理 HTTP 頭部
Nginx 作爲高性能的 HTTP 服務器和反向代理服務器,在處理 HTTP 請求時,對 HTTP 頭部的處理是至關重要的一環。
接收請求事件模塊
Nginx 使用了一個事件驅動的架構,這使得它能夠高效地處理大量的併發連接。下面是 Nginx 處理 HTTP 請求的詳細流程:
1. 建立連接
三次握手:客戶端通過發送 SYN 包開始與 Nginx 建立 TCP 連接。Nginx 響應 SYN+ACK 包,客戶端再次發送 ACK 包完成握手。
負載均衡:如果有多個 worker 進程在監聽相同的端口,操作系統的負載均衡機制會選擇一個 worker 進程來處理新的連接。
讀事件:一旦連接建立,Nginx 的事件模塊會監聽來自客戶端的數據。當有數據到達時,操作系統會通知 Nginx。
Nginx Worker 負載均衡選擇
Nginx 使用事件驅動和異步 I/O 來處理請求,它的工作進程(worker processes)可以並行處理多個客戶端連接。當多個工作進程監聽相同的端口時,操作系統的負載均衡機制會介入:
-
操作系統負載均衡:現代操作系統(如 Linux)通常具備內核級別的負載均衡功能。當多個進程監聽同一個端口時,操作系統內核會使用特定的算法(如 round-robin)來分配新的連接請求到不同的工作進程。這種機制在多個 Nginx 工作進程監聽同一個端口時非常有效。
-
Nginx 工作進程模型:Nginx 的配置文件中可以設置
worker_processes
指令,這決定了 Nginx 將啓動多少個工作進程。每個工作進程都是獨立的,並且能夠處理自己的連接和請求。
讀事件監聽與處理
一旦 TCP 連接建立,Nginx 需要準備接收客戶端發送的數據。這是通過 Nginx 的事件模塊來實現的:
-
I/O 多路複用:Nginx 使用
epoll
(或其他類似的 I/O 多路複用技術)來同時監控多個網絡套接字上的事件。epoll
允許 Nginx 以非阻塞的方式檢測哪些套接字上有數據可讀。 -
事件通知:當操作系統檢測到某個網絡套接字上有數據到達時,
epoll
會通知 Nginx。Nginx 的事件模塊會捕獲這個事件,並將事件加入到事件隊列中。 -
事件處理:Nginx 會從事件隊列中取出事件,並調用相應的處理函數來讀取數據。在 Nginx 的源碼中,這個處理函數通常是
ngx_event_accept
,它會處理新的連接請求。
2. 接收請求
epoll_wait:Nginx 使用
epoll_wait
系統調用來等待 I/O 事件的發生,如客戶端發送的數據到達。讀取請求:當
epoll_wait
檢測到讀事件時,Nginx 會調用ngx_http_wait_request_handler
來讀取客戶端發送的 HTTP 請求數據。
epoll_wait 系統調用
epoll_wait
是 Linux 系統中用於等待 I/O 事件的系統調用,它是 epoll
I/O 多路複用機制的一部分。Nginx 使用 epoll
來監控大量的網絡套接字,以檢測哪些套接字上有數據可讀或可寫。
-
事件循環:Nginx 通過
epoll_wait
進入一個事件循環,在這個循環中,Nginx 會阻塞等待事件發生。當epoll_wait
返回時,它提供了一組就緒的文件描述符(即套接字),這些套接字上的數據已經準備好讀取或寫入。 -
非阻塞 I/O:由於
epoll
是非阻塞的,Nginx 可以在等待事件發生時執行其他任務,例如處理其他連接或執行定時任務。
讀取請求數據
一旦 epoll_wait
檢測到讀事件,Nginx 將調用相應的處理函數來讀取客戶端發送的數據。這個過程在 Nginx 源碼中是由 ngx_http_wait_request_handler
函數負責的。
-
請求處理鏈:
ngx_http_wait_request_handler
函數是請求處理鏈的一部分,它負責從客戶端讀取請求行和請求頭。 -
緩衝區管理:讀取到的數據會被存儲在 Nginx 配置的緩衝區中,這個緩衝區由
client_header_buffer_size
和large_client_header_buffers
指令控制其大小。 -
狀態機解析:Nginx 使用內部的狀態機來解析請求行和請求頭。狀態機根據 HTTP 協議的規範逐步解析請求數據,並將其存儲在
ngx_http_request_t
結構體中。 -
請求上下文:解析過程中,Nginx 會爲每個請求創建一個請求上下文,其中包含了請求的所有信息,如方法、URI、頭部字段等。這個上下文會在請求的整個生命週期中被使用。
3. 分配內存資源
分配連接內存池:Nginx 會爲每個新的連接分配一個連接內存池,其大小由
connection_pool_size
配置指令指定,默認爲 512 字節。設置回調方法:Nginx 會設置回調方法
ngx_http_init_connection
來處理新的連接,並將其讀事件加入到epoll
監控中。添加超時定時器:爲了防止客戶端長時間不發送請求,Nginx 會添加一個超時定時器
client_header_timeout
,默認爲 60 秒。
分配連接內存池
Nginx 使用內存池來管理連接相關的數據,這樣可以提高內存使用的效率並減少內存分配和釋放的開銷。
-
connection_pool_size:這個配置指令定義了每個連接的連接內存池的大小。默認情況下,這個大小設置爲 512 字節,足以存儲大多數連接狀態信息。
-
連接內存池的結構:在 Nginx 源碼中,
ngx_connection_t
結構體代表了單個連接,它包含了連接的狀態、套接字文件描述符、地址信息等。ngx_connection_t
結構體中的某些字段會使用連接內存池來存儲數據。
設置回調方法
Nginx 通過設置回調方法來處理新的連接,這是事件驅動編程的一個重要部分。
-
ngx_http_init_connection:這個回調方法在新的連接建立時被調用,它負責初始化連接狀態、設置讀取事件的處理函數,並準備接收客戶端的 HTTP 請求。
-
epoll 監控:
ngx_http_init_connection
會將新連接的讀取事件註冊到epoll
系統中,這樣當有數據可讀時,epoll
能夠通知 Nginx。
添加超時定時器
爲了防止客戶端長時間不發送請求,Nginx 會爲每個連接設置一個超時定時器。
-
client_header_timeout:這個配置指令定義了客戶端發送請求頭的超時時間,默認爲 60 秒。如果在這個時間內客戶端沒有發送任何數據,Nginx 會認爲連接已經超時,並關閉連接。
-
超時處理:在源碼中,超時處理通常由
ngx_http_request_handler
或類似的處理函數來管理。當超時發生時,Nginx 會停止等待客戶端的數據,並關閉連接。
4. 用戶正式請求
讀取數據:Nginx 讀取客戶端發送的數據,並將其存儲在讀緩衝區中。
分配讀緩衝區:讀緩衝區的大小由
client_header_buffer_size
配置指令指定,默認爲 1KB。(這個值也不是越大越好,因爲當用戶有一個請求進來的時候,nignx 就會分配 1kb 內存出來,)
在 Nginx 處理用戶正式請求的過程中,讀取數據和分配讀緩衝區是兩個基礎而關鍵的步驟。這兩個步驟確保了 Nginx 能夠接收和存儲客戶端發送的 HTTP 請求數據。以下是結合 Nginx 底層原理與源碼對這兩個步驟的詳細展開:
讀取數據
Nginx 通過讀取客戶端發送的數據來開始處理 HTTP 請求。這個過程是在 I/O 事件觸發時進行的,通常是在 epoll
事件循環中,當檢測到讀事件(即客戶端發送數據)時,Nginx 會執行以下操作:
-
讀取數據到緩衝區:Nginx 使用
read
系統調用來從網絡套接字讀取數據。讀取的數據被存儲在一個特定的緩衝區中,這個緩衝區稱爲讀緩衝區。 -
處理分片數據:由於網絡傳輸的特性,客戶端發送的數據可能會被分片(即分成多個數據包)。Nginx 需要處理這些分片,以便正確地重組 HTTP 請求。
分配讀緩衝區
讀緩衝區是用來臨時存儲從客戶端接收到的數據的內存區域。Nginx 爲每個連接分配一個讀緩衝區,以便存儲請求頭和請求體。
-
client_header_buffer_size:這個配置指令定義了讀緩衝區的初始大小,默認爲 1KB。這個大小適用於大多數標準的 HTTP 請求頭。
-
動態擴展:雖然默認大小爲 1KB,但 Nginx 的讀緩衝區是可以根據需要動態擴展的。如果接收到的請求頭超過了 1KB,Nginx 會根據需要分配更多的內存。這種動態分配機制確保了 Nginx 能夠有效地處理各種大小的請求,同時避免了不必要的內存浪費。
-
內存管理:在 Nginx 源碼中,內存管理是通過
ngx_pool_t
結構體來實現的,它提供了內存分配和釋放的功能。ngx_palloc
函數用於從內存池中分配內存,而ngx_pfree
用於釋放內存。
上面是 nginx 處理連接,下面我們來看下 nginx 處理請求的過程,處理請求的過程跟處理連接是不一樣的,因爲系統需要進行大量的上下文分析,分析 http 協議跟 http 的 header 信息。
接收請求 HTTP 模塊
1. 解析請求
狀態機解析請求行:Nginx 使用狀態機來解析客戶端發送的 HTTP 請求行,這包括請求方法、URI 和 HTTP 版本。
接收 URI 和 Header:Nginx 繼續讀取請求的 URI 和 Header 信息。
在 Nginx 的工作流程中,解析請求是一個至關重要的步驟,它涉及到從客戶端接收的原始 HTTP 請求中提取出有用的信息,如請求方法、URI 和 HTTP 版本等。這一過程是通過狀態機來實現的,狀態機是一種編程模式,用於按順序處理輸入數據,這裏是指 HTTP 請求的不同部分。以下是結合 Nginx 底層原理與源碼對解析請求過程的詳細展開:
狀態機解析請求行
Nginx 使用內部的狀態機來逐行解析客戶端發送的 HTTP 請求。狀態機的主要任務是識別請求行中的各個組成部分,並將其存儲在相應的數據結構中。
-
請求行的結構:HTTP 請求行通常包含三個部分:請求方法(如 GET、POST)、請求的資源路徑(URI)和 HTTP 版本(如 HTTP/1.1)。
-
狀態機的工作方式:狀態機根據當前讀取的字符和預定義的規則(如空格分隔方法和 URI)來確定請求的各個部分。狀態機在 Nginx 源碼中通常由一系列函數和跳轉表組成,這些函數會根據解析的進度調用彼此。
-
錯誤處理:如果在解析過程中遇到不符合 HTTP 協議規範的數據,狀態機會觸發錯誤處理機制,這可能導致請求被拒絕或產生 400 錯誤響應。
接收 URI 和 Header
在請求行被成功解析之後,Nginx 會繼續讀取請求的 URI 和 Header 信息。
-
讀取數據:Nginx 會從客戶端讀取更多的數據,直到遇到請求頭的結束標誌(即兩個連續的換行符)。
-
解析 Header:每個 HTTP 頭部由一個字段名、一個冒號和一個字段值組成。Nginx 會逐個解析這些頭部字段,並將它們存儲在請求的上下文中,以便後續的處理階段可以使用。
-
內存管理:在解析過程中,如果遇到大的請求頭或 URI,Nginx 會動態地分配更多的內存來存儲這些數據。這是通過
large_client_header_buffers
指令來控制的,它定義了可以分配的最大內存塊的數量和大小。
2. 分配內存資源
分配請求內存池:爲了存儲請求數據,Nginx 會分配一個請求內存池,其大小由
request_pool_size
指令指定,默認爲 4KB。分配大內存:如果請求行或 Header 超過了基礎的內存池大小,Nginx 會根據
large_client_header_buffers
指令分配更大的內存塊,該指令默認設置爲 4 個 8KB 的內存塊。
分配請求內存池
當一個 HTTP 請求到達 Nginx 時,Nginx 需要一塊內存區域來存儲請求的各個部分,包括請求行(包含方法、URI 和 HTTP 版本)、請求頭(包含各種頭部字段)以及可能的請求體(例如 POST 請求中的數據)。爲了高效地管理這些數據,Nginx 使用了一個稱爲 “內存池” 的機制。
- request_pool_size:這個指令定義了每個請求的內存池的大小,默認爲 4KB。這個內存池足夠存儲大多數請求的頭部和部分請求體數據。
分配大內存
在某些情況下,請求的頭部或請求行可能會非常大,超出了默認的 4KB 內存池的限制。例如,如果客戶端發送了一個包含大量頭部字段的請求,或者 URI 非常長,那麼就需要更多的內存來存儲這些數據。
-
large_client_header_buffers:這個指令允許 Nginx 分配更大的內存塊來存儲請求頭部。默認情況下,它設置爲 4 個 8KB 的內存塊。這意味着 Nginx 可以處理最大 32KB 的請求頭部。
-
large_client_header_buffers 的工作機制:當狀態機解析請求頭時,如果發現當前的內存池不足以存儲更多的數據,Nginx 會動態地分配一個 8KB 的大內存塊,然後把 1KB 裏面的內容拷貝到這個 8KB 裏面。這些大內存塊是按需分配的,只有當實際需要時纔會分配更多的內存。
狀態機解析請求行
Nginx 使用內部的狀態機來解析客戶端發送的 HTTP 請求行和請求頭。狀態機是一種編程模型,它根據輸入數據(在這種情況下是 HTTP 請求的各個部分)和當前的狀態來決定下一步的操作。
-
解析請求行:狀態機首先解析請求行,這包括識別 HTTP 方法(如 GET、POST 等)、URI 和 HTTP 版本。這一步驟需要從接收到的數據中提取這些關鍵信息,併爲後續的處理做準備。
-
解析請求頭:請求行之後,狀態機開始解析請求頭。它會逐行讀取頭部數據,並根據頭部字段的名稱和值執行相應的操作。例如,如果遇到
Content-Length
字段,狀態機需要記錄下請求體的大小,以便後續能夠正確地讀取請求體。 -
動態內存分配:在解析過程中,如果狀態機發現需要更多的內存來存儲請求頭或請求行,它會觸發內存分配機制,如上文所述的
large_client_header_buffers
。
通過這種機制,Nginx 能夠靈活地處理各種大小的 HTTP 請求,同時保持內存使用的高效性。狀態機的解析過程是 Nginx 請求處理的核心部分,它確保了請求數據的正確解析和後續處理的順利進行。
3. 標識 URI、狀態機解析 Header 和分配大內存
在 Nginx 處理 HTTP 請求的過程中,標識 URI、狀態機解析 Header 和分配大內存是關鍵步驟,這些步驟確保了 Nginx 能夠正確解析客戶端請求併爲後續處理做好準備。以下是結合 Nginx 底層原理與源碼對這些步驟的詳細展開:
標識 URI
-
解析請求行:Nginx 首先使用狀態機解析請求行,這包括識別請求方法(如 GET 或 POST)、URI 和 HTTP 版本。
-
URI 處理:解析出的 URI 會被進一步處理,Nginx 會根據配置的路由規則和重寫規則來確定最終的請求目標。
-
位置匹配:Nginx 會查找與請求的 URI 匹配的
location
塊,這決定了請求將如何被處理,例如轉發到代理服務器或直接提供靜態文件。
狀態機解析 Header
-
讀取請求頭:在請求行被解析之後,Nginx 繼續讀取請求頭。請求頭包含了客戶端傳遞的元數據,如
Host
、User-Agent
、Content-Type
等。 -
狀態機:Nginx 使用一個內部狀態機來逐行解析請求頭。狀態機根據 HTTP 協議規範和請求頭的格式來逐個處理頭部字段。
-
存儲頭部信息:解析出的頭部信息被存儲在
ngx_http_request_t
結構體中,以便在後續的請求處理階段中使用。
分配大內存
-
默認內存池:每個請求都會分配一個默認大小的內存池,由
request_pool_size
指令指定,默認爲 4KB。 -
大內存需求:如果請求頭的大小超過了默認內存池的容量,Nginx 需要分配額外的內存來存儲這些數據。
-
動態內存分配:Nginx 根據
large_client_header_buffers
指令動態分配額外的內存。這個指令定義了可以分配的最大內存塊的數量和大小,通常設置爲 4 個 8KB 的內存塊。
標識 Header
-
處理頭部字段:一旦請求頭被讀取和解析,Nginx 會根據頭部字段的內容執行特定的操作。例如,如果存在
Content-Length
字段,Nginx 會知道請求體的大小,並準備讀取相應的數據。 -
變量賦值:Nginx 會將請求頭中的某些值賦給內部變量,這些變量可以在配置文件中引用,用於重寫規則、日誌記錄等。
-
模塊處理:不同的 Nginx 模塊可能會對請求頭進行特定的處理。例如,安全模塊可能會檢查
Sec-WebSocket-Key
字段以支持 WebSocket 連接。
4. 處理請求
移除超時定時器:在請求行和 Header 被成功解析之後,Nginx 會移除之前設置的
client_header_timeout
超時定時器,該定時器默認設置爲 60 秒,用於檢測客戶端是否在超時時間內發送完整的請求頭。開始 11 個階段的 HTTP 請求處理:Nginx 將請求處理分爲 11 個階段,每個階段可以包含多個模塊的處理函數。這些階段包括重寫、權限檢查、內容生成、日誌記錄等。
處理請求是 Nginx 接收到客戶端 HTTP 請求後的核心環節,涉及到多個階段的執行和多個模塊的參與。以下是結合 Nginx 底層原理與源碼對處理請求過程的詳細展開:
移除超時定時器
在 Nginx 配置中,client_header_timeout
指令用於設置讀取客戶端請求頭的超時時間。如果在指定時間內,Nginx 未能接收到完整的請求頭,那麼連接將被關閉以避免資源佔用。
-
超時機制:Nginx 使用定時器來實現超時機制。當請求頭和請求行被成功解析後,Nginx 會檢查是否有設置超時定時器,並將其移除,因爲此時請求已經被認爲是有效的。
-
事件通知:如果超時發生,Nginx 會收到一個事件通知,這將觸發一個內部函數來關閉連接並釋放資源。
11 個階段的 HTTP 請求處理
Nginx 將每個請求的處理分爲 11 個階段,每個階段負責處理特定的任務。這種模塊化的設計使得 Nginx 能夠靈活地配置和擴展其功能。
-
服務器重寫階段 (
NGX_HTTP_SERVER_REWRITE_PHASE
):在這個階段,Nginx 可以重寫請求的 URI 和請求方法。 -
查找配置階段 (
NGX_HTTP_FIND_CONFIG_PHASE
):Nginx 根據請求的 URI 查找最合適的服務器配置。 -
重寫階段 (
NGX_HTTP_REWRITE_PHASE
):在這個階段,Nginx 可以根據 location 塊進一步重寫 URI。 -
權限檢查階段 (
NGX_HTTP_ACCESS_PHASE
):Nginx 檢查客戶端是否有權限訪問請求的資源。 -
內容生成階段 (
NGX_HTTP_CONTENT_PHASE
):在這個階段,Nginx 調用內容生成模塊來生成響應內容。 -
日誌記錄階段 (
NGX_HTTP_LOG_PHASE
):Nginx 記錄請求的日誌信息。 -
其他階段:還包括嘗試文件階段、文件查找階段、錯誤處理階段等。
上面的所有步驟都是 nginx 的框架執行的,後面的 11 個階段的 HTTP 請求處理 是 nginx http 模塊的執行流程
5. 詳細處理流程
NGX_HTTP_SERVER_REWRITE_PHASE:服務器重寫階段,用於修改請求的 URI。
NGX_HTTP_FIND_CONFIG_PHASE:查找配置階段,用於確定請求應該由哪個 server 塊處理。
NGX_HTTP_REWRITE_PHASE:重寫階段,用於修改請求的 URI 和頭部。
NGX_HTTP_POST_REWRITE_PHASE:重寫後階段,用於處理重寫後的結果。
NGX_HTTP_PREACCESS_PHASE:訪問前階段,用於進行權限檢查。
NGX_HTTP_ACCESS_PHASE:訪問階段,用於執行訪問控制。
NGX_HTTP_POST_ACCESS_PHASE:訪問後階段,用於執行訪問控制後的清理工作。
NGX_HTTP_TRY_FILES_PHASE:嘗試文件階段,用於嘗試查找請求的文件。
NGX_HTTP_CONTENT_PHASE:內容生成階段,用於生成響應的內容。
NGX_HTTP_LOG_PHASE:日誌記錄階段,用於記錄請求的日誌。
NGX_HTTP_CLOSE_REQUEST_PHASE:關閉請求階段,用於清理請求資源。
6. 結束處理
-
發送響應:Nginx 根據處理結果構建 HTTP 響應,並將其發送給客戶端。
-
清理資源:請求結束後,Nginx 釋放分配的內存資源,並關閉連接或保持連接以待後續請求。
Nginx 處理 HTTP 頭部的過程是高效且靈活的,它通過精細的內存管理和狀態機解析,確保了在各種情況下都能快速準確地處理客戶端請求。我們在下一篇詳細的學習 Nginx 處理 HTTP 請求的 11 個階段的過程與原理。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DFvayYOXY7kuhouLAAy61A