天天看點

Emiller的子產品開發指南0. 預備知識 1. Nginx子產品任務委派的主要輪廓 2.  Nginx子產品的組成 3. 處理子產品、過濾子產品和 負載均衡子產品 4. Advanced Topics5. Writing and Compiling a New Nginx ModuleAppendix A: Code ReferencesAppendix B: Changelog

Emiller 的子產品開發指南

作者:Evan Miller 草稿: July 14, 2008 (changes)

譯者:YaoWeibin 草稿:Sep 20,2008

0. 預備知識... 1

1. Nginx子產品任務委派的主要輪廓... 1

2.  Nginx子產品的組成... 3

2.1. 子產品的配置結構體... 3

2.2. 子產品的指令... 3

2.3. 子產品的上下文... 6

2.3.1. create_loc_conf 7

2.3.2. merge_loc_conf 8

2.4. 子產品定義... 9

2.5. 子產品注冊... 9

2.5.1.處理子產品的注冊... 10

2.5.2. 過濾子產品的注冊... 10

3. 處理子產品、過濾子產品和 負載均衡子產品... 11

3.1. 剖析處理子產品(非代理) 12

3.1.1. 獲得位置配置結構體... 12

3.1.2. 産生回複... 12

3.1.3. 發送HTTP頭部... 13

3.1.4. 發送HTTP負載... 14

3.2. 上遊子產品剖析(又稱代理子產品)... 15

3.2.1.代理子產品回調函數的概要... 16

3.2.2. create_request 回調函數... 17

3.2.3. process_header 回調函數... 18

3.2.4. 狀态保持... 19

3.3. 剖析頭部過濾函數... 20

3.4. 剖析負載過濾函數... 21

3.5. 剖析負載均衡子產品... 22

3.5.1.激活指令... 23

3.5.2.注冊函數... 23

3.5.3.上遊主機初始化函數... 25

3.5.4.同伴初始化函數... 26

3.5.5.負載均衡函數... 28

3.5.6. 同伴釋放函數... 29

4. Advanced Topics. 30

4.1. Shared Memory. 30

4.1.1. A (fore)word of caution. 31

4.1.2. Creating and using a shared memory segment 31

4.1.3. Using the slab allocator 33

4.1.4. Spinlocks, atomic memory access. 34

4.1.5. Using rbtrees. 35

4.2. Subrequests. 36

4.2.1. Internal redirects. 36

4.2.2. A single subrequest 36

4.2.3. Sequential subrequests. 38

4.2.4. Parallel subrequests. 40

4.3. TODO: Advanced Topics Not Yet Covered Here. 40

5. Writing and Compiling a New Nginx Module. 41

Appendix A: Code References. 42

Appendix B: Changelog. 42

翻譯說明:

在Nginx的子產品編寫過程中,時常苦于文檔的不足,而源代碼中又沒多少注釋。感謝Emiller的這篇英文文檔帶我入門。這次我翻譯的是一至三章,希望能給中國廣大Nginxer能有幫助,對于一般的應用來說,前三章的内容就足夠了。如果時間夠的話,我可能也會翻譯第四章。

由于英語水準一般,接觸Nginx時間不多,翻譯中碰到的錯誤在所難免,如果您覺得哪裡翻譯得不對,請跟我聯系:[email protected]

翻譯詞彙對照表:

Backend:後端備選伺服器。

Buffer:緩沖區。

Callback:回調函數,一般來說是将某個回調函數指派給某個函數指針

CHAIN OF RESPONSIBILITY:接力連結清單。

Filter:過濾子產品/函數,子產品和函數的概念似乎有點模糊不清。

Handler: 處理子產品/函數,另外也有指向具體的處理函數的指針或句柄的意思。

Installation:願意為安裝,我覺得還是譯作注冊好點。

Load-balancer:負載均衡子產品/函數。

Location:指URL位置,比如http://img.blog.163.com/photo/中的“/photo”目錄。

Master:主程序,由主程序産生worker程序,同時也可以而監視worker程序的動态,worker因為異常而退出的時候也可以重新開機一個新的worker程序。

Reference:一般譯作“引用”,不過很多時候,似乎還不如稱作“指針”來的直接些。

Request:HTTP 請求。

Response:HTTP 回複。

Server-side:服務端。

Upstream:上遊服務(器),有時亦稱backend。

Worker:工作程序,子產品真正發揮作用的地方。

0. 預備知識

對于C語言,你應該熟悉。不僅是會C的文法,對于結構體和預處理指令你應該有深入的了解,特别見到大量的指針和函數引用的時候不會驚慌失措。如果覺得需要補習,就多看看K&R(C語言的文法書)。

如果你對于HTTP協定已經有了基本概念,那是很有好處的。畢竟你正在Web服務上工作。

你應該熟悉Nginx的配置檔案。如果不熟悉,也沒關系,這裡有一些基本了解:Nginx配置檔案主要分成四部分:main(全局設定)、server(主機設定)、upstream(上遊伺服器設定)和location(URL比對特定位置後的設定)。每部分包含若幹個指令。main部分設定的指令将影響其它所有部分;server部分的指令主要用于指定主機和端口;upstream的指令用于設定後端伺服器;location部分用于比對網頁位置(比如,根目錄“/”,“/images”,等等)。Location部分會繼承server部分的指令,而server部分的指令會繼承main部分;upstream既不會繼承指令也不會影響其他部分。它有自己的特殊指令,不需要在其他地方的應用。在下面很多地方都會涉及這四個部分,不要忘記喲。

讓我們開始吧。

1. Nginx子產品任務委派的主要輪廓

Nginx子產品主要有3種角色:

·         handlers(處理子產品) 用于處理HTTP請求,然後産生輸出

·         filters(過濾子產品) 過濾handler産生的輸出

·         load-balancers(負載均衡子產品)當有多于一台的後端備選伺服器時,選擇一台轉發HTTP請求

子產品可以做任何你配置設定給web伺服器的實際工作:當Nginx發送檔案或者轉發請求到其他伺服器,有處理子產品為其服務;當需要Nginx把輸出壓縮或者在服務端加一些東西,可以用過濾子產品;Nginx的核心子產品主要管理網絡層和應用層協定,以及啟動針對特定應用的一系列候選子產品。集中式的體系結構讓你可以随心所欲得實作一些功能強大的内部單元。

注意:Nginx不像apache,子產品不是動态添加的(換句話就是說,所有的子產品都要預先編譯進Nginx的二進制可執行檔案)。

子產品是如何被調用的?典型的講,當伺服器啟動,每個處理子產品都有機會映射到配置檔案中定義的特定位置;如果有多個處理子產品映射到特定位置時,隻有一個會“赢”(但聰明如你,當然不會讓這些沖突産生)。處理子產品以三種形式傳回:OK、ERROR、或者放棄處理這個請求而讓預設處理子產品來處理(主要是用來處理一些靜态檔案,事實上如果是位置正确而真實的靜态檔案,預設的處理子產品會搶先處理)。

如果處理子產品把請求反向代理到後端的伺服器,就變成另外一類的子產品:負載均衡子產品。負載均衡子產品的配置中有一組後端伺服器,當一個HTTP請求過來時,它決定哪台伺服器應當獲得這個請求。Nginx的負載均衡子產品采用兩種方法:輪轉法,它處理請求就像紙牌遊戲一樣從頭到尾分發;IP哈希法,在衆多請求的情況下,它確定來自同一個IP的請求會分發到相同的後端伺服器。

如果處理子產品沒有産生錯誤,過濾子產品将被調用。多個過濾子產品能映射到每個位置,是以(比如)每個請求都可以被壓縮成塊。它們的執行順序在編譯時決定。過濾子產品是經典的“接力連結清單(CHAIN OF RESPONSIBILITY)”模型:一個過濾子產品被調用,完成其工作,然後調用下一個過濾子產品,直到最後一個過濾子產品。Nginx完成這個回複。

真正cool的地方是在過濾子產品鍊中,每個過濾子產品不會等上一個過濾子產品全部完成;它能把前一個過濾子產品的輸出作為其處理内容;有點像Unix中的流水線。過濾子產品能以buffer(緩沖區)為機關進行操作,這些buffer一般都是一頁(4K)大小,當然你也可以在nginx.conf檔案中進行配置。這意味着,比如,子產品可以壓縮來自後端伺服器的回複,然後像流一樣的到達用戶端,直到整個回複發送完成。

是以總結下上面的内容,一個典型的處理周期是這樣的:

用戶端發送HTTP請求->Nginx基于配置檔案中的位置選擇一個合适的處理子產品->(如果有)負載均衡子產品選擇一台後端伺服器->處理子產品進行處理并把輸出緩沖放到第一個過濾子產品上->第一個過濾子產品處理後輸出給第二個過濾子產品->然後第二個過濾子產品又到第三個->依此類推->最後把回複發給用戶端。

我說“典型”這個詞是因為Nginx的子產品調用是具有很強的定制性的。子產品開發者需要花很多精力精确定義子產品在何時如何産生作用(我認為是件不容易的事)。子產品的調用事實上通過一系列的回調函數來實作,很多很多。名義上來說,你的函數可在以下時段執行某些功能:

·         當服務讀配置檔案之前

·         讀存在location和server或其他任何部分的每一個配置指令

·         當Nginx初始化全局部分的配置時

·         當Nginx初始化主機部分(比如主機/端口)的配置時

·         當Nginx将全局部分的配置與主機部分的配置合并的時候

·         當Nginx初始化比對位置部配置設定置的時候

·         當Nginx将其上層主機配置與位置部配置設定置合并的時候

·         當Nginx的主(master)程序開始的時候

