Nginx 源碼分析之 Filter 與 Upstream

第一章 Filter 模塊

1.1 過濾模塊簡介

1. 執行時間和內容

過濾(filter)模塊是過濾響應頭和內容的模塊,可以對回覆的頭和內容進行處理。它的處理 時間在獲取回覆內容之後,向用戶發送響應之前。它的處理過程分為兩個階段,過濾 HTTP 回覆的頭部和主體,在這兩個階段可以分別對頭部和主體進行修改。 在代碼中有類似的函數:

ngx_http_top_header_filter(r); ngx_http_top_body_filter(r, in);

就是分別對頭部和主體進行過濾的函數。所有模塊的響應內容要返回給客戶端,都必須調用 這兩個接口。

2. 執行順序(私信linux,獲取更多幹貨資料)

過濾模塊的調用是有順序的,它的順序在編譯的時候就決定了。控制編譯的腳本位於 auto/modules 中,當你編譯完 Nginx 以後,可以在 objs 目錄下面看到一個 ngx_modules.c 的 文件。打開這個文件,有類似的代碼:

<code>

ngx_module_t

*ngx_modules[] = { ... &ngx_http_write_filter_module, &ngx_http_header_filter_module, &ngx_http_chunked_filter_module, &ngx_http_range_header_filter_module, &ngx_http_gzip_filter_module, &ngx_http_postpone_filter_module, &ngx_http_ssi_filter_module, &ngx_http_charset_filter_module, &ngx_http_userid_filter_module, &ngx_http_headers_filter_module, &ngx_http_copy_filter_module, &ngx_http_range_body_filter_module, &ngx_http_not_modified_filter_module,

NULL

}; /<code>

從 write_filter 到 not_modified_filter,模塊的執行順序是反向的。也就是說最早執行的 是 not_modified_filter,然後各個模塊依次執行。所有第三方的模塊只能加入到 copy_filter 和 headers_filter 模塊之間執行。 Nginx 執行的時候是怎麼按照次序依次來執行各個過濾模塊呢?它採用了一種很隱晦的 方法,即通過局部的全局變量。比如,在每個 filter 模塊,很可能看到如下代碼:

<code>

static

ngx_http_output_header_filter_pt

static

ngx_http_output_body_filter_pt

ngx_http_next_header_filter

=

ngx_http_top_header_filter;

ngx_http_top_header_filter

=

ngx_http_example_header_filter;

ngx_http_next_body_filter

=

ngx_http_top_body_filter;

ngx_http_top_body_filter

=

ngx_http_example_body_filter;

/<code>


ngx_http_top_header_filter 是一個全局變量。當編譯進一個 filter 模塊的時候,就被賦值 為當前 filter 模塊的處理函數。而
ngx_http_next_header_filter 是一個局部全局變量,它保存 了編譯前上一個 filter 模塊的處理函數。所以整體看來,就像用全局變量組成的一條單向鏈 表。 每個模塊想執行下一個過濾函數,只要調用一下
ngx_http_next_header_filter 這個局部 變量。而整個過濾模塊鏈的入口,需要調用


ngx_http_top_header_filter 這個全局變量。 ngx_http_top_body_filter 的行為與 header fitler 類似。 響應頭和響應體過濾函數的執行順序如下所示:


Nginx 源碼分析之 Filter 與 Upstream


這圖只表示了 head_filter 和 body_filter 之間的執行順序,在 header_filter 和 body_filter 處理 函數之間,在 body_filter 處理函數之間,可能還有其他執行代碼。


3. 模塊編譯(私信linux,獲取更多幹貨資料)

Nginx 可以方便的加入第三方的過濾模塊。在過濾模塊的目錄裡,首先需要加入 config 文件, 文件的內容如下:

<code>

ngx_addon_name

=ngx_http_example_filter_module

HTTP_AUX_FILTER_MODULES

=

"$HTTP_AUX_FILTER_MODULES ngx_http_example_filter_module"

NGX_ADDON_SRCS

=

"$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_example_filter_module.c"

/<code>

說 明 把 這 個 名 為
ngx_http_example_filter_module 的 過 濾 模 塊 加 入 ,
ngx_http_example_filter_module.c 是該模塊的源代碼。 注意 HTTP_AUX_FILTER_MODULES 這個變量與一般的內容處理模塊不同。

