天天看點

ng的upstream子產品

upstream子產品 (100%)

nginx子產品一般被分成三大類:handler、filter和upstream。前面的章節中,讀者已經了解了handler、filter。利用這兩類子產品,可以使nginx輕松完成任何單機工作。而本章介紹的upstream子產品,将使nginx跨越單機的限制,完成網絡資料的接收、處理和轉發。

資料轉發功能,為nginx提供了跨越單機的橫向處理能力,使nginx擺脫隻能為終端節點提供單一功能的限制,而使它具備了網路應用級别的拆分、封裝和整合的戰略功能。在雲模型大行其道的今天,資料轉發是nginx有能力建構一個網絡應用的關鍵元件。當然,鑒于開發成本的問題,一個網絡應用的關鍵元件一開始往往會采用進階程式設計語言開發。但是當系統到達一定規模,并且需要更重視性能的時候,為了達到所要求的性能目标,進階語言開發出的元件必須進行結構化修改。此時,對于修改代價而言,nginx的upstream子產品呈現出極大的吸引力,因為它天生就快。作為附帶,nginx的配置系統提供的階層化和松耦合使得系統的擴充性也達到比較高的程度。

言歸正傳,下面介紹upstream的寫法。

upstream子產品接口

從本質上說,upstream屬于handler,隻是他不産生自己的内容,而是通過請求後端伺服器得到内容,是以才稱為upstream(上遊)。請求并取得響應内容的整個過程已經被封裝到nginx内部,是以upstream子產品隻需要開發若幹回調函數,完成構造請求和解析響應等具體的工作。

這些回調函數如下表所示:

create_request 生成發送到後端伺服器的請求緩沖(緩沖鍊),在初始化upstream 時使用。
reinit_request 在某台後端伺服器出錯的情況,nginx會嘗試另一台後端伺服器。 nginx標明新的伺服器以後,會先調用此函數,以重新初始化 upstream子產品的工作狀态,然後再次進行upstream連接配接。
process_header 處理後端伺服器傳回的資訊頭部。所謂頭部是與upstream server 通信的協定規定的,比如HTTP協定的header部分,或者memcached 協定的響應狀态部分。
abort_request 在用戶端放棄請求時被調用。不需要在函數中實作關閉後端服務 器連接配接的功能,系統會自動完成關閉連接配接的步驟,是以一般此函 數不會進行任何具體工作。
finalize_request 正常完成與後端伺服器的請求後調用該函數,與abort_request 相同,一般也不會進行任何具體工作。
input_filter 處理後端伺服器傳回的響應正文。nginx預設的input_filter會 将收到的内容封裝成為緩沖區鍊ngx_chain。該鍊由upstream的 out_bufs指針域定位,是以開發人員可以在子產品以外通過該指針 得到後端伺服器傳回的正文資料。memcached子產品實作了自己的 input_filter,在後面會具體分析這個子產品。
input_filter_init 初始化input filter的上下文。nginx預設的input_filter_init 直接傳回。

memcached子產品分析

memcache是一款高性能的分布式cache系統,得到了非常廣泛的應用。memcache定義了一套私有通信協定,使得不能通過HTTP請求來通路memcache。但協定本身簡單高效,而且memcache使用廣泛,是以大部分現代開發語言和平台都提供了memcache支援,友善開發者使用memcache。

nginx提供了ngx_http_memcached子產品,提供從memcache讀取資料的功能,而不提供向memcache寫資料的功能。作為web伺服器,這種設計是可以接受的。

下面,我們開始分析ngx_http_memcached子產品,一窺upstream的奧秘。

Handler子產品?

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

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

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

{ 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 }      

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

Upstream子產品!

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

1. 建立upstream資料結構。

if (ngx_http_upstream_create(r) != NGX_OK) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}      

2. 設定子產品的tag和schema。schema現在隻會用于日志,tag會用于buf_chain管理。

u = r->upstream;

ngx_str_set(&u->schema, "memcached://");
u->output.tag = (ngx_buf_tag_t) &ngx_http_memcached_module;      

3. 設定upstream的後端伺服器清單資料結構。

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