·         當一個新的工作程序(worker)開始的時候

·         當一個工作程序退出的時候

·         當主程序退出的時候

·         處理HTTP請求

·         過濾HTTP回複的頭部

·         過濾HTTP回複的主體

·         選擇一台後端伺服器

·         初始化到後端伺服器的請求

·         重新初始化到後端的伺服器的請求

·         處理來自後端伺服器的回複

·         完成與後端伺服器的互動

難以置信!有這麼多的功能任你處置,而你隻需僅僅通過多組有用的鈎子(由函數指針組成的結構體)和相應的實作函數。讓我們開始接觸一些子產品吧。

2.  Nginx子產品的組成

我說過, Nginx子產品的使用是很靈活的。本段描述的東西會經常出現。它引導你了解子產品,也可以成為你開始寫子產品的參考。

2.1. 子產品的配置結構體

子產品的配置結構體的定義有三種,分别是全局,主機和位置的配置結構體。大多數子產品僅僅需要一個位置的配置結構體。名稱約定是這樣的:ngx_http_<module name>_(main|srv|loc)_conf_t。這裡有一個來自dav子產品的列子:

typedef struct {

    ngx_uint_t  methods;

    ngx_flag_t  create_full_put_path;

    ngx_uint_t  access;

} ngx_http_dav_loc_conf_t;

注意Nginx有一些特别的類型(如:

ngx_uint_t

ngx_flag_t

);這些可能是一些基本類型的别名。(如果你好奇的話,可以看這裡:core/ngx_config.h)

這些類型用在配置結構體中出現的情形很多。

2.2. 子產品的指令

子產品的指令出現在靜态數組

ngx_command_t

。這裡有一個例子,說明它們是如何被定義的,來自我寫的一個小子產品:

static ngx_command_t  ngx_http_circle_gif_commands[] = {
           
    { ngx_string("circle_gif"),
           
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
           
      ngx_http_circle_gif,
           
      NGX_HTTP_LOC_CONF_OFFSET,
           
      0,
           
      NULL },
           
    { ngx_string("circle_gif_min_radius"),
           
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
           
      ngx_conf_set_num_slot,
           
      NGX_HTTP_LOC_CONF_OFFSET,
           
      offsetof(ngx_http_circle_gif_loc_conf_t, min_radius),
           
      NULL },
           
      ...
           
      ngx_null_command
           
};
           

這是

ngx_command_t

的函數原型(也就是我們定義的那些結構體),出自core/ngx_conf_file.h :

struct ngx_command_t {
           
    ngx_str_t             name;
           
    ngx_uint_t            type;
           
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
           
    ngx_uint_t            conf;
           
    ngx_uint_t            offset;
           
    void                 *post;
           

};

參數多了點,但每個參數都是有用的:

name

是指令的字元串(也就是包含指令名稱),不包含空格(有空格的話就是另外指令的參數了)。資料類型是

ngx_str_t

,經常用來進行字元串執行個體化(比如:

ngx_str("proxy_pass")

)。注意:

ngx_str_t

結構體由包含有字元串的

data

成員和表示字元串長度的

len

成員組成。Nginx用這個資料類型來存放字元串。

type

是辨別的集合,表明這個指令在哪裡出現是合法的、指令的參數個數。應用中,辨別一般是下面多個值的位或:

  • NGX_HTTP_MAIN_CONF

    : 指令出現在全局配置部分是合法的
  • NGX_HTTP_SRV_CONF

    : 指令在主機配置部分出現是合法的
  • NGX_HTTP_LOC_CONF

    : 指令在位置配置部分出現是合法的
  • NGX_HTTP_UPS_CONF

    : 指令在上遊伺服器配置部分出現是合法的
  • NGX_CONF_NOARGS

    : 指令沒有參數
  • NGX_CONF_TAKE1

    : 指令讀入一個參數
  • NGX_CONF_TAKE2

    : 指令讀入兩個參數
  • ...
  • NGX_CONF_TAKE7

    : 指令讀入七個參數
  • NGX_CONF_FLAG

    :  指令讀入一個布爾型資料
  • NGX_CONF_1MORE

    : 指令至少讀入1個參數
  • NGX_CONF_2MORE

    : 指令至少讀入2個參數

這裡有很多另外的選項:core/ngx_conf_file.h。

結構體成員set是一個函數指針,用來設定子產品的配置;典型地,這個函數會轉化指令傳進來的參數,然後将合适的值儲存到配置結構體。這個設定函數有三個參數:

1.            指向ngx_conf_t結構體的指針,包含從指令後面傳過來的參數。

2.            指向目前ngx_command_t結構體的指針

3.            指向自定義子產品配置結構體的指針

這個設定函數在指令被遇到的時候就會調用。在自定義的配置結構體中,Nginx提供了多個函數用來儲存特定類型的資料,這些函數包含有:

·         ngx_conf_set_flag_slot: 将"on"或"off"轉化為

·         ngx_conf_set_str_slot: 将字元串儲存位ngx_str_t類型

·         ngx_conf_set_num_slot: 解析一個數字并儲存為int型

·         ngx_conf_set_size_slot: 解析一個資料大小(如:"8k", "1m") 并儲存為 size_t類型

還有另外一些,很容易查到(看,core/ngx_conf_file.h)。子產品也可以把它們自己函數的引用放在這裡,但這樣内嵌的類型不是很好。

那這些内嵌函數怎麼知道要把值儲存在哪裡呢?

ngx_command_t

接下來

的兩個成員conf和offset正好可用。conf告訴Nginx把這個值是放在全局配置部分、主機配置部分還是位置配置部分呢(用

NGX_HTTP_MAIN_CONF_OFFSET

,

NGX_HTTP_SRV_CONF_OFFSET

NGX_HTTP_LOC_CONF_OFFSET

)。然後offset确定到底是儲存在結構體的哪個位置。

最後,post指向子產品在讀配置的時候需要的一些零碎變量。一般它是NULL。

這個

ngx_command_t

數組在讀入

ngx_null_command

後停止,也即最後一個成員。

2.3. 子產品的上下文

靜态的

ngx_http_module_t

結構體,包含一大把的函數引用。用來建立三個部分的配置和合并配置。一般結構體命名為

ngx_http_<module name>_module_ctx

。以此,這些函數引用包括:

l        在讀入配置前調用

l        在讀入配置後調用

l        在建立全局部配置設定置時調用(比如,用來配置設定空間和設定預設值)

l        在初始化全局部分的配置時調用(比如,把原來的預設值用nginx.conf讀到的值來覆寫)

l        在建立主機部分的配置時調用

l        與全局部配置設定置合并時調用

l        建立位置部分的配置時掉用

l        與主機部配置設定置合并時調用

這些函數參數不同,依賴與它們的功能。這裡有這個結構體的定義,摘自http/ngx_http_config.h,你可以看到屬性各不同的回調函數:

typedef struct {
           
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
           
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);
           
    void       *(*create_main_conf)(ngx_conf_t *cf);
           
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);
           
    void       *(*create_srv_conf)(ngx_conf_t *cf);
           
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
           
    void       *(*create_loc_conf)(ngx_conf_t *cf);
           
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
           
} ngx_http_module_t;
           

如果不用某些函數,可以設定為NULL,Nginx會剔除它。

大多數處理子產品隻使用最後兩個:一個函數用來為特定的位置部分的配置結構體配置設定記憶體(稱為

ngx_http_<module name>_create_loc_conf

),另外一個函數用來設定預設值和與繼承過來的配置合并(稱為

ngx_http_<module name >_merge_loc_conf

)。這個合并函數負責檢驗讀入的數值是否有效,否則報錯;這些錯誤将會中止服務的運作。

這裡有一個子產品上下文結構體的例子:

static ngx_http_module_t  ngx_http_circle_gif_module_ctx = {
           
    NULL,                          /* preconfiguration */
           
    NULL,                          /* postconfiguration */
           
    NULL,                          /* create main configuration */
           
    NULL,                          /* init main configuration */
           
    NULL,                          /* create server configuration */
           
    NULL,                          /* merge server configuration */
           
    ngx_http_circle_gif_create_loc_conf,  /* create location configuration */
           
    ngx_http_circle_gif_merge_loc_conf /* merge location configuration */
           

};

現在開始講得更深一點。這些配置回調函數看其來很像,所有子產品都一樣,而且Nginx的API都會用到這個部分,是以值得了解。

2.3.1. create_loc_conf

create_loc_conf函數骨架看起來像這個樣子,摘自我寫的circle_gif子產品(看源代碼)。它的參數是結構體         ngx_conf_t                  ,傳回更新的子產品配置結構體(例子中是ngx_http_circle_gif_loc_conf_t              )。           
static void *
           
ngx_http_circle_gif_create_loc_conf(ngx_conf_t *cf)
           
{
           
    ngx_http_circle_gif_loc_conf_t  *conf;
           
    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_circle_gif_loc_conf_t));
           
    if (conf == NULL) {
           
        return NGX_CONF_ERROR;
           
    }
           
    conf->min_radius = NGX_CONF_UNSET_UINT;
           
    conf->max_radius = NGX_CONF_UNSET_UINT;
           
    return conf;
           
}
           

第一個需要注意的是Nginx的記憶體配置設定,隻要使用

ngx_palloc

malloc

的包裝函數)或ngx_pcalloc

calloc

的包裝函數)就不需要關心記憶體的釋放。

UNSET

常量有:NGX_CONF_UNSET_UINT

,

NGX_CONF_UNSET_PTR

,

NGX_CONF_UNSET_SIZE

,

NGX_CONF_UNSET_MSEC

, 和一起的

NGX_CONF_UNSET