1.2 過濾模塊的分析

1. 相關結構體

ngx_chain_t 結構非常簡單,是一個單向鏈表:

<code>

typedef

struct

ngx_chain_s

ngx_chain_t

;

struct

ngx_chain_s

{

ngx_buf_t

*buf;

ngx_chain_t

*next; }; /<code>

在過濾模塊中,所有輸出的內容都是通過一條單向鏈表所組成。這種單向鏈表的設計,正好 應和了 Nginx 流式的輸出模式。每次 Nginx 都是讀到一部分的內容,就放到鏈表,然後輸出 出去。這種設計的好處是簡單,非阻塞,但是相應的問題就是跨鏈表的內容操作非常麻煩, 如果需要跨鏈表,很多時候都只能緩存鏈表的內容。 單鏈表負載的就是 ngx_buf_t,這個結構體使用非常廣泛,先讓我們看下該結構體的代碼:

<code>

struct

ngx_buf_s

{

u_char *pos; u_char *last;

off_t

file_pos;

off_t

file_last; u_char *start; u_char *end;

ngx_buf_tag_t

tag;

ngx_file_t

*file; /<code>


<code>          
        

ngx_buf_t

*shadow;

unsigned

temporary:

1

;

unsigned

memory:

1

;

unsigned

mmap:

1

;

unsigned

recycled:

1

;

unsigned

in_file:

1

;

unsigned

flush:

1

;

unsigned

sync:

1

;

unsigned

last_buf:

1

;

unsigned

last_in_chain:

1

;

unsigned

last_shadow:

1

;

unsigned

temp_file:

1

;

int

num; }; /<code>

一般 buffer 結構體可以表示一塊內存,內存的起始和結束地址分別用 start 和 end 表示,pos 和 last 表示實際的內容。如果內容已經處理過了,pos 的位置就可以往後移動。如果讀取到 新的內容,last 的位置就會往後移動。所以 buffer 可以在多次調用過程中使用。如果 last 等

於 end,就說明這塊內存已經用完了。如果 pos 等於 last,說明內存已經處理完了。下面是 一個簡單的示意圖,說明 buffer 中指針的用法:


Nginx 源碼分析之 Filter 與 Upstream

2. 響應頭過濾函數(私信linux,獲取更多幹貨資料)

響應頭過濾函數主要的用處就是處理 HTTP 響應的頭,可以根據實際情況對於響應頭進行修 改或者添加刪除。響應頭過濾函數先於響應體過濾函數,而且只調用一次,所以一般可作過 濾模塊的初始化工作。 響應頭過濾函數的入口只有一個:

<code>

ngx_int_t

ngx_http_send_header(

ngx_http_request_t

*r) { ...

return

ngx_http_top_header_filter(r); } /<code>

該函數向客戶端發送回覆的時候調用,然後按前一節所述的執行順序。該函數的返回值一般 是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。可以把 HTTP 響應頭的存儲方式想象成一個 hash 表,在 Nginx 內部可以很方便地查找和修改各個響應頭 部,
ngx_http_header_filter_module 過濾模塊把所有的 HTTP 頭組合成一個完整的 buffer,最 終
ngx_http_write_filter_module 過濾模塊把 buffer 輸出。 按照前一節過濾模塊的順序,依次講解如下:


Nginx 源碼分析之 Filter 與 Upstream


Nginx 源碼分析之 Filter 與 Upstream

3. 響應體過濾函數

響應體過濾函數是過濾響應主體的函數。ngx_http_top_body_filter 這個函數每個請求可能會 被執行多次,它的入口函數是 ngx_http_output_filter,比如:

<code>

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; rc = ngx_http_top_body_filter(r, in);

if

(rc == NGX_ERROR) { c->error =

1

; }

return

rc; } /<code>

ngx_http_output_filter 可以被一般的靜態處理模塊調用,也有可能是在 upstream 模塊裡面被 調用,對於整個請求的處理階段來說,他們處於的用處都是一樣的,就是把響應內容過濾,然後發給客戶端。具體模塊的響應體過濾函數的格式類似這樣:

<code>

static

int

ngx_http_example_body_filter

