nginx--upstream

upstream模塊

upstream模塊 (100%)

nginx模塊通常被分紅三大類:handler、filter和upstream。前面的章節中,讀者已經瞭解了handler、filter。利用這兩類模塊,可使nginx輕鬆完成任何單機工做。而本章介紹的upstream,將使nginx將跨越單機的限制,完成網絡數據的接收、處理和轉發。php

數據轉發功能,爲nginx提供了跨越單機的橫向處理能力,使nginx擺脫只能爲終端節點提供單一功能的限制,而使它具有了網路應用級別的拆分、封裝和整合的戰略功能。在雲模型大行其道的今天,數據轉發使nginx有能力構建一個網絡應用的關鍵組件。固然,一個網絡應用的關鍵組件每每一開始都會考慮經過高級開發語言編寫,由於開發比較方便,但系統到達必定規模,須要更重視性能的時候,這些高級語言爲了達成目標所作的結構化修改所付出的代價會使nginx的upstream模塊就呈現出極大的吸引力,由於他天生就快。做爲附帶,nginx的配置提供的層次化和鬆耦合使得系統的擴展性也可能達到比較高的程度。nginx

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

upstream模塊接口

從本質上說,upstream屬於handler,只是他不產生本身的內容,而是經過請求後端服務器獲得內容,因此才稱爲upstream(上游)。請求並取得響應內容的整個過程已經被封裝到nginx內部,因此upstream模塊只須要開發若干回調函數,完成構造請求和解析響應等具體的工做。算法

這些回調函數以下表所示:後端

create_request 生成發送到後端服務器的請求緩衝(緩衝鏈)。
reinit_request 在某臺後端服務器出錯的狀況,nginx會嘗試另外一臺後端服務器。 nginx選定新的服務器之後,會先調用此函數,而後再次調用 create_request,以從新初始化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模塊的基本規則:配置該模塊纔會執行該模塊。session

{ 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步中的最大差異會出如今第二、三、四、5上。其中第二、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->status->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高效的奧祕之一。

 

負載均衡模塊 (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負載均衡模塊的配置的內存佈局。

_images/chapter-5-1.PNG

從圖上能夠看出,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總體流程以下:
_images/chapter-5-2.PNG
相關文章
相關標籤/搜索