UNSET

告訴合并函數這些值還沒有被初始化過,需要并覆寫。

2.3.2. merge_loc_conf

這是用在circle_gif子產品的合并函數:

static char *
           
ngx_http_circle_gif_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
           
{
           
    ngx_http_circle_gif_loc_conf_t *prev = parent;
           
    ngx_http_circle_gif_loc_conf_t *conf = child;
           
    ngx_conf_merge_uint_value(conf->min_radius, prev->min_radius, 10);
           
    ngx_conf_merge_uint_value(conf->max_radius, prev->max_radius, 20);
           
    if (conf->min_radius < 1) {
           
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 
           
            "min_radius must be equal or more than 1");
           
        return NGX_CONF_ERROR;
           
    }
           
    if (conf->max_radius < conf->min_radius) {
           
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 
           
            "max_radius must be equal or more than min_radius");
           
        return NGX_CONF_ERROR;
           
    }
           
    return NGX_CONF_OK;
           
}
           

首先需要注意的是Nginx為不同的類型提供了良好的合并函數(

ngx_conf_merge_<data type>_value

);這些參數含義是:

1、            目前參數變量

2、            如果第一個參數沒有被設定

3、            如果第一個和第二個參數都沒有設定時的預設值

結果會儲存在第一個參數中。有效的合并函數包括

ngx_conf_merge_size_value

,

ngx_conf_merge_msec_value

,在core/ngx_conf_file.h裡有完整的列出。

有個問題:因為第一個參數是傳值的,這些值是如何寫到第一個參數中?

回答:這些函數其實都是預處理指令(在真正編譯之前,它們會被擴充成一些if語句)。

注意錯誤是如何産生的;這些函數把錯誤資訊寫到log檔案中,然後傳回

NGX_CONF_ERROR

。傳回代碼會中止服務的啟動。(因為被辨別為NGX_LOG_EMERG,這些消息也會被會輸出到标準輸出;core/ngx_log.h有完整的日志級别。)

2.4. 子產品定義

第二步,我們間接的介紹更多一層,結構體

ngx_module_t

。這個結構體變量命名為

ngx_http_<module name>_module

。它包含有子產品的主要内容和指令的執行部分,也有一些回調函數(退出線程,退出程序,等等)。這個子產品的定義是把資料處理關聯到特定子產品的關鍵。子產品的定義像這個樣子:

ngx_module_t  ngx_http_<module name>_module = {
           
    NGX_MODULE_V1,
           
    &ngx_http_<module name>_module_ctx, /* module context */
           
    ngx_http_<module name>_commands,   /* module directives */
           
    NGX_HTTP_MODULE,               /* module type */
           
    NULL,                          /* init master */
           
    NULL,                          /* init module */
           
    NULL,                          /* init process */
           
    NULL,                          /* init thread */
           
    NULL,                          /* exit thread */
           
    NULL,                          /* exit process */
           
    NULL,                          /* exit master */
           
    NGX_MODULE_V1_PADDING
           
};
           

…僅僅替換掉合适的子產品名稱就可以了。在程序/線程退出的時候,子產品可以添加一些回調函數來運作,但大多數子產品為了簡單的原因而沒有使用。(這些回調函數的參數,可以參考core/ngx_conf_file.h.)

2.5. 子產品注冊

有兩種途徑來注冊(Installation)子產品:處理子產品經常是通過指令的回調函數來注冊,過濾子產品通過子產品上下文結構體中的postconfigration回調函數來注冊。最後,我們告訴Nginx如何找到這些代碼。(負載均衡子產品有點特殊,是個費解的例子,具體可以看3.5節)

2.5.1.處理子產品的注冊

處理子產品的注冊通過添加代碼到指令中的回調函數。比如,我的circle gif

ngx_command_t

結構體看起來這樣的:

    { ngx_string("circle_gif"),
           
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
           
      ngx_http_circle_gif,
           
      0,
           
      0,
           
      NULL }
           

回調函數是第三個成員,在這個例子中是

ngx_http_circle_gif

函數

。須知這個回調函數參數分别是配置結構體(

ngx_conf_t

,儲存使用者的參數),相關的

ngx_command_t

結構體,以及一個指向子產品自定義的配置結構體。在我的circle gif 子產品,這個函數看來像這樣:

static char *
           
ngx_http_circle_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
           
{
           
    ngx_http_core_loc_conf_t  *clcf;
           
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
           
    clcf->handler = ngx_http_circle_gif_handler;
           
    return NGX_CONF_OK;
           
}
           

函數中有兩步,獲得這個位置配置的“核心”結構體,然後為它配置設定一個處理函數。是不是很簡單,呵呵。

2.5.2. 過濾子產品的注冊

過濾子產品通過配置後的函數(postconfigration)來注冊。它們一般包括兩種過濾函數:頭部過濾函數

header filters)處理HTTP頭,負載過濾函數(body filters)處理負載。它們的注冊在同一個位置。

以chunked filter子產品為例,它的子產品上下文是這樣的:

static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = {
           
   NULL,                                  /* preconfiguration */
           
    ngx_http_chunked_filter_init,          /* postconfiguration */
           
  ...
           
};
           

下面是

ngx_http_chunked_filter_init

函數做的一些事情:

static ngx_int_t
           
ngx_http_chunked_filter_init(ngx_conf_t *cf)
           
{
           
    ngx_http_next_header_filter = ngx_http_top_header_filter;
           
    ngx_http_top_header_filter = ngx_http_chunked_header_filter;
           
    ngx_http_next_body_filter = ngx_http_top_body_filter;
           
    ngx_http_top_body_filter = ngx_http_chunked_body_filter;
           
    return NGX_OK;
           
}
           

這裡到底做了些什麼?如果你還記得,過濾子產品組成了一條“接力連結清單”。當一個處理子產品産生一個回複,它會調用兩個函數:

ngx_http_output_filter

調用全局函數

ngx_http_top_body_filter

ngx_http_send_header

調用全局函數

ngx_top_header_filter

ngx_http_top_body_filter

ngx_http_top_header_filter

是負載和頭部各自的“連結清單頭部”。在這個鍊上每個成員都有指向下一個成員的函數指針(這個指針稱為

ngx_http_next_body_filter

ngx_http_next_header_filter

)。當一個過濾子產品執行完成後,它就調用下一個過濾子產品,直到一個特殊的“寫”過濾子產品被調用,它把整個HTTP包裹起來。在filter_init函數中,可以看到是把子產品本身添加到過濾子產品鍊中;它用next變量儲存了一個指向舊的“top”過濾子產品,然後聲明自己是新的“top”過濾子產品。(是以,最後注冊進過濾鍊的子產品是最先被執行的。)

作者邊注:這是如何正常工作的呢?

原來每個過濾函數用下面的語句或者錯誤代碼傳回:

return ngx_http_next_body_filter();

是以,如果過濾連結清單達到最後一個成員(特别定義的),就會傳回一個OK。如果出現錯誤,整條連結清單會被縮短,Nginx産生合适的錯誤消息。這是一條單向連結清單,僅僅使用函數引用,就可以實作快速的錯誤傳回。聰明。

3. 處理子產品、過濾子產品和 負載均衡子產品

接下來我們把子產品的細節放到顯微鏡下面來看,它們到底怎麼運作的。

3.1. 剖析處理子產品(非代理)

處理子產品一般做四樣東西:獲得位置配置結構體,産生合适的回複,發送HTTP頭部和發送HTTP負載。它隻有一個變量--請求結構體。這個結構體有很多關于用戶端請求的有用資訊,比如請求方法(request method),URI和請求頭部。我們會一步一步分析整個過程。

3.1.1. 獲得位置配置結構體

這部分很簡單。所有你需要做的是根據目前的請求結構體和子產品定義,調用

ngx_http_get_module_loc_conf

,獲得目前的配置結構體

。這是我寫的circle gif hanlder函數的相關部分。

static ngx_int_t
           
ngx_http_circle_gif_handler(ngx_http_request_t *r)
           