(

ngx_http_request_t

*r,

ngx_chain_t

*in)

{ ...

return

ngx_http_next_body_filter(r, in); } /<code>

該函數的返回值一般是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和 未完成。

4. 主要功能介紹

響應的主體內容就存於單鏈表 in,鏈表一般不會太長,有時 in 參數可能為 NULL。in 中存有 buf 結構體中,對於靜態文件,這個 buf 大小默認是 32K;對於反向代理的應用,這個 buf 可 能是 4k 或者 8k。為了保持內存的低消耗,Nginx 一般不會分配過大的內存,處理的原則是 收到一定的數據,就發送出去。一個簡單的例子,可以看看 Nginx 的 chunked_filter 模塊,在 沒有 content-length 的情況下,chunk 模塊可以流式(stream)的加上長度,方便瀏覽器接收 和顯示內容。 在響應體過濾模塊中,尤其要注意的是 buf 的標誌位,完整描述可以在“相關結構體”這個節 中看到。如果 buf 中包含 last 標誌,說明是最後一塊 buf,可以直接輸出並結束請求了。如 果有 flush 標誌,說明這塊 buf 需要馬上輸出,不能緩存。如果整塊 buffer 經過處理完以後, 沒有數據了,你可以把 buffer 的 sync 標誌置上,表示只是同步的用處。 當所有的過濾模塊都處理完畢時,在最後的 write_fitler 模塊中,Nginx 會將 in 輸出鏈拷貝到 r->out 輸出鏈的末尾,然後調用 sendfile 或者 writev 接口輸出。由於 Nginx 是非阻塞的 socket 接口,寫操作並不一定會成功,可能會有部分數據還殘存在 r->out。在下次的調用中,Nginx 會繼續嘗試發送,直至成功。

5. 發出子請求

Nginx 過濾模塊一大特色就是可以發出子請求,也就是在過濾響應內容的時候,你可以發送 新的請求,Nginx 會根據你調用的先後順序,將多個回覆的內容拼接成正常的響應主體。一 個簡單的例子可以參考 addition 模塊。 Nginx 是如何保證父請求和子請求的順序呢?當 Nginx 發出子請求時,就會調用 ngx_http_subrequest 函數,將子請求插入父請求的 r->postponed 鏈表中。子請求會在主請求 執行完畢時獲得依次調用。子請求同樣會有一個請求所有的生存期和處理過程,也會進入過 濾模塊流程。 關鍵點是在 postpone_filter 模塊中,它會拼接主請求和子請求的響應內容。r->postponed 按 次序保存有父請求和子請求,它是一個鏈表,如果前面一個請求未完成,那後一個請求內容 就不會輸出。當前一個請求完成時並輸出時,後一個請求才可輸出,當所有的子請求都完成 時,所有的響應內容也就輸出完畢了。

6. 一些優化措施

Nginx 過濾模塊涉及到的結構體,主要就是 chain 和 buf,非常簡單。在日常的過濾模塊中, 這兩類結構使用非常頻繁,Nginx 採用類似 freelist 重複利用的原則,將使用完畢的 chain 或 者 buf 結構體,放置到一個固定的空閒鏈表裡,以待下次使用。 比如,在通用內存池結構體中,pool->chain 變量裡面就保存著釋放的 chain。而一般的 buf 結構體,沒有模塊間公用的空閒鏈表池,都是保存在各模塊的緩存空閒鏈表池裡面。對於 buf 結構體,還有一種 busy 鏈表,表示該鏈表中的 buf 都處於輸出狀態,如果 buf 輸出完畢,這 些 buf 就可以釋放並重複利用了。


Nginx 源碼分析之 Filter 與 Upstream


7. 過濾內容的緩存