4. 設定upstream回調函數。在這裡列出的代碼稍稍調整了代碼順序。

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;      

5. 建立并設定upstream環境資料結構。

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;      

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

r->main->count++;
ngx_http_upstream_init(r);
return NGX_DONE;      

任何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請求和用戶端請求進行一對一綁定,這個設計有優勢也有缺陷。優勢就是簡化子產品開發,可以将精力集中在子產品邏輯上,而缺陷同樣明顯,一對一的設計很多時候都不能滿足複雜邏輯的需要。對于這一點,将會在後面的原理篇來闡述。

回調函數

前面剖析了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:子產品的業務重點函數。memcache協定的頭部資訊被定義為第一行文本,可以找到這段代碼證明:

for (p = u->buffer.pos; p < u->buffer.last; p++) {
    if ( * p == LF) {
    goto found;
}      

如果在已讀入緩沖的資料中沒有發現LF(‘n’)字元,函數傳回NGX_AGAIN,表示頭部未完全讀入,需要繼續讀取資料。nginx在收到新的資料以後會再次調用該函數。

nginx處理後端伺服器的響應頭時隻會使用一塊緩存,所有資料都在這塊緩存中,是以解析頭部資訊時不需要考慮頭部資訊跨越多塊緩存的情況。而如果頭部過大,不能儲存在這塊緩存中,nginx會傳回錯誤資訊給用戶端,并記錄error log,提示緩存不夠大。

process_header的重要職責是将後端伺服器傳回的狀态翻譯成傳回給用戶端的狀态。例如,在ngx_http_memcached_process_header中,有這樣幾段代碼:

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;      

u->state用于計算upstream相關的變量。比如u->state->status将被用于計算變量“upstream_status”的值。u->headers_in将被作為傳回給用戶端的響應傳回狀态碼。而第一行則是設定傳回給用戶端的響應的長度。

在這個函數中不能忘記的一件事情是處理完頭部資訊以後需要将讀指針pos後移,否則這段資料也将被複制到傳回給用戶端的響應的正文中,進而導緻正文内容不正确。

u->buffer.pos = p + 1;      

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子產品的基本組成。upstream子產品是從handler子產品發展而來,指令系統和子產品生效方式與handler子產品無異。不同之處在于,upstream子產品在handler函數中設定衆多回調函數。實際工作都是由這些回調函數完成的。每個回調函數都是在upstream的某個固定階段執行,各司其職,大部分回調函數一般不會真正用到。upstream最重要的回調函數是create_request、process_header和input_filter,他們共同實作了與後端伺服器的協定的解析部分。

負載均衡子產品 (100%)

負載均衡子產品用于從”upstream”指令定義的後端主機清單中選取一台主機。nginx先使用負載均衡子產品找到一台主機,再使用upstream子產品實作與這台主機的互動。為了友善介紹負載均衡子產品,做到言之有物,以下選取nginx内置的ip hash子產品作為實際例子進行分析。

配置

要了解負載均衡子產品的開發方法,首先需要了解負載均衡子產品的使用方法。因為負載均衡子產品與之前書中提到的子產品差别比較大,是以我們從配置入手比較容易了解。

在配置檔案中,我們如果需要使用ip hash的負載均衡算法。我們需要寫一個類似下面的配置:

upstream test {
    ip_hash;

    server 192.168.0.1;
    server 192.168.0.2;
}      

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

upstream test {
    server 192.168.0.1 weight=5;
    ip_hash;
    server 192.168.0.2 weight=7;
}      

神奇的事情出現了:

nginx: [emerg] invalid parameter "weight=7" in nginx.conf:103
configuration file nginx.conf test failed      

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

指令

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

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
};      

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

鈎子

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

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;
}      

這段代碼中有兩點值得我們注意。一個是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屬性;

此外還有下面屬性:

  1. NGX_HTTP_UPSTREAM_WEIGHT:可以在server中使用weight屬性;
  2. NGX_HTTP_UPSTREAM_BACKUP:可以在server中使用backup屬性。

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

設定init_upstream回調