{
           
    ngx_http_circle_gif_loc_conf_t  *circle_gif_config;
           
    circle_gif_config = ngx_http_get_module_loc_conf(r, ngx_http_circle_gif_module);
           
    ...
           

現在我們就能通路在合并函數中定義的各個變量。

3.1.2. 産生回複

這部分很有趣,也是子產品真正起作用的地方。

請求結構體在這裡也很有用,特别是這些成員變量:

typedef struct {
           
...
           
/* the memory pool, used in the ngx_palloc functions */
           
    ngx_pool_t                       *pool; 
           
    ngx_str_t                         uri;
           
    ngx_str_t                         args;
           
    ngx_http_headers_in_t             headers_in;
           
...
           
} ngx_http_request_t;
           

uri

是請求的路徑,比如:"/query.cgi"。

args

是在問号之後請求的參數(比如 "name=john")。

headers_in

有很多有用的東西,如cookie和浏覽器資訊,但很多子產品不需要。如果你感興趣,請看這裡http/ngx_http_request.h 。

這裡應該有足夠的資訊來産生有用的輸出。

ngx_http_request_t

結構體的完整定義在http/ngx_http_request.h。

3.1.3. 發送HTTP頭部

回複頭部存在于被稱為

headers_out

結構體中。它包含在請求結構體中。這個處理函數生成頭部變量,然後調用

ngx_http_send_header(r)函數

,下面列出些有用的部分:

typedef stuct {
           
...
           
    ngx_uint_t                        status;
           
    size_t                            content_type_len;
           
    ngx_str_t                         content_type;
           
    ngx_table_elt_t                  *content_encoding;
           
    off_t                             content_length_n;
           
    time_t                            date_time;
           
    time_t                            last_modified_time;
           
..
           
} ngx_http_headers_out_t;
           

(另外一些你可以在這裡找到: http/ngx_http_request.h.)

舉個例子,如果一個子產品把Content-Type需要設定為“image/gif”,Content-Length為100,然後傳回200 OK的回複,代碼将是這樣的:

    r->headers_out.status = NGX_HTTP_OK;
           
    r->headers_out.content_length_n = 100;
           
    r->headers_out.content_type.len = sizeof("image/gif") - 1;
           
    r->headers_out.content_type.data = (u_char *) "image/gif";
           
    ngx_http_send_header(r);
           

上面的設定方式針對大多數參數都是有效的。但一些頭部的變量設定要比上面的例子要麻煩;比如,

content_encoding

含有類型

(ngx_table_elt_t*)

,這時子產品必須為它配置設定記憶體。可以用一個叫

ngx_list_push

函數來做。它需要傳入一個

ngx_list_t

變量(與數組類似),然後傳回一個list中的新成員(類型是

ngx_table_elt_t

)。下面的代碼把Content-Encoding設定為“deflate”,然後把頭部發出。

    r->headers_out.content_encoding = ngx_list_push(&r->headers_out.headers);
           
    if (r->headers_out.content_encoding == NULL) {
           
        return NGX_ERROR;
           
    }
           
    r->headers_out.content_encoding->hash = 1;
           
    r->headers_out.content_encoding->key.len = sizeof("Content-Encoding") - 1;
           
    r->headers_out.content_encoding->key.data = (u_char *) "Content-Encoding";
           
    r->headers_out.content_encoding->value.len = sizeof("deflate") - 1;
           
    r->headers_out.content_encoding->value.data = (u_char *) "deflate";
           
ngx_http_send_header(r);
           

當頭部有多個值的時候,這個機制會經常用到;它(理論上講)使得過濾子產品添加、删除某個值而保留其他值的時候更加容易,在操縱字元串的時候,不需要把字元串重新排序。

3.1.4. 發送HTTP負載

現在子產品已經産生了一個回複,把它放到記憶體中。需要為回複配置設定一塊特别的buffer,并把這個buffer連接配接到一個連結清單,然後調用“send body”函數發送。

這些連結清單有什麼用?在Nginx中,處理子產品和過濾子產品在處理完成後産生的回複都包含在緩沖中,每次産生一個buffer;每個連結清單成員儲存指向下一個成員的指針,如果是最後的buffer,就置為NULL。這裡我們簡單地假定隻有一個buffer成員。

首先,子產品聲明一個buffer和一條連結清單:

    ngx_buf_t    *b;
           
    ngx_chain_t   out;
           

第二步是配置設定緩沖,然後指向我們的回複資料:

    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
           
    if (b == NULL) {
           
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 
           
            "Failed to allocate response buffer.");
           
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
           
    }
           
    b->pos = some_bytes; /* first position in memory of the data */
           
    b->last = some_bytes + some_bytes_length; /* last position */
           
    b->memory = 1; /* content is in read-only memory */
           
    /* (i.e., filters should copy it rather than rewrite in place) */
           
    b->last_buf = 1; /* there will be no more buffers in the request */
           

現在子產品buffer添加到了連結清單上:

    out.buf = b;
           
    out.next = NULL;
           

最後,我們把負載發送出去,傳回值是output_filter函數對整個連結清單的傳回狀态。

    return ngx_http_output_filter(r, &out);
           

緩沖連結清單是一個典型的Nginx IO模型,是以你必須清楚它們是如何工作的。

問題:為什麼會有變量last_buf,什麼時候我們才能說這條連結清單結束了,并把next設為NULL?

回答:連結清單可能不完全的,比如,有多個buffer的時候,但是不是所有的buffer都在這個請求或者回複中。是以一些buffer是連結清單的結尾,而不是請求的結尾。這意味着…

3.2. 上遊子產品剖析(又稱代理子產品)

我已經幫你了解了如何用處理子產品來産生回複。你可能用一小塊C代碼就可以完成,但有時你想和另外一台伺服器通信(比如,你寫一個子產品來實作另一種協定)。你可能需要自己做所有的網絡程式設計。如果收到部分的回複,需要等待餘下的回複資料,你會怎麼辦?你不會想阻塞整個事件處理循環吧?這樣會毀掉Nginx的良好性能!幸運的是, Nginx可以讓你使用某種機制将回複映射到一台後端伺服器(叫做“upstream”),你的子產品可以與另外的伺服器通信而不需要任何請求。這個小節将會描述子產品是如何與上遊伺服器通信,比如Memcached, FastCGI或其它HTTP伺服器。

3.2.1.代理子產品回調函數的概要

與其他子產品的處理函數不一樣,代理子產品的處理函數僅作少量的實際工作。它也不會調用

ngx_http_output_filter

,僅僅設定一些回調函數。這些函數在代理服務等待接受和發送的時候被調用。總共有6個:

l       

create_request

生成發送到上遊伺服器的請求緩沖(或者一條緩沖鍊)

l       

reinit_request

在後端伺服器被重置的情況下(在

create_request

被第二次調用之前

) 被調用

l       

process_header

處理上遊伺服器回複的第一個bit,時常是儲存一個指向上遊回複負載的指針

l       

abort_request

在用戶端放棄請求的時候被調用

l       

finalize_request

在Nginx完成從上遊伺服器讀入回複以後被調用

l       

input_filter

這是一個負載過濾函數,在産生回複負載時調用(比如把尾部删掉)

這些是怎麼附加上去的?下面是一個例子,簡單版本的代理子產品處理函數:

static ngx_int_t
           
ngx_http_proxy_handler(ngx_http_request_t *r)
           
{
           
    ngx_int_t                   rc;
           
    ngx_http_upstream_t        *u;
           
    ngx_http_proxy_loc_conf_t  *plcf;
           
    plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);
           
/* set up our upstream struct */
           
    u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t));
           
    if (u == NULL) {
           
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
           
    }
           
    u->peer.log = r->connection->log;
           
    u->peer.log_error = NGX_ERROR_ERR;
           
    u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module;
           
    u->conf = &plcf->upstream;
           
/* attach the callback functions */
           
    u->create_request = ngx_http_proxy_create_request;
           
    u->reinit_request = ngx_http_proxy_reinit_request;
           
    u->process_header = ngx_http_proxy_process_status_line;
           
    u->abort_request = ngx_http_proxy_abort_request;
           
    u->finalize_request = ngx_http_proxy_finalize_request;
           
    r->upstream = u;
           
    rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
           
    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
           
        return rc;
           
    }
           
    return NGX_DONE;
           
}
           

這個函數隻是些打掃工作,重要的部分在回調函數,另外對于

ngx_http_read_client_request_body

也要注意。當結束從用戶端讀以後,這個函數用來設定另外一個回調函數。

這些每個回調函數都是怎麼做的?一般來說reinit_request

,

abort_request

, 和

finalize_request

會設定或者重新設定一些内部狀态,可能隻有幾行代碼。真正的工作是在

create_request

process_header

完成

3.2.2. create_request 回調函數

為了簡單起見,我們假定有個上遊伺服器讀入一個字元,然後列印出兩個字元。我們的函數會怎麼寫呢?

create_request

需要為每個字元配置設定緩沖區,然後為緩沖配置設定一條連結清單,最後把連結清單挂到upstream結構體。看起來像這樣:

static ngx_int_t
           
ngx_http_character_server_create_request(ngx_http_request_t *r)
           
{
           
/* make a buffer and chain */
           
    ngx_buf_t *b;
           
    ngx_chain_t *cl;
           
    b = ngx_create_temp_buf(r->pool, sizeof("a") - 1);
           
    if (b == NULL)
           
        return NGX_ERROR;
           
    cl = ngx_alloc_chain_link(r->pool);
           
    if (cl == NULL)
           
        return NGX_ERROR;
           
/* hook the buffer to the chain */
           
    cl->buf = b;
           
/* chain to the upstream */
           
    r->upstream->request_bufs = cl;
           
/* now write to the buffer */
           
    b->pos = "a";
           
    b->last = b->pos + sizeof("a") - 1;
           
    return NGX_OK;
           
}
           

聽起來不錯,是嗎?當然,在現實中,你可能需要在很多方面需要用到URI。

r->uri

是一個

ngx_str_t

類型,GET的參數在

r->args

裡面,不要忘記你還可以通路請求結構體的頭部和cookie。

3.2.3. process_header 回調函數

現在講

process_header

,就像

create_request

把連結清單指針挂到請求結構體上去一樣,

process_header

把回複指針移到用戶端可以接收到的部分。

這裡有一個小例子,讀進兩個字元的回複。讓我們假定第一個字元是“狀态”字元。如果是問号,我們可以傳回404 File Not Found給用戶端,并把餘下的字元抛棄掉。如果是空格,我們可以傳回給用戶端一個200 OK。好吧,這不是最有用的協定,但是用來說明原理還是可以的。那我們怎麼來寫這個

process_header

函數呢?

static ngx_int_t
           
ngx_http_character_server_process_header(ngx_http_request_t *r)
           
{
           
    ngx_http_upstream_t       *u;
           
    u = r->upstream;
           
    /* read the first character */
           
    switch(u->buffer.pos[0]) {
           
        case '?':
           
            r->header_only; /* suppress this buffer from the client */
           
            u->headers_in.status_n = 404;
           
            break;
           
        case ' ':
           
            u->buffer.pos++; /* move the buffer to point to the next character */
           
            u->headers_in.status_n = 200;
           
            break;
           
    }
           
    return NGX_OK;
           
}
           