由於 Nginx 設計流式的輸出結構,當我們需要對響應內容作全文過濾的時候,必須緩存部分 的 buf 內容。該類過濾模塊往往比較複雜,比如 sub,ssi,gzip 等模塊。這類模塊的設計非 常靈活,我簡單講一下設計原則: 1. 輸入鏈 in 需要拷貝操作,經過緩存的過濾模塊,輸入輸出鏈往往已經完全不一樣了, 所以需要拷貝,通過 ngx_chain_add_copy 函數完成。 2. 一般有自己的 free 和 busy 緩存鏈表池,可以提高 buf 分配效率。 3. 如果需要分配大塊內容,一般分配固定大小的內存卡,並設置 recycled 標誌,表示可以 重複利用。 4. 原有的輸入 buf 被替換緩存時,必須將其 buf->pos 設為 buf->last,表明原有的 buf 已經 被輸出完畢。或者在新建立的 buf,將 buf->shadow 指向舊的 buf,以便輸出完畢時及時 釋放舊的 buf。


第二章 Upstream 模塊

2.1 Upstream 模塊

nginx 模塊一般被分成三大類:handler、filter 和 upstream。前面的章節中,讀者已經了 解了 handler、filter。利用這兩類模塊,可以使 nginx 輕鬆完成任何單機工作。而本章介紹的 upstream 模塊,將使 nginx 跨越單機的限制,完成網絡數據的接收、處理和轉發。 數據轉發功能,為 nginx 提供了跨越單機的橫向處理能力,使 nginx 擺脫只能為終端節點提 供單一功能的限制,而使它具備了網路應用級別的拆分、封裝和整合的戰略功能。在雲模型 大行其道的今天,數據轉發是 nginx 有能力構建一個網絡應用的關鍵組件。當然,鑑於開發

成本的問題,一個網絡應用的關鍵組件一開始往往會採用高級編程語言開發。但是當系統到 達一定規模,並且需要更重視性能的時候,為了達到所要求的性能目標,高級語言開發出的 組件必須進行結構化修改。此時,對於修改代價而言,nginx 的 upstream 模塊呈現出極大的 吸引力,因為它天生就快。作為附帶,nginx 的配置系統提供的層次化和松耦合使得系統的 擴展性也達到比較高的程度。言歸正傳,下面介紹 upstream 的寫法。

從本質上說,upstream 屬於 handler,只是他不產生自己的內容,而是通過請求後端服務器 得到內容,所以才稱為 upstream(上游)。請求並取得響應內容的整個過程已經被封裝到 nginx 內部,所以 upstream 模塊只需要開發若干回調函數,完成構造請求和解析響應等具體的工 作。這些回調函數如下表所示:


Nginx 源碼分析之 Filter 與 Upstream

2.1.1 Memcached 模塊分析

Memcached 是一款高性能的分佈式 cache 系統,得到了非常廣泛的應用。memcached 定義了一套私有通信協議,使得不能通過 HTTP 請求來訪問 memcached。但協議本身簡單高 效,而且 memcached 使用廣泛,所以大部分現代開發語言和平臺都提供了 memcached 支 持,方便開發者使用 memcached。 Nginx 提供了 ngx_http_memcached 模塊,提供從 memcached 讀取數據的功能,而不提 供向 memcache 寫數據的功能。作為 web 服務器,這種設計是可以接受的。 下面,我們開始分析 ngx_http_memcached 模塊,一窺 upstream 的奧秘。


1. Handler 模塊

初看 memcached 模塊,大家可能覺得並無特別之處。如果稍微細看,甚至覺得有點像 handler 模塊,當大家看到這段代碼以後,必定疑惑為什麼會跟 handler 模塊一模一樣。

<code>clcf = ngx_http_conf_get_module_loc_conf(cf,
ngx_http_core_module);
clcf->handler = ngx_http_memcached_handler; /<code>

因為 upstream 模塊使用的就是 handler 模塊的接入方式。同時,upstream 模塊的指令系統 的設計也是遵循 handler 模塊的基本規則:配置該模塊才會執行該模塊。

<code>

{

ngx_string("memcached_pass"),

NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,

ngx_http_memcached_pass,

NGX_HTTP_LOC_CONF_OFFSET,

0

,

NULL

}

/<code>

所以大家覺得眼熟是好事,說明大家對 Handler 的寫法已經很熟悉了。

2. Upstream 模塊

那麼,upstream 模塊的特別之處究竟在哪裡呢?答案是就在模塊處理函數的實現中。 upstream 模塊的處理函數進行的操作都包含一個固定的流程。在 memcached 的例子中,可 以觀察
ngx_http_memcached_handler 的代碼,可以發現,這個固定的操作流程是: 1. 創建 upstream 數據結構。