nginx初始化upstream時,會在ngx_http_upstream_init_main_conf函數中調用設定的回調函數初始化負載均衡子產品。這裡不太好了解的是uscf的具體位置。通過下面的示意圖,說明upstream負載均衡子產品的配置的記憶體布局。

ng的upstream子產品

從圖上可以看出,MAIN_CONF中ngx_upstream_module子產品的配置項中有一個指針數組upstreams,數組中的每個元素對應就是配置檔案中每一個upstream{}的資訊。更具體的将會在後面的原理篇讨論。

初始化配置

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

ngx_http_upstream_init_round_robin(cf, us);
us->peer.init = ngx_http_upstream_init_ip_hash_peer;      

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

初始化請求

nginx收到一個請求以後,如果發現需要通路upstream,就會執行對應的peer.init函數。這是在初始化配置時設定的回調函數。這個函數最重要的作用是構造一張表,目前請求可以使用的upstream伺服器被依次添加到這張表中。之是以需要這張表,最重要的原因是如果upstream伺服器出現異常,不能提供服務時,可以從這張表中取得其他伺服器進行重試操作。此外,這張表也可以用于負載均衡的計算。之是以構造這張表的行為放在這裡而不是在前面初始化配置的階段,是因為upstream需要為每一個請求提供獨立隔離的環境。

為了讨論peer.init的核心,我們還是看IP hash子產品的實作:

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;      

第一行是設定資料指針,這個指針就是指向前面提到的那張表;

第二行是調用Round Robin子產品的回調函數對該子產品進行請求初始化。面前已經提到,一個負載均衡子產品可以調用其他負載均衡子產品以提供功能的補充。

第三行是設定一個新的回調函數get。該函數負責從表中取出某個伺服器。除了get回調函數,還有另一個r->upstream->peer.free的回調函數。該函數在upstream請求完成後調用,負責做一些善後工作。比如我們需要維護一個upstream伺服器通路計數器,那麼可以在get函數中對其加1,在free中對其減1。如果是SSL的話,nginx還提供兩個回調函數peer.set_session和peer.save_session。一般來說,有兩個切入點實作負載均衡算法,其一是在這裡,其二是在get回調函數中。

peer.get和peer.free回調函數

這兩個函數是負載均衡子產品最底層的函數,負責實際擷取一個連接配接和回收一個連接配接的預備操作。之是以說是預備操作,是因為在這兩個函數中,并不實際進行建立連接配接或者釋放連接配接的動作,而隻是執行擷取連接配接的位址或維護連接配接狀态的操作。需要了解的清楚一點,在peer.get函數中擷取連接配接的位址資訊,并不代表這時連接配接一定沒有被建立,相反的,通過get函數的傳回值,nginx可以了解是否存在可用連接配接,連接配接是否已經建立。這些傳回值總結如下:

傳回值 說明 nginx後續動作
NGX_DONE 得到了連接配接位址資訊,并且連接配接已經建立。 直接使用連接配接,發送資料。
NGX_OK 得到了連接配接位址資訊,但連接配接并未建立。 建立連接配接,如連接配接不能立即建立,設定事件, 暫停執行本請求,執行别的請求。
NGX_BUSY 所有連接配接均不可用。 傳回502錯誤至用戶端。

各位讀者看到上面這張表,可能會有幾個問題浮現出來:

Q: 什麼時候連接配接是已經建立的?
A: 使用後端keepalive連接配接的時候,連接配接在使用完以後并不關閉,而是存放在一個隊列中,新的請求隻需要從隊列中取出連接配接,這些連接配接都是已經準備好的。
Q: 什麼叫所有連接配接均不可用?
A: 初始化請求的過程中,建立了一張表,get函數負責每次從這張表中不重複的取出一個連接配接,當無法從表中取得一個新的連接配接時,即所有連接配接均不可用。
Q: 對于一個請求,peer.get函數可能被調用多次麼?
A: 正式如此。當某次peer.get函數得到的連接配接位址連接配接不上,或者請求對應的伺服器得到異常響應,nginx會執行ngx_http_upstream_next,然後可能再次調用peer.get函數嘗試别的連接配接。upstream整體流程如下:
ng的upstream子產品

本節回顧

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