就是這樣,操作頭部,改變指針,完成。注意

headers_in

是回複的頭部結構體,就像我們以前看到過的一樣(http/ngx_http_request.h),在代理子產品中,操作這些頭部是很流行的。真正的代理子產品會對頭部做很多事情,不僅僅是錯誤處理,這就看你的想法了。

但是,如果我們從上遊伺服器獲得的回複頭部不在一個緩沖區中,那怎麼辦?

3.2.4. 狀态保持

記得我曾經說過,

abort_request

,

reinit_request

, 和

finalize_request

可以用來重設内部狀态麼?那是因為許多代理子產品有内部狀态。這個狀态需要自定義一個

上下文結構體

,以便用來标記到現在為止我們到底從上遊伺服器讀到了什麼。這個跟前面提到的“子產品上下文”是不一樣的。子產品上下文隻是些已經确定了的類型,而這裡的自定義上下文卻可以包含任何你想要的成員或者資料(這是你的結構體)。這個上下文結構體應該在

create_request

函數裡面執行個體化,可能像這樣:

    ngx_http_character_server_ctx_t   *p;   /* my custom context struct */
           
    p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t));
           
    if (p == NULL) {
           
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
           
    }
           
    ngx_http_set_ctx(r, p, ngx_http_character_server_module);
           

最後一行實際上把這個自定義上下文結構體注冊到了這個特定的請求和子產品名稱上去,以便以後拿來用。當你需要這個結構體的時候(也可能在其它任何回調函數中),這樣做:

    ngx_http_proxy_ctx_t  *p;
           
    p = ngx_http_get_module_ctx(r, ngx_http_proxy_module);
           

指針p就可以得到目前的狀态。給它指派,重設它,把它增加,把它減少,所有資料放在那裡,由你操縱。當從上遊伺服器不斷傳回一塊塊的資料的時候,這裡剛好可以用支援狀态機,而不用阻塞基本的事件處理循環。很好,很強大!

我已經把我知道的關于處理子產品的東西都說了。是時候移駕過濾子產品了,講講輸出過濾連結清單的成員。頭部過濾函數可以修改HTTP頭,負載過濾函數可以修改回複内容。

3.3. 剖析頭部過濾函數

頭部過濾函數由下面三個基礎部分組成:

1.        決定是否操縱這個回複

2.        操縱這個回複

3.        調用下一個過濾函數

舉個例子,這裡有一個簡單版本的“沒有修改過的”頭部過濾函數。如果用戶端的If-Modfied-Since頭部與回複的Last-Modified頭部比對,就把狀态設為304 Not Modified。頭部過濾函數隻有一個參數

ngx_http_request_t

,但它可以讓我們通路到用戶端的頭部和不久要發送的回複頭部。

static
           
ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r)
           
{
           
    time_t  if_modified_since;
           
    if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data,
           
                              r->headers_in.if_modified_since->value.len);
           
/* step 1: decide whether to operate */
           
    if (if_modified_since != NGX_ERROR && 
           
        if_modified_since == r->headers_out.last_modified_time) {
           
/* step 2: operate on the header */
           
        r->headers_out.status = NGX_HTTP_NOT_MODIFIED;
           
        r->headers_out.content_type.len = 0;
           
        ngx_http_clear_content_length(r);
           
        ngx_http_clear_accept_ranges(r);
           
    }
           
/* step 3: call the next filter */
           
    return ngx_http_next_header_filter(r);
           
}
           

headers_out

結構體與處理子產品中的一樣(http/ngx_http_request.h),也可以随便改。

3.4. 剖析負載過濾函數

因為有了緩沖連結清單,寫入負載過濾函數的時候可能有點棘手,負載過濾函數每次隻能操作一個緩沖區(連結清單成員)。子產品必須決定是否覆寫輸入緩沖區,或者把緩沖去替換掉并配置設定一個新的緩沖,還是在原來的緩沖區後面插入一個新緩沖,這是個問題。複雜的時候,子產品會接收到幾個不完全的緩沖區,還需要繼續操縱。不幸的是,Nginx不提供高層次的API來操作緩沖區連結清單,是以負載過濾函數可能很難懂(也很難寫)。但是,這裡有些你可以看到在實際中用到的操作。

一個負載filter的函數原型看起來這樣:(源代碼摘自Nginx的"chunked" 過濾子產品)

static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
           

第一個參數是我們的老朋友請求結構體。第二個參數是指向目前連結清單頭部的指針(可能有0、1或者多個緩沖)。

舉個簡單例子。假設我們像在每個請求的最後插入文本“<l!-- Served by Nginx -->”。首先,我們需要确定回複的最後一個緩沖區是否包含在我們的緩沖區連結清單中。就像我說的那樣,現在還沒有一個完美的API,是以我們必須自己做循環:

    ngx_chain_t *chain_link;
           
    int chain_contains_last_buffer = 0;
           
    for ( chain_link = in; chain_link->next != NULL; chain_link = chain_link->next ) {
           
        if (chain_link->buf->last_buf)
           
            chain_contains_last_buffer = 1;
           
    }
           

如果沒有找到最後的緩沖區,就傳回:

    if (!chain_contains_last_buffer)
           
        return ngx_http_next_body_filter(r, in);
           

很好,現在最後一個緩沖區已經存在連結清單中了。我們配置設定一個新緩沖區:

    ngx_buf_t    *b;
           
    b = ngx_calloc_buf(r->pool);
           
    if (b == NULL) {
           
        return NGX_ERROR;
           
    }
           

然後把資料放進去:

    b->pos = (u_char *) "<!-- Served by Nginx -->";
           
    b->last = b->pos + sizeof("<!-- Served by Nginx -->") - 1;
           

把這個緩沖區挂到新的連結清單成員:

    ngx_chain_t   added_link;
           
    added_link.buf = b;
           
    added_link.next = NULL;
           

最後,把這個新的連結清單成員挂到先前連結清單的末尾:

    chain_link->next = added_link;
           

然後根據變化重設last_buf變量:

    chain_link->buf->last_buf = 0;
           
    added_link->buf->last_buf = 1;
           

傳回修改過的連結清單,進入下一個輸出過濾函數:

    return ngx_http_next_body_filter(r, &in);
           

結果函數其實可以比我們做的多得多,比如,mod_perl (

$response->body =~ s/$/<!-- Served by mod_perl -->/

),緩沖區連結清單功能強大,可以讓程式員在遞增得處理資料,以便用戶端可以盡快獲得資料。不管怎樣,按照我的觀點,Nginx實在應該有個幹淨的接口,程式員也可以擺脫連結清單中不一緻的狀态。到現在為止,我們操縱它是有風險的。

3.5. 剖析負載均衡子產品

負載均衡子產品決定哪個後端伺服器可以分到特定的請求;現有的實作有通過輪轉法或者對請求的某些部分進行哈希處理。這一節将會介紹負載均衡子產品的注冊和調用,并以upstream_hash子產品為例子(源代碼)。Upstream_hash子產品通過對在nginx.conf中确定的某個變量進行哈希。

一個負載均衡子產品有以下六個方面:

1.        激活配置指令需要調用一個注冊函數registration function(比如:

hash

2.        注冊函數定義合法的伺服器選項(比如:

weight=

),同時注冊上遊主機初始化函數upstream initialization function

3.        上遊主機初始化函數在配置确認好以後被調用:

a)    解析伺服器名稱,指向特定的IP位址

b)    為每個socket配置設定空間

c)    設定同伴初始化函數peer initialization function的回調入口

4.        同伴初始化函數在每個請求來臨的時候調用一次,設定一個負載均衡函數可以進入和操作的資料結構體。

5.        負載均衡函數load-balancing function決定這個請求的去向;在用戶端請求來時至少被調用一次(如果後端伺服器回複失敗,可能要更多次數)。這裡發生的東西很有趣。

6.        最後,在和特定的後端伺服器結束通信的時候,同伴釋放函數peer release function會更新統計(通信是否成功或失敗)。

有很多内容,我将會一點點得展開。

3.5.1.激活指令

指令聲明,既确定了他們的有效存在位置也給出會調用哪個函數。對于負載均衡的子產品應該有的辨別

NGX_HTTP_UPS_CONF

,以便Nginx知道這個指令隻在

upstream

配置部分

有效。它應該提供一個指向注冊函數的指針。下面是upstream_hash子產品的指令聲明:

    { ngx_string("hash"),
           
      NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
           
      ngx_http_upstream_hash,
           
      0,
           
      0,
           
      NULL },
           

其他沒什麼了。

3.5.2.注冊函數

上面的回調函數其實是個注冊函數,這樣稱呼是因為它把upstream初始化函數注冊到了upstream配置結構體中。另外,注冊函數定義了在特定的upstream塊中server指令一些選項(比如:

weight=

,

fail_timeout=

)。這裡是upstream_hash子產品的注冊函數

ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
           
{
           
    ngx_http_upstream_srv_conf_t  *uscf;
           
    ngx_http_script_compile_t      sc;
           
    ngx_str_t                     *value;
           
    ngx_array_t                   *vars_lengths, *vars_values;
           
    value = cf->args->elts;
           
    /* the following is necessary to evaluate the argument to "hash" as a $variable */
           
    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
           
    vars_lengths = NULL;
           
    vars_values = NULL;
           
    sc.cf = cf;
           
    sc.source = &value[1];
           
    sc.lengths = &vars_lengths;
           
    sc.values = &vars_values;
           
    sc.complete_lengths = 1;
           
    sc.complete_values = 1;
           
    if (ngx_http_script_compile(&sc) != NGX_OK) {
           
        return NGX_CONF_ERROR;
           
    }
           
    /* end of $variable stuff */
           
    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
           
    /* the upstream initialization function */
           
    uscf->peer.init_upstream = ngx_http_upstream_init_hash;
           
    uscf->flags = NGX_HTTP_UPSTREAM_CREATE;
           
    /* OK, more $variable stuff */
           
    uscf->values = vars_values->elts;
           
    uscf->lengths = vars_lengths->elts;
           
    /* set a default value for "hash_method" */
           
    if (uscf->hash_function == NULL) {
           
        uscf->hash_function = ngx_hash_key;
           
    }
           
    return NGX_CONF_OK;
           
}
           

經過上面的函數,我們很直接得取得了

$variable

,配置設定一個回調函數,設定一些辨別。有哪些辨別是可用的呢?

  • NGX_HTTP_UPSTREAM_CREATE

    : 在upstream塊中有有

    server

    指令。我想象不出你會不用它。
  • NGX_HTTP_UPSTREAM_WEIGHT

    : 讓

    server

    指令擷取

    weight=

  • NGX_HTTP_UPSTREAM_MAX_FAILS

    : 允許

    max_fails=

    選項
  • NGX_HTTP_UPSTREAM_FAIL_TIMEOUT

    : 允許

    fail_timeout=

    選項
  • NGX_HTTP_UPSTREAM_DOWN

    : 允許

    down

    選項
  • NGX_HTTP_UPSTREAM_BACKUP

    : 允許

    backup

    選項

每個子產品都可以設定這些配置選項。用哪些取決于你用它來幹嘛。它們不會自動加上去;所有的失敗邏輯由子產品作者來維護。以後可能會有更多。現在為止,我們還沒有完成對回調函數的追蹤。接下來,我們講講upstream初始化函數。

3.5.3.上遊主機初始化函數

上遊主機初始化函數的用處是解析主機名稱,為每個socket配置設定空間,并配置設定(又一個)回調函數。看看upstream_hash是如何做的:

ngx_int_t
           
ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
           
{
           
    ngx_uint_t                       i, j, n;
           
    ngx_http_upstream_server_t      *server;
           
    ngx_http_upstream_hash_peers_t  *peers;
           
    /* set the callback */
           
    us->peer.init = ngx_http_upstream_init_upstream_hash_peer;
           
    if (!us->servers) {
           
        return NGX_ERROR;
           
    }
           
    server = us->servers->elts;
           
    /* figure out how many IP addresses are in this upstream block. */
           
    /* remember a domain name can resolve to multiple IP addresses. */
           
    for (n = 0, i = 0; i < us->servers->nelts; i++) {
           
        n += server[i].naddrs;
           
    }
           
    /* allocate space for sockets, etc */
           
    peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t)
           
            + sizeof(ngx_peer_addr_t) * (n - 1));
           
    if (peers == NULL) {
           
        return NGX_ERROR;
           
    }
           
    peers->number = n;
           
    /* one port/IP address per peer */
           
    for (n = 0, i = 0; i > us->servers->nelts; i++) {
           
        for (j = 0; j < server[i].naddrs; j++, n++) {
           
            peers->peer[n].sockaddr = server[i].addrs[j].sockaddr;
           
            peers->peer[n].socklen = server[i].addrs[j].socklen;
           
            peers->peer[n].name = server[i].addrs[j].name;
           
        }
           
    }
           
    /* save a pointer to our peers for later */
           
    us->peer.data = peers;
           
    return NGX_OK;
           
}
           

這個函數可能多于一個功能。大部分工作看起來很抽象,但事實上不是,那些是我們要用到的。一個簡單的政策就是調用另外一個upstream子產品的初始化函數,讓它做所有的繁重工作,覆寫下面的

us->peer.init

回調函數。比如,看看這裡:http/modules/ngx_http_upstream_ip_hash_module.c。

關鍵點是設定同伴初始化函數的指針,比如這裡的

ngx_http_upstream_init_upstream_hash_peer

3.5.4.同伴初始化函數

每當用戶端請求的時候,同伴初始化函數就被調用一次。它設定一個結構體,子產品會用它選擇一個合适的伺服器來服務這個請求;該結構體在後端伺服器重試的時候是一緻的。是以很容易在這裡追蹤伺服器的連接配接失敗或計算過的哈希值。按照約定,這個結構體稱為

ngx_http_upstream_<module name>_peer_data_t

此外,同伴初始化函數設定兩個回調函數:

  • get

    : 負載均衡函數
  • free

    :同伴釋放函數(通常在連接配接結束的時候更新一些統計資訊)

看起來還不夠,它還初始化了一個名叫

tries

的變量。隻要

tries

是正的,Nginx就會重試這個負載均衡函數。當

tries

是零的時候,Nginx會放棄。這取決于get和free函數的

tries

設定的初始值。

這裡是摘自upstream_hash子產品的同伴初始化函數:

static ngx_int_t
           
ngx_http_upstream_init_hash_peer(ngx_http_request_t *r,
           
    ngx_http_upstream_srv_conf_t *us)
           
{
           
    ngx_http_upstream_hash_peer_data_t     *uhpd;
           
    ngx_str_t val;
           
    /* evaluate the argument to "hash" */
           
    if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) {
           
        return NGX_ERROR;
           
    }
           
    /* data persistent through the request */
           
    uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t)
           
            + sizeof(uintptr_t) 
           
              * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number 
           
                  / (8 * sizeof(uintptr_t)));
           
    if (uhpd == NULL) {
           
        return NGX_ERROR;
           
    }
           
    /* save our struct for later */
           
    r->upstream->peer.data = uhpd;
           
    uhpd->peers = us->peer.data;
           
    /* set the callbacks and initialize "tries" to "hash_again" + 1*/
           
    r->upstream->peer.free = ngx_http_upstream_free_hash_peer;
           
    r->upstream->peer.get = ngx_http_upstream_get_hash_peer;
           
    r->upstream->peer.tries = us->retries + 1;
           
    /* do the hash and save the result */
           
    uhpd->hash = us->hash_function(val.data, val.len);
           
    return NGX_OK;
           
}
           

看起來不錯,現在我們準備選擇一台上遊伺服器了。

3.5.5.負載均衡函數

主要部分開始了,真正的洋芋燒肉哦。在這裡子產品選擇一台上遊伺服器。負載均衡函數的原型是這樣的:

static ngx_int_t 
           
ngx_http_upstream_get_<module_name>_peer(ngx_peer_connection_t *pc, void *data);
           

Data是我們的結構體,包含用戶端的連接配接等有效資訊。Pc包含我們要連接配接的伺服器資訊。負載均衡函數的主要工作就是往

pc->sockaddr

,

pc->socklen

, 和

pc->name

裡面填進資料。如果你熟悉網絡程式設計,那麼對這些變量應該會很熟悉;但它們在這裡不是很重要。我們不必留意他們代表什麼;隻需要知道去哪裡找到合适的值填進去。

這個函數需要找到一列伺服器,選擇一個,把值賦給pc。讓我們看看upstream_hash子產品是怎麼做的。

此前在函數調用

ngx_http_upstream_init_hash

,upstream_hash 子產品已經把伺服器清單存在結構體

ngx_http_upstream_hash_peer_data_t

中。這個結構展現在是有效的:

    ngx_http_upstream_hash_peer_data_t *uhpd = data;
           

現在同伴清單存在

uhpd->peers->peer

,根據配置設定好的哈希值和伺服器數目,從這個數組中選擇一台同伴伺服器。

    ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];
           

終于大功告成:

    pc->sockaddr = peers->sockaddr;
           
    pc->socklen  = peers->socklen;
           
    pc->name     = &peers->name;
           
    return NGX_OK;
           

如果負載均衡函數傳回

NGX_OK

,它表明,繼續嘗試這台伺服器。如果傳回

NGX_BUSY

,意味着所有背景主機均無效,Nginx應該再嘗試一次。

但是,我們如何知道伺服器無效?或者不想再嘗試了呢?

3.5.6. 同伴釋放函數

同伴釋放函數在上遊連接配接發生之後運作;它的目的是記錄失敗數。這裡是它的函數原型:

void 
           
ngx_http_upstream_free_<module name>_peer(ngx_peer_connection_t *pc, void *data, 
           
    ngx_uint_t state);
           

前面兩個參數跟我們在負載均衡函數看到的一樣,第三個參數是

state

變量,辨別連接配接是否成功。它可能是

NGX_PEER_FAILED

NGX_PEER_NEXT

的位或(要麼連接配接失敗,要麼連接配接成功但是應用傳回錯誤)。零表示連接配接成功。

這取決于子產品作者在碰到這些失敗事件的時候會做什麼。如果完全不做,那結果應該存進data,它指向每個請求的資料結構體

如果不想讓Nginx在請求來的時候繼續嘗試負載均衡,關鍵你要在這個函數中把

pc->tries

設為0。是以最簡單的同伴釋放函數看起來是這樣的:

    pc->tries = 0;
           

如果在到達後端伺服器過程中碰到錯誤,用戶端會收到502 Bad Proxy錯誤。

這裡有個更複雜的例子,摘自upstream_hash子產品。如果後端連接配接失敗,它會在位向量中辨別失敗(稱作

tried

,是一個

uintptr_t

數組),然後選擇一台新的後端伺服器,直到找到一台不失敗的。

#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t))
           
#define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t))
           
static void
           
ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data,
           
    ngx_uint_t state)
           
{
           
    ngx_http_upstream_hash_peer_data_t  *uhpd = data;
           
    ngx_uint_t                           current;
           
    if (state & NGX_PEER_FAILED
           
            && --pc->tries)
           
    {
           
        /* the backend that failed */
           
        current = uhpd->hash % uhpd->peers->number;
           
       /* mark it in the bit-vector */
           
        uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current);
           
        do { /* rehash until we're out of retries or we find one that hasn't been tried */
           
            uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t));
           
            current = uhpd->hash % uhpd->peers->number;
           
        } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries);
           
    }
           
}
           

隻要為負載均衡函數

uhpd->hash

找到一個新值就可以了,是以上面的函數是可行的。

許多應用不需要重試或高可用的邏輯,你看上面,這隻需要提供幾行代碼。

4. Advanced Topics

The examples above were fairly simple. This section will give you some tips for performing more complicated functions in your Nginx module. Since these are "advanced topics", the code examples might be less detailed than in previous sections.

4.1. Shared Memory

Guest chapter written by Grzegorz Nosek

Nginx, while being unthreaded, allows worker processes to share memory between them. However, this is quite different from the standard pool allocator as the shared segment has fixed size and cannot be resized without restarting nginx or destroying its contents in another way.

4.1.1. A (fore)word of caution

First of all, caveat hacker. This guide has been written several months after hands-on experience with shared memory in nginx and while I try my best to be accurate (and have spent some time refreshing my memory), in no way is it guaranteed. You've been warned.

Also, 100% of this knowledge comes from reading the source and reverse-engineering the core concepts, so there are probably better ways to do most of the stuff described.

Oh, and this guide is based on 0.6.31, though 0.5.x is 100% compatible AFAIK and 0.7.x also brings no compatibility-breaking changes that I know of.

For real-world usage of shared memory in nginx, see my upstream_fair module.

This probably does not work on Windows at all. Core dumps in the rear mirror are closer than they appear.

4.1.2. Creating and using a shared memory segment

To create a shared memory segment in nginx, you need to:

  • provide a constructor function to initialise the segment
  • call

    ngx_shared_memory_add

These two points contain the main gotchas (that I came across), namely:

1.    Your constructor will be called multiple times and it's up to you to find out whether you're called the first time (and should set something up), or not (and should probably leave everything alone). The prototype for the shared memory constructor looks like:

2.           static ngx_int_t init(ngx_shm_zone_t *shm_zone, void *data);      

The data variable will contain the contents of

oshm_zone->data

, where

oshm_zone

is the "old" shm zone descriptor (more about it later). This variable is the only value that can survive a reload, so you must use it if you don't want to lose the contents of your shared memory.

Your constructor function will probably look roughly similar to the one in upstream_fair, i.e.:

static ngx_int_t
           
init(ngx_shm_zone_t *shm_zone, void *data)
           
{
           
        if (data) { /* we're being reloaded, propagate the data "cookie" */
           
                shm_zone->data = data;
           
                return NGX_OK;
           
        }
           
        /* set up whatever structures you wish to keep in the shm */
           
        /* initialise shm_zone->data so that we know we have
           
        been called; if nothing interesting comes to your mind, try
           
        shm_zone->shm.addr or, if you're desperate, (void*) 1, just set
           
        the value to something non-NULL for future invocations
           
        */
           
        shm_zone->data = something_interesting;
           
        return NGX_OK;
           
}
           

3.    You must be careful when to access the shm segment.

The interface for adding a shared memory segment looks like:

ngx_shm_zone_t *
           
ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size,
           
        void *tag);
           

cf

is the reference to the config file (you'll probably create the segment in response to a config option), name is the name of the segment (as a

ngx_str_t

, i.e. a counted string), size is the size in bytes (which will usually get rounded up to the nearest multiple of the page size, e.g. 4KB on many popular architectures) and tag is a, well, tag for detecting naming conflicts. If you call

ngx_shared_memory_add

multiple times with the same name, tag and size, you'll get only a single segment. If you specify different names, you'll get several distinct segments and if you specify the same name but different size or tag, you'll get an error. A good choice for the tag value could be e.g. the pointer to your module descriptor.

After you call

ngx_shared_memory_add

and receive the new

shm_zone

descriptor, you must set up the constructor in

shm_zone->init

. Wait... after you add the segment? Yes, and that's a major gotcha. This implies that the segment is not created while calling

ngx_shared_memory_add

(because you specify the constructor only later). What really happens looks like this (grossly simplified):

1.    parse the whole config file, noting requested shm segments

2.    afterwards, create/destroy all the segments in one go

The constructors are called here. Note that every time your ctor is called, it is with another value of

shm_zone

. The reason is that the descriptor lives as long as the cycle (generation in Apache terms) while the segment lives as long as the master and all the workers. To let some data survive a reload, you have access to the old descriptor's

->data

field (mentioned above).

3.    (re)start workers which begin handling requests

4.    upon receipt of SIGHUP, goto 1

Also, you really must set the constructor, otherwise nginx will consider your segment unused and won't create it at all.

Now that you know it, it's pretty clear that you cannot rely on having access to the shared memory while parsing the config. You can access the whole segment as

shm_zone->shm.addr

(which will be NULL before the segment gets really created). Any access after the first parsing run (e.g. inside request handlers or on subsequent reloads) should be fine.

4.1.3. Using the slab allocator

Now that you have your new and shiny shm segment, how do you use it? The simplest way is to use another memory tool that nginx has at your disposal, namely the slab allocator. Nginx is nice enough to initialise the slab for you in every new shm segment, so you can either use it, or ignore the slab structures and overwrite them with your own data.

The interface consists of two functions:

  • void *ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size);

  • void ngx_slab_free(ngx_slab_pool_t *pool, void *p);

The first argument is simply

(ngx_slab_pool_t *)shm_zone->shm.addr

and the other one is either the size of the block to allocate, or the pointer to the block to free. (trivia: not once is

ngx_slab_free

called in vanilla nginx code)

4.1.4. Spinlocks, atomic memory access

Remember that shared memory is inherently dangerous because you can have multiple processes accessing it at the same time. The slab allocator has a per-segment lock (

shpool->mutex

) which is used to protect the segment against concurrent modifications.

You can also acquire and release the lock yourself, which is useful if you want to implement some more complicated operations on the segment, like searching or walking a tree. The two snippets below are essentially equivalent:

/*
           
void *new_block;
           
ngx_slab_pool_t *shpool = (ngx_slab_pool_t *)shm_zone->shm.addr;
           
*/
           
new_block = ngx_slab_alloc(shpool, ngx_pagesize);
           
ngx_shmtx_lock(&shpool->mutex);
           
new_block = ngx_slab_alloc_locked(shpool, ngx_pagesize);
           
ngx_shmtx_unlock(&shpool->mutex);
           

In fact, ngx_slab_alloc looks almost exactly like above.

If you perform any operations which depend on no new allocations (or, more to the point, frees), protect them with the slab mutex. However, remember that nginx mutexes are implemented as spinlocks (non-sleeping), so while they are very fast in the uncontended case, they can easily eat 100% CPU when waiting. So don't do any long-running operations while holding the mutex (especially I/O, but you should avoid any system calls at all).

You can also use your own mutexes for more fine-grained locking, via the

ngx_mutex_init()

,

ngx_mutex_lock()

and

ngx_mutex_unlock()

functions.

As an alternative for locks, you can use atomic variables which are guaranteed to be read or written in an uninterruptible way (no worker process may see the value halfway as it's being written by another one).

Atomic variables are defined with the type

ngx_atomic_t

or

ngx_atomic_uint_t

(depending on signedness). They should have at least 32 bits. To simply read or unconditionally set an atomic variable, you don't need any special constructs:

ngx_atomic_t i = an_atomic_var;
           
an_atomic_var = i + 5;
           

Note that anything can happen between the two lines; context switches, execution of code on other other CPUs, etc.

To atomically read and modify a variable, you have two functions (very platform-specific) with their interface declared in

src/os/unix/ngx_atomic.h

:

·        

ngx_atomic_cmp_set(lock, old, new)

Atomically retrieves old value of

*lock

and stores

new

under the same address. Returns 1 if

*lock

was equal to

old

before overwriting.

·        

ngx_atomic_fetch_add(value, add)

Atomically adds

add

to

*value

and returns the old

*value

.

4.1.5. Using rbtrees

OK, you have your data neatly allocated, protected with a suitable lock but you'd also like to organise it somehow. Again, nginx has a very nice structure just for this purpose - a red-black tree.

Highlights (API-wise):

  • requires an insertion callback, which inserts the element in the tree (probably according to some predefined order) and then calls

    ngx_rbt_red(the_newly_added_node)

    to rebalance the tree
  • requires all leaves to be set to a predefined sentinel object (not NULL)

This chapter is about shared memory, not rbtrees so shoo! Go read the source for upstream_fair to see creating and walking an rbtree in action.

4.2. Subrequests

Subrequests are one of the most powerful aspects of Nginx. With subrequests, you can return the results of a different URL than what the client originally requested. Some web frameworks call this an "internal redirect." But Nginx goes further: not only can modules perform multiple subrequests and combine the outputs into a single response, subrequests can perform their own sub-subrequests, and sub-subrequests can initiate sub-sub-subrequests, and... you get the idea. Subrequests can map to files on the hard disk, other handlers, or upstream servers; it doesn't matter from the perspective of Nginx. As far as I know, only filters can issue subrequests.

4.2.1. Internal redirects

If all you want to do is return a different URL than what the client originally requested, you will want to use the

ngx_http_internal_redirect

function. Its prototype is:

ngx_int_t
           
ngx_http_internal_redirect(ngx_http_request_t *r, ngx_str_t *uri, ngx_str_t *args)
           

Where

r

is the request struct, and

uri

and

args

are the new URI. Note that URIs must be locations already defined in nginx.conf; you cannot, for instance, redirect to an arbitrary domain. Handlers should return the return value of

ngx_http_internal_redirect

, i.e. redirecting handlers will typically end like

return ngx_http_internal_redirect(r, &uri, &args);
           

Internal redirects are used in the "index" module (which maps URLs that end in / to index.html) as well as Nginx's X-Accel-Redirect feature.

4.2.2. A single subrequest

Subrequests are most useful for inserting additional content based on data from the original response. For example, the SSI (server-side include) module uses a filter to scan the contents of the returned document, and then replaces "include" directives with the contents of the specified URLs.

We'll start with a simpler example. We'll make a filter that treats the entire contents of a document as a URL to be retrieved, and then appends the new document to the URL itself. Remember that the URL must be a location in nginx.conf.

static ngx_int_t
           
ngx_http_append_uri_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
           
{
           
    int                 rc; 
           
    ngx_str_t           uri;
           
    ngx_http_request_t *sr;
           
    /* First copy the document buffer into the URI string */
           
    uri.len = in->buf->last - in->buf->pos;
           
    uri.data = ngx_palloc(r->pool, uri.len);
           
    if (uri.data == NULL)
           
        return NGX_ERROR;
           
    ngx_memcpy(uri.data, in->-buf->pos, uri.len);
           
    /* Now return the original document (i.e. the URI) to the client */
           
    rc = ngx_http_next_body_filter(r, in);
           
    if (rc == NGX_ERROR)
           
        return rc;
           
    /* Finally issue the subrequest */
           
    return ngx_http_subrequest(r, &uri, NULL /* args */, 
           
        NULL /* callback */, 0 /* flags */);
           
}
           

The prototype of

ngx_http_subrequest

is:

ngx_int_t ngx_http_subrequest(ngx_http_request_t *r,
           
    ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr, 
           
        ngx_http_post_subrequest_t *ps, ngx_uint_t flags)
           

Where:

  • *r

    is the original request
  • *uri

    and

    *args

    refer to the sub-request
  • **psr

    is a reference to a NULL pointer that will point to the new (sub-)request structure
  • *ps

    is a callback for when the subrequest is finished. I've never used this, but see http/ngx_http_request.h for details.
  • flags

    can be a bitwise-OR'ed combination of:
    • NGX_HTTP_ZERO_IN_URI

      : the URI contains a character with ASCII code 0 (also known as '/0'), or contains "%00"
    • NGX_HTTP_SUBREQUEST_IN_MEMORY

      : store the result of the subrequest in a contiguous chunk of memory (usually not necessary)

The results of the subrequest will be inserted where you expect. If you want to modify the results of the subrequest, you can use another filter (or the same one!). You can tell whether a filter is operating on the primary request or a subrequest with this test:

if (r == r->main) { 
           
    /* primary request */
           
} else {
           
    /* subrequest */
           
}
           

The simplest example of a module that issues a single subrequest is the "addition" module.

4.2.3. Sequential subrequests

You might think issuing multiple subrequests is as simple as:

int rc1, rc2, rc3;
           
rc1 = ngx_http_subrequest(r, uri1, ...);
           
rc2 = ngx_http_subrequest(r, uri2, ...);
           
rc3 = ngx_http_subrequest(r, uri3, ...);
           

You'd be wrong! Remember that Nginx is single-threaded. Subrequests might need to access the network, and if so, Nginx needs to return to its other work while it waits for a response. So we need to check the return value of

ngx_http_subrequest

, which can be one of:

  • NGX_OK

    : the subrequest finished without touching the network
  • NGX_DONE

    : the client reset the network connection
  • NGX_ERROR

    : there was a server error of some sort
  • NGX_AGAIN

    : the subrequest requires network activity

If your subrequest returns

NGX_AGAIN

, your filter should also immediately return

NGX_AGAIN

. When that subrequest finishes, and the results have been sent to the client, Nginx is nice enough to call your filter again, from which you can issue the next subrequest (or do some work in between subrequests). It helps, of course, to keep track of your planned subrequests in a context struct. You should also take care to return errors immediately, too.

Let's make a simple example. Suppose our context struct contains an array of URIs, and the index of the next subrequest:

typedef struct {
           
    ngx_array_t  uris;
           
    int          i;
           
} my_ctx_t;
           

Then a filter that simply concatenates the contents of these URIs together might something look like:

static ngx_int_t
           
ngx_http_multiple_uris_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
           
{
           
    my_ctx_t  *ctx;
           
    int rc = NGX_OK;
           
    ngx_http_request_t *sr;
           
    if (r != r->main) { /* subrequest */
           
        return ngx_http_next_body_filter(r, in);
           
    }
           
    ctx = ngx_http_get_module_ctx(r, my_module);
           
    if (ctx == NULL) {
           
        /* populate ctx and ctx->uris here */
           
    }
           
    while (rc == NGX_OK && ctx->i < ctx->uris.nelts) {
           
        rc = ngx_http_subrequest(r, &((ngx_str_t *)ctx->uris.elts)[ctx->i++],
           
            NULL /* args */, &sr, NULL /* cb */, 0 /* flags */);
           
    }
           
    return rc; /* NGX_OK/NGX_ERROR/NGX_DONE/NGX_AGAIN */
           
}
           

Let's think this code through. There might be more going on than you expect.

First, the filter is called on the original response. Based on this response we populate

ctx

and

ctx->uris

. Then we enter the while loop and call

ngx_http_subrequest

for the first time.

If

ngx_http_subrequest

returns NGX_OK then we move onto the next subrequest immediately. If it returns with NGX_AGAIN, we break out of the while loop and return NGX_AGAIN.

Suppose we've returned an NGX_AGAIN. The subrequest is pending some network activity, and Nginx has moved on to other things. But when that subrequest is finished, Nginx will call our filter at least two more times:

1.      once with

r

set to the subrequest, and

in

set to buffers from the subrequest's response

2.      once with

r

set to the original request, and

in

set to NULL

To distinguish these two cases, we must test whether

r == r->main

. In this example we call the next filter if we're filtering the subrequest. But if we're in the main request, we'll just pick up the while loop where we last left off.

in

will be set to NULL because there aren't actually any new buffers to process.

When the last subrequest finishes and all is well, we return NGX_OK.

This example is of course greatly simplified. You'll have to figure out how to populate

ctx->uris

on your own. But the example shows how simple it is to re-enter the subrequesting loop, and break out as soon as we get an error or

NGX_AGAIN

.

4.2.4. Parallel subrequests

It's also possible to issue several subrequests at once without waiting for previous subrequests to finish. This technique is, in fact, too advanced even for Emiller's and Gnosek's Advanced Topics in Nginx Module Development. See the SSI module for an example.

4.3. TODO: Advanced Topics Not Yet Covered Here

Topics not yet covered in this guide:

  • Parallel subrequests
  • Parsing
  • Built-in data structures (red-black trees, arrays, hash tables...)
  • Access control modules

5. Writing and Compiling a New Nginx Module

So by now, you should be prepared to look at an Nginx module and try to understand what's going on (and you'll know where to look for help). Take a look in src/http/modules/ to see the available modules. Pick a module that's similar to what you're trying to accomplish and look through it. Stuff look familiar? It should. Refer between this guide and the module source to get an understanding about what's going on.

But Emiller didn't write a Balls-In Guide to Reading Nginx Modules. Hell no. This is a Balls-Out Guide. We're not reading. We're writing. Creating. Sharing with the world.

First thing, you're going to need a place to work on your module. Make a folder for your module anywhere on your hard drive, but separate from the Nginx source (and make sure you have the latest copy from nginx.net). Your new folder should contain two files to start with:

  • "config"
  • "ngx_http_<your module>_module.c"

The "config" file will be included by

./configure

, and its contents will depend on the type of module.

"config" for filter modules:

ngx_addon_name=ngx_http_<your module>_module
           
HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_<your module>_module"
           
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c"
           

"config" for other modules:

ngx_addon_name=ngx_http_<your module>_module
           
HTTP_MODULES="$HTTP_MODULES ngx_http_<your module>_module"
           
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c"
           

Now for your C file. I recommend copying an existing module that does something similar to what you want, but rename it "ngx_http_<your module>_module.c". Let this be your model as you change the behavior to suit your needs, and refer to this guide as you understand and refashion the different pieces.

When you're ready to compile, just go into the Nginx directory and type

./configure --add-module=path/to/your/new/module/directory
           

and then

make

and

make install

like you normally would. If all goes well, your module will be compiled right in. Nice, huh? No need to muck with the Nginx source, and adding your module to new versions of Nginx is a snap, just use that same

./configure

command. By the way, if your module needs any dynamically linked libraries, you can add this to your "config" file:

CORE_LIBS="$CORE_LIBS -lfoo"
           

Where

foo

is the library you need. If you make a cool or useful module, be sure to send a note to the Nginx mailing list and share your work.

Happy hacking!

Appendix A: Code References

Nginx source tree (cross-referenced)

Nginx module directory (cross-referenced)

Example addon: circle_gif

Example addon: upstream_hash

Example addon: upstream_fair

Appendix B: Changelog

  • July 14, 2008: Added information about subrequests; slight reorganization
  • July 12, 2008: Added Grzegorz Nosek's guide to shared memory
  • July 2, 2008: Corrected "config" file for filter modules; rewrote introduction; added TODO section
  • May 28, 2007: Changed the load-balancing example to the simpler upstream_hash module
  • May 19, 2007: Corrected bug in body filter example
  • May 4, 2007: Added information about load-balancers
  • April 28, 2007: Initial draft

繼續閱讀