<code>

if

(ngx_http_upstream_create(r) != NGX_OK) {

return

NGX_HTTP_INTERNAL_SERVER_ERROR; } /<code>

2. 設置模塊的 tag 和 schema。 schema 現在只會用於日誌,tag 會用於 buf_chain 管理。

<code>u = r->upstream; 
 
ngx_str_set(&u->schema, 

"memcached://"

); u->output.tag = (ngx_buf_tag_t) &ngx_http_memcached_module; /<code>

3. 設置 upstream 的後端服務器列表數據結構。

<code>mlcf = ngx_http_get_module_loc_conf(r,
ngx_http_memcached_module);
u->conf = &mlcf->upstream; /<code>

4. 設置 upstream 回調函數。

在這裡列出的代碼稍稍調整了代碼順序。

<code>u->create_request = ngx_http_memcached_create_request;
u->reinit_request = ngx_http_memcached_reinit_request;
u->process_header = ngx_http_memcached_process_header;
u->abort_request = ngx_http_memcached_abort_request;
u->finalize_request = ngx_http_memcached_finalize_request;
u->input_filter_init = ngx_http_memcached_filter_init;
u->input_filter = ngx_http_memcached_filter; /<code>

5. 創建並設置 upstream 環境數據結構。

<code>ctx = ngx_palloc(r->pool, sizeof(ngx_http_memcached_ctx_t));

if

(ctx ==

NULL

) {

return

NGX_HTTP_INTERNAL_SERVER_ERROR; } ctx->rest = NGX_HTTP_MEMCACHED_END; ctx->request = r; ngx_http_set_ctx(r, ctx, ngx_http_memcached_module); u->input_filter_ctx = ctx; /<code>

6. 完成 upstream 初始化並進行收尾工作。

<code>r->main->count++;
ngx_http_upstream_init(r);

return

NGX_DONE; /<code>

任何upstream模塊,簡單如memcached,複雜如proxy、fastcgi都是如此。不同的upstream 模塊在這 6 步中的最大差別會出現在第 2、3、4、5 上。其中第 2、4 兩步很容易理解,不同 的模塊設置的標誌和使用的回調函數肯定不同。第 5 步也不難理解,只有第 3 步是最為晦澀 的,不同的模塊在取得後端服務器列表時,策略的差異非常大,有如 memcached 這樣簡單 明瞭的,也有如 proxy 那樣邏輯複雜的。這個問題先記下來,等把 memcached 剖析清楚了, 再單獨討論。

第 6 步是一個常態。將 count 加 1,然後返回 NGX_DONE。nginx 遇到這種情況,雖然會 認為當前請求的處理已經結束,但是不會釋放請求使用的內存資源,也不會關閉與客戶端的 連接。之所以需要這樣,是因為 nginx 建立了 upstream 請求和客戶端請求之間一對一的關 系,在後續使用 ngx_event_pipe 將 upstream 響應發送回客戶端時,還要使用到這些保存著 客戶端信息的數據結構。這部分會在後面的原理篇做具體介紹,這裡不再展開。 將 upstream 請求和客戶端請求進行一對一綁定,這個設計有優勢也有缺陷。優勢就是簡化 模塊開發,可以將精力集中在模塊邏輯上,而缺陷同樣明顯,一對一的設計很多時候都不能 滿足複雜邏輯的需要。對於這一點,將會在後面的原理篇來闡述。

3. 回調函數

前面剖析了 memcached 模塊的骨架,現在開始逐個解決每個回調函數。

1.
ngx_http_memcached_create_request:很簡單的按照設置的內容生成一個 key,接著生成 一個“get $key”的請求,放在 r->upstream->request_bufs 裡面。

2.
ngx_http_memcached_reinit_request:無需初始化。

3.
ngx_http_memcached_abort_request:無需額外操作。

4.
ngx_http_memcached_finalize_request:無需額外操作。

5.
ngx_http_memcached_process_header:模塊的業務重點函數。

Memcached 協議的頭部信息被定義為第一行文本,可以找到這段代碼證明:

<code>

for

(p = u->buffer.pos; p < u->buffer.last; p++) {

if

( * p == LF) {

goto

found; } /<code>

如果在已讀入緩衝的數據中沒有發現 LF(‘n’)字符,函數返回 NGX_AGAIN,表示頭部未完 全讀入,需要繼續讀取數據。nginx 在收到新的數據以後會再次調用該函數。 nginx 處理後端服務器的響應頭時只會使用一塊緩存,所有數據都在這塊緩存中,所以 解析頭部信息時不需要考慮頭部信息跨越多塊緩存的情況。而如果頭部過大,不能保存在這 塊緩存中,nginx 會返回錯誤信息給客戶端,並記錄 error log,提示緩存不夠大。 process_header 的重要職責是將後端服務器返回的狀態翻譯成返回給客戶端的狀態。例如, 在


ngx_http_memcached_process_header 中,有這樣幾段代碼:

<code>r->headers_out.content_length_n = ngx_atoof(len, p - len - 

1

); u->headers_in.status_n =

200

; u->state->status =

200

; u->headers_in.status_n =

404

; u->state->status =

404

; /<code>

u->state 用於計算 upstream 相關的變量。比如 u->state->status 將被用於計算變量 “upstream_status”的值。u->headers_in 將被作為返回給客戶端的響應返回狀態碼。而第一行 則是設置返回給客戶端的響應的長度。 在這個函數中不能忘記的一件事情是處理完頭部信息以後需要將讀指針 pos 後移,否則這段 數據也將被複制到返回給客戶端的響應的正文中,進而導致正文內容不正確。

<code>u->buffer.pos = p + 

1

; /<code>

process_header 函數完成響應頭的正確處理,應該返回 NGX_OK。如果返回 NGX_AGAIN,表 示未讀取完整數據,需要從後端服務器繼續讀取數據。返回 NGX_DECLINED 無意義,其他任 何返回值都被認為是出錯狀態,nginx 將結束 upstream 請求並返回錯誤信息。


6.ngx_http_memcached_filter_init:修正從後端服務器收到的內容長度。因為在處理 header 時沒有加上這部分長度。

7. ngx_http_memcached_filter:memcached 模塊是少有的帶有處理正文的回調函數的模塊。 因為 memcached 模塊需要過濾正文末尾 CRLF “END” CRLF,所以實現了自己的 filter 回調函 數。處理正文的實際意義是將從後端服務器收到的正文有效內容封裝成 ngx_chain_t,並加 在 u->out_bufs 末尾。nginx 並不進行數據拷貝,而是建立 ngx_buf_t 數據結構指向這些數據 內存區,然後由 ngx_chain_t 組織這些 buf。這種實現避免了內存大量搬遷,也是 nginx 高效 的奧秘之一。

upstream 模塊是從 handler 模塊發展而來,指令系統和模塊生效方式與 handler 模塊無 異。不同之處在於,upstream 模塊在 handler 函數中設置眾多回調函數。實際工作都是由這 些回調函數完成的。每個回調函數都是在 upstream 的某個固定階段執行,各司其職,大部 分回調函數一般不會真正用到。upstream 最重要的回調函數是 create_request、 process_header 和 input_filter,他們共同實現了與後端服務器的協議的解析部分。


2.2 負載均衡模塊

負載均衡模塊用於從”upstream”指令定義的後端主機列表中選取一臺主機。nginx 先使用負 載均衡模塊找到一臺主機,再使用 upstream 模塊實現與這臺主機的交互。為了方便介紹負 載均衡模塊,做到言之有物,以下選取 nginx 內置的 ip hash 模塊作為實際例子進行分析。

1. 配置

要了解負載均衡模塊的開發方法,首先需要了解負載均衡模塊的使用方法。因為負載均衡模 塊與之前書中提到的模塊差別比較大,所以我們從配置入手比較容易理解。 在配置文件中,我們如果需要使用 ip hash 的負載均衡算法。我們需要寫一個類似下面的配 置:

<code>

upstream

test

{

ip_hash;

server

192.168

.0

.1

;

server

192.168

.0

.2

;

}

/<code>

從配置我們可以看出負載均衡模塊的使用場景: 1. 核心指令”ip_hash”只能在 upstream {}中 使用。這條指令用於通知 nginx 使用 ip hash 負載均衡算法。如果沒加這條指令,nginx 會使 用默認的 round robin 負載均衡模塊。請各位讀者對比 handler 模塊的配置,是不是有共同 點? 2. upstream {}中的指令可能出現在”server”指令前,可能出現在”server”指令後,也可能 出現在兩條”server”指令之間。各位讀者可能會有疑問,有什麼差別麼?那麼請各位讀者嘗 試下面這個配置:

<code>

upstream

test

{

server

192.168

.0

.1

weight=5;

ip_hash;

server

192.168

.0

.2

weight=7;

}

/<code>

神奇的事情出現了:

<code>

nginx

: [emerg] invalid parameter

"weight=7"

in nginx.

conf

:

103

configuration file nginx.conf test failed /<code>

可見 ip_hash 指令的確能影響到配置的解析。

2. 指令

配置決定指令系統,現在就來看 ip_hash 的指令定義:

<code>

static

ngx_command_t

ngx_http_upstream_ip_hash_commands[] = { { ngx_string(

"ip_hash"

), NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, ngx_http_upstream_ip_hash,

0

,

0

,

NULL

}, ngx_null_command }; /<code>

沒有特別的東西,除了指令屬性是 NGX_HTTP_UPS_CONF。這個屬性表示該指令的適用範圍 是 upstream{}。

3. 鉤子

以從前面的章節得到的經驗,大家應該知道這裡就是模塊的切入點了。負載均衡模塊的鉤子 代碼都是有規律的,這裡通過 ip_hash 模塊來分析這個規律。

<code>

static

char

*

ngx_http_upstream_ip_hash

(

ngx_conf_t

*cf,

ngx_command_t

*cmd,

void

*conf)

{

ngx_http_upstream_srv_conf_t

*uscf; uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash; uscf->flags = NGX_HTTP_UPSTREAM_CREATE |NGX_HTTP_UPSTREAM_MAX_FAILS |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT |NGX_HTTP_UPSTREAM_DOWN;

return

NGX_CONF_OK; } /<code>

這段代碼中有兩點值得我們注意。一個是 uscf->flags 的設置,另一個是設置 init_upstream 回 調。

設置 uscf->flags

1. NGX_HTTP_UPSTREAM_CREATE:創建標誌,如果含有創建標誌的話,nginx 會檢查重複 創建,以及必要參數是否填寫;

2.
NGX_HTTP_UPSTREAM_MAX_FAILS:可以在 server 中使用 max_fails 屬性;

3.
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:可以在 server 中使用 fail_timeout 屬性;

4. NGX_HTTP_UPSTREAM_DOWN:可以在 server 中使用 down 屬性; 此外還有下面屬性:

5. NGX_HTTP_UPSTREAM_WEIGHT:可以在 server 中使用 weight 屬性;

6. NGX_HTTP_UPSTREAM_BACKUP:可以在 server 中使用 backup 屬性。 聰明的讀者如果聯想到剛剛遇到的那個神奇的配置錯誤,可以得出一個結論:在負載均衡模 塊的指令處理函數中可以設置並修改 upstream{}中”server”指令支持的屬性。這是一個很重 要的性質,因為不同的負載均衡模塊對各種屬性的支持情況都是不一樣的,那麼就需要在解 析配置文件的時候檢測出是否使用了不支持的負載均衡屬性並給出錯誤提示,這對於提升系 統維護性是很有意義的。但是,這種機制也存在缺陷,正如前面的例子所示,沒有機制能夠 追加檢查在更新支持屬性之前已經配置了不支持屬性的”server”指令。

設置 init_upstream 回調

nginx 初始化 upstream 時,會在
ngx_http_upstream_init_main_conf 函數中調用設置的回調函 數初始化負載均衡模塊。這裡不太好理解的是 uscf 的具體位置。通過下面的示意圖,說明 upstream 負載均衡模塊的配置的內存佈局。


Nginx 源碼分析之 Filter 與 Upstream

從圖上可以看出,MAIN_CONF 中 ngx_upstream_module 模塊的配置項中有一個指針數組 upstreams,數組中的每個元素對應就是配置文件中每一個 upstream{}的信息。更具體的將會 在後面的原理篇討論。

4. 初始化配置

init_upstream 回調函數執行時需要初始化負載均衡模塊的配置,還要設置一個新鉤子,這個 鉤子函數會在 nginx 處理每個請求時作為初始化函數調用,關於這個新鉤子函數的功能,後 面會有詳細的描述。這裡,我們先分析 IP hash 模塊初始化配置的代碼:

<code>ngx_http_upstream_init_round_robin(cf, us);
us->peer.

init

= ngx_http_upstream_init_ip_hash_peer; /<code>

這段代碼非常簡單:IP hash 模塊首先調用另一個負載均衡模塊 Round Robin 的初始化函數, 然後再設置自己的處理請求階段初始化鉤子。實際上幾個負載均衡模塊可以組成一條鏈表, 每次都是從鏈首的模塊開始進行處理。如果模塊決定不處理,可以將處理權交給鏈表中的下 一個模塊。這裡,IP hash 模塊指定 Round Robin 模塊作為自己的後繼負載均衡模塊,所以在 自己的初始化配置函數中也對 Round Robin 模塊進行初始化。

5. 初始化請求

nginx 收到一個請求以後,如果發現需要訪問 upstream,就會執行對應的 peer.init 函數。這 是在初始化配置時設置的回調函數。這個函數最重要的作用是構造一張表,當前請求可以使 用的 upstream 服務器被依次添加到這張表中。之所以需要這張表,最重要的原因是如果 upstream 服務器出現異常,不能提供服務時,可以從這張表中取得其他服務器進行重試操 作。此外,這張表也可以用於負載均衡的計算。之所以構造這張表的行為放在這裡而不是在 前面初始化配置的階段,是因為 upstream 需要為每一個請求提供獨立隔離的環境。 為了討論 peer.init 的核心,我們還是看 IP hash 模塊的實現:

<code>r->upstream->peer.data = &iphp->rrp; 
 
ngx_http_upstream_init_round_robin_peer(r, us); 
 
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer; /<code>

第一行是設置數據指針,這個指針就是指向前面提到的那張表; 第二行是調用 Round Robin 模塊的回調函數對該模塊進行請求初始化。面前已經提到,一個 負載均衡模塊可以調用其他負載均衡模塊以提供功能的補充。 第三行是設置一個新的回調函數 get。該函數負責從表中取出某個服務器。除了 get 回調函 數,還有另一個 r->upstream->peer.free 的回調函數。該函數在 upstream 請求完成後調用, 負責做一些善後工作。比如我們需要維護一個 upstream 服務器訪問計數器,那麼可以在 get 函數中對其加 1,在 free 中對其減 1。如果是 SSL 的話,nginx 還提供兩個回調函數 peer.set_session 和 peer.save_session。一般來說,有兩個切入點實現負載均衡算法,其一是 在這裡,其二是在 get 回調函數中。

6. peer.get 和 peer.free 回調函數

這兩個函數是負載均衡模塊最底層的函數,負責實際獲取一個連接和回收一個連接的預備操 作。之所以說是預備操作,是因為在這兩個函數中,並不實際進行建立連接或者釋放連接的 動作,而只是執行獲取連接的地址或維護連接狀態的操作。需要理解的清楚一點,在 peer.get 函數中獲取連接的地址信息,並不代表這時連接一定沒有被建立,相反的,通過 get 函數的 返回值,nginx 可以瞭解是否存在可用連接,連接是否已經建立。這些返回值總結如下:


Nginx 源碼分析之 Filter 與 Upstream


Nginx 源碼分析之 Filter 與 Upstream


Nginx 源碼分析之 Filter 與 Upstream

負載均衡模塊的配置區集中在 upstream{}塊中。負載均衡模塊的回調函數體系是以 init_upstream 為起點,經歷 init_peer,最終到達 peer.get 和 peer.free。其中 init_peer 負責建 立每個請求使用的 server 列表,peer.get 負責從 server 列表中選擇某個 server(一般是不重 複選擇),而 peer.free 負責 server 釋放前的資源釋放工作。最後,這一節通過一張圖將 upstream 模塊和負載均衡模塊在請求處理過程中的相互關係展現出來。


感謝支持!後臺私信《Linux》獲取C++/Linux後臺開發進階視頻資料。


分享到:


相關文章: