1. 程式人生 > >《深入剖析NGINX》學習記錄

《深入剖析NGINX》學習記錄

1.HTTP服務基本特性

  •   處理靜態頁面請求;
  •   處理index首頁請求
  •   對請求目錄進行列表顯示;
  •   支援多程序間的負載均衡;
  •   對開啟檔案描述符進行快取(提高效能);
  •   對反向代理進行快取(加速);
  •   支援gzip、ranges、chunked、XSLT、SSI以及影象縮放;
  •   支援SSL、TLS SNI

2.HTTP服務高階特性

  •     基於名稱的虛擬主機;
  •     基於IP的虛擬主機;
  •     支援Keep-alive和pipelined連線;
  •     靈活和方便的配置;
  •     在更新配置和升級執行程式時提供不間斷服務;
  •     可自定義客戶端訪問的日誌格式;
  •     帶快取的日誌寫操作(提高效能);
  •     支援快速的日誌檔案切換;
  •     支援對3xx-5xx錯誤程式碼進行重定向;
  •     URI重寫支援正則表示式;
  •     根據客戶端地址執行不同的功能;
  •     支援基於客戶端IP地址的訪問控制;
  •     支援基於HTTP基本認證機制的訪問控制;
  •     支援HTTP referer驗證;
  •     支援HTTP協議的PUT、DELETE、MKCOL、COPY以及MOVE方法;
  •     支援FLV流和MP4流;
  •     支援限速機制;
  •     支援單客戶端的併發控制;
  •     支援Perl指令碼嵌入;

3.郵件代理服務特性

  •     使用外部HTTP認證伺服器將使用者重定向到IMAP/POP3伺服器;
  •     使用外部HTTP認證伺服器將使用者重定向到內部SMTP伺服器;
  •     支援的認證方式;     

            POP3: USER/PASS、APOP、AUTHLOGIN/PLAIN/CRAM-MD5.

            IMAP:sLOGIN、AUTHLOGIN/PLAIN/CRAM-MD5.

            SMTP: AUTHLOGIN/PLAIN/CRAM-MD5.

  •     支援SSL;
  •     支援STARTTLS和STLS.

4.架構和擴充套件性

  •     一個主程序和多個工作程序配合服務的工作模型;
  •     工作程序以非特權使用者執行(安全性考慮);
  •     支援的事件機制有:kqueue(FreeBSD 4.1+)、epoll(Line 2.6+)、rt signals(Linux 2.2.19+)、/dev/poll(Solaris 7 11/99+)、event ports(Solaris 10)、select和poll;
  •     支援kqueue的眾多特性,包括EV_CLEAR、EV_DISABLE(臨時禁止事件)、NOTE_LOWAT、EV_EOF等;
  •     支援非同步檔案IO(FreeBSD4.3+、Linux2.6.22+);
  •     支援DIRECTIO(FreeBSD 4.4+、Linux 2.4+、Solaris 2.6+、Mac OS X);
  •     支援Accept-filters(FreeBSD 4.1+、NetBSD 5.0+)和TCP_DEFER_ACCEPT(Linux 2.4+);
  •     10000個非活躍HTTP keep-alive連線僅佔用約2.5MB記憶體;

5.已測試過的作業系統和平臺

  •     FreeBSD 3~10/i386、FreeBSD 5~10/amd64;
  •     Linux 2.2~3/i386、Linux 2.6~3/amd64;
  •     Solaris 9/i386、sun4u、Solaris 10/i386、amd64、sun4v;
  •     AIX 7.1/powerpc;
  •     HP-UX 11.31/ia64;
  •     Max OS X/ppc、i386;
  •     Windows XP、Windows Server 2003.

6.由於strace能夠提供Nginx執行過程中的這些內部資訊,所以在出現一些奇怪現象時,比如Nginx啟動失敗、響應的檔案資料和預期不一致、莫名其妙的Segment action Fault段錯誤、存在效能瓶頸(利用-T選項跟蹤各個函式的消耗時間),利用strace也許能夠提供一些相關幫助,最後,要退出strace跟蹤,按Ctrl+C即可。

     pstack的使用非常簡單,後面跟程序ID即可。比如在無客戶端請求的情況下,Nginx阻塞在epoll_wait系統呼叫處,此時利用pstack檢視到的Nginx函式呼叫堆疊關係。

 7.利用addr2line工具可以將這些函式地址轉換回可讀的函式名。

8.整體架構

    正常執行起來後的Nginx會有多個程序,最基本的有master_process(即監控程序,也叫主程序)和worker_process(即工作程序),也可能會有Cache相關程序。這些程序之間會相互通訊,以傳遞一些資訊(主要是監控程序往工作程序傳遞)。除了自身程序之間的相互通訊,Nginx還憑藉強悍的功能模組與外界四通八達,比如通過upstream與後端Web伺服器通訊、依靠fastcgi與後端應用伺服器通訊等。一個較為完整的整體框架結構體如圖所示:

    

 9.分析Nginx多程序模型的入口為主程序的ngx_master_process_cycle()函式,在該函式做完訊號處理設定等之後就會呼叫一個名為ngx_start_worker_processes()的函式用於fork()產生出子程序(子程序數目通過函式呼叫的第二個實參指定),子程序作為一個新的實體開始充當工作程序的角色執行ngx_worker_process_cycle()函式,該函式主體為一個無限for(;;)迴圈,持續不斷地處理客戶端的服務請求,而主程序繼續執行ngx_master_process_cycle()函式,也就是作為監控程序執行主體for(;;)迴圈,這自然也是一個無限迴圈,直到程序終止才退出。服務程序基本都是這種寫法,所以不用詳述。

   

下圖表現的很清晰,監控程序和每個工作程序各有一個無限for(;;)迴圈,以便程序持續的等待和處理自己負責的事務,直到程序退出。

    

10.監控程序

    監控程序的無限for(;;)迴圈內有一個關鍵的sigsuspend()函式呼叫,該函式的呼叫使得監控程序的大部分時間都處於掛起等待狀態,直到監控程序接收到訊號為止。當監控程序接收到訊號時,訊號處理函式ngx_signal_handler()就會被執行。我們知道訊號處理函式一般都要求足夠簡單,所以在該函式內執行的動作主要也就是根據當前訊號值對相應的旗標變數做設定,而實際的處理邏輯必須放在主體程式碼裡來進行,所以該for(;;)迴圈接下來的程式碼就是判斷有哪些旗標變數被設定而需要處理的,比如ngx_reap(有子程序退出?)、ngx_quit或ngx_terminate(進行要退出或終止?值得注意的是,雖然兩個旗標都是表示結束Nginx,不過ngx_quit的結束更優雅,它會讓Nginx監控程序做一些清理工作且等待子程序也完全清理並退出之後才終止,而ngx_terminate更為粗暴,不過它通過使用SIGKILL訊號能保證在一段時間後必定被結束掉)、ngx_reconfigure(重新載入配置)等。當所有訊號都處理完時又掛起在函式sigsuspend()呼叫處繼續等待新的訊號,如此反覆,構成監控程序的主要執行體。

11.工作程序

    工作程序的主要關注點就是與客戶端或後端真實伺服器(此時Nginx作為中即代理)之間的資料可讀/可寫等I/O互動事件,而不是程序訊號,所以工作程序的阻塞點是在像select()、epoll_wait()等這樣的I/O多路複用函式呼叫處,以等待發生資料可讀/可寫事件,當然,也可能被新收到的程序訊號中斷。

12.cache manager process(Cache管理程序)與cache loader process(Cache載入程序)則是與Cache快取機制相關的程序。它們也是由主程序建立,對應的模型框圖如下所示:

    

    Cache程序不處理客戶端請求,也就沒有監控的I/O事件,而其處理的是超時事件,在ngx_process_events_and_timers()函式內執行的事件處理函式只有ngx_event_expire_timers()函式。

13.Cache管理程序的任務就是清理超時快取檔案,限制快取檔案總大小,這個過程反反覆覆,直到Nginx整個程序退出為止。

14.採用socketpair()函式創造一對未命名的UNIX域套接字來進行Linux下具有親緣關係的程序之間的雙向通訊是一個非常不錯的解決方案。Nginx就是這麼做的,先看fork()生成新工作程序的ngx_spawn_process()函式以及相關程式碼。

程式碼片段3.4-1,檔名: ngx_process.h
typedef struct {
    ngx_pid_t pid;
    int status;
    ngx_socket_t channel[2];
    ...
} ngx_process_t;
...
#define NGX_MAX_PROCESSES 1024
程式碼片段3.4-2, 檔名: ngx_process.c
ngx_process_t ngx_processes[NGX_MAX_PROCESSES];

ngx_pid_t 
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn)
{
    ...
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
    ...
    pid = fork();
    ...
}

  15.共享記憶體是Linux下程序之間進行資料通訊的最有效方式之一,而Nginx就為我們提供了統一的操作介面來使用共享記憶體。

       在Nginx裡,一塊完整的共享記憶體以結構體Ngx_shm_zone_t來封裝表示,其中包括的欄位有共享記憶體的名稱(shm_zone[i].shm_name)、大小(shm_zone[i].shm.size)、標籤(shm_zone[i].tag)、分配記憶體的起始地址(shm_zone[i].shm.addr)以及初始回撥函式(shm_zone[i].init)等。

程式碼片段3.5-1,檔名: ngx_cycle.h
typedef struct ngx_shm_zone_s ngx_shm_zone_t;
...
struct ngx_shm_zone_s {
    void *data;
    ngx_shm_t   shm;
    ngx_shm_zone_init_pt init;
    void *tag;
};

16.共享記憶體的真正建立是在配置檔案全部解析完後,所有代表共享記憶體的結構體ngx_shm_zone_t變數以連結串列的形式掛載在全域性變數cf->cycle->shared_memory下,Nginx此時遍歷該連結串列並逐個進行實際建立,即分配記憶體、管理機制(比如鎖、slab)初始化等。

17.Nginx互斥鎖介面函式

函式 含義
ngx_shmtx_create() 建立
ngx_shmtx_destory() 銷燬
ngx_shmtx_trylock() 嘗試加鎖(加鎖失敗則直接返回,不等待)
ngx_shmtx_lock() 加鎖(持續等待,知道加鎖成功)
ngx_shmtx_unlock() 解鎖
ngx_shmtx_force_unlock() 強制解鎖(可對其它程序進行解鎖)
ngx_shmtx_wakeup() 喚醒等待加鎖程序(系統支援訊號量的情況下才可使用)

18.Nginx的slab機制與Linux的slab機制在基本原理上並沒有什麼特別大的不同(當然,相比而言,Linux的slab機制要複雜得多),簡單來說也就是基於兩點:快取與對齊。快取意味著預分配,即提前申請好記憶體並對記憶體做好劃分形成記憶體池,當我們需要使用一塊記憶體空間時,Nginx就直接從已經申請並劃分好的記憶體池裡取出一塊合適大小的記憶體即可,而記憶體的釋放也是把記憶體返還給Nginx的記憶體池,而不是作業系統;對齊則意味著記憶體的申請與分配總是按2的冪次方進行,即記憶體大小總是為8、16、32、64等,比如,雖然只申請33個位元組的記憶體,但也將獲得實際64位元組可用大小的記憶體,這的確存在一些記憶體浪費,但對於記憶體效能的提升是顯著的,更重要的是把內部碎片也掌握在可控的範圍內。

    Nginx的slab機制主要是和共享記憶體一起使用,前面提到對於共享記憶體,Nginx在解析完配置檔案,把即將使用的共享記憶體全部以list連結串列的形式組織在全域性變數cf->cycle->shared_memory下之後,就會統一進行實際的記憶體分配,而Nginx的slab機制要做的就是對這些共享記憶體進行進一步的內部劃分與管理。

19.函式ngx_init_zone_pool()是在共享記憶體分配號後進行的初始化呼叫,而該函式內又呼叫了本節結束騷的重點物件slab的初始化物件ngx_slab_init();此時的情況如圖:

   

20.常變數的值與描述

變數名 描述
ngx_pagesize 4096

系統記憶體頁大小,Linux下一般情況就是4KB

ngx_pagesize_shift 12 對應ngx_pagesize(4096),即是4096=1<<12;
ngx_slab_max_size 2048 slots分配和pages分配的分割點,大於等於該值則需從pages裡分配
ngx_slab_exact_size 128

正好能用一個uintptr_t型別的點陣圖變量表示的頁劃分;比如在4KB記憶體頁、32位系統環境下,一個uintptr_t型別的點陣圖變數最多可以對應表示32個劃分塊的裝填,所以要恰好完整地表示一個4KB記憶體頁的每一個劃分塊狀態,必須把這個4KB記憶體頁劃分為32塊,即每一塊大小為:

ngx_slab_exact_size = 4096/32=128

ngx_slab_exact_shift 7 對應ngx_slab_exact_size(128),即是128=1<<7;
pool->min_shift 3 固定值為3
pool->min_size 8 固定值為8,最小劃分塊大小,即是1<<pool->min_shift;

  再來看slab機制對page頁的管理,初始結構示意圖如下所示:

  

  21.Nginx對所有發往其自身的訊號進行了統一管理,其封裝了一個對應的ngx_signal_t結構體來描述一個訊號。

程式碼片段3.7.1-1,檔名:ngx_process.c
typedef struct {
    int signo;
    char *signame;
    char *name;
    void (*handler)(int signo);
} ngx_signal_t;

    其中欄位signo也就是對應的訊號值,比如SIGHUP、SIGINT等。

    欄位signame為訊號名,訊號值所對應巨集的字串,比如“SIGHUP”。欄位name和訊號名不一樣,名稱表明該訊號的自定義作用,即Nginx根據自身對該訊號的使用功能而設定的一個字串,比如SIGHUP用於實現"在不終止Nginx服務的情況下更新配置"的功能,所以對應的該欄位為"reload"。欄位handler,處理訊號的回撥函式指標,未直接忽略的訊號,其處理函式全部為函式ngx_signal_handler()。

22.字串巨集操作

巨集定義 說明 舉例
#define Conn(x,y) x##y 子串x和y連線起來形成新的串

int n = Conn(123,456);

結果為: n=123456;

char *str = Conn("abc", "def");

結果為:str = "abcdef";

#define ToChar(x) #@x 給x加上單引號,因此返回是一個const字元,另外,x長度不可超過4

char a = ToChar(a);

結果為:a = 'a';

char a = ToChar(abcd);

結果為:a = 'd';

char a = ToChar(abcde);

結果為:error C2015: too many characters in constant

#define ToString(x) #x 給x加上雙引號,因此返回是一個字串

char *str = ToString(abcde);

結果為:str="abcde";

23.對訊號進行設定並生效是在fork()函式呼叫之前進行的,所以工作程序等都能受此作用。當然,一般情況下,我們不會向工作程序等子程序傳送控制資訊,而主要是向監控程序父程序傳送,父程序收到訊號做相應處理後,再根據情況看是否要把訊號再通知到其他所有子程序。

24.ngx_pool_t結構圖

   

25.

void *ngx_palloc(ngx_pool_t *pool, size_t size)
void *ngx_pnalloc(ngx_pool_t *pool, size_t size)
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
void *ngx_pcalloc(ngx_pool_t *pool, size_t size)
static void *ngx_palloc_block(ngx_pool_t *pool, size_t size)
static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)

26.申請大塊記憶體

27.釋放大塊記憶體

   

28.資源釋放

  

  來看記憶體池的釋放問題,從程式碼中不難看出Nginx僅提供對大塊記憶體的釋放(通過介面ngx_pfree()),而沒有提供對小塊記憶體的釋放,這意味著從記憶體池裡分配出去的記憶體不會再回收到記憶體池裡來,而只有在銷燬整個記憶體池時,所有這些記憶體才會回收到系統記憶體裡,這裡Nginx記憶體池一個很重要的特點,前面介紹的很多記憶體池設置於處理也都是基於這個特點。

   Nginx記憶體池這樣設計的原因在於Web Server應用的特殊性,即階段與時效,對於其處理的業務邏輯分憂明確的階段,而對每一個階段又有明確的時效,因此Nginx可針對階段來分配記憶體池,針對時效來銷燬記憶體池。比如,當一個階段(比如request處理)開始(或其過程中)就建立對應所需的記憶體池,而當這個階段結束時就銷燬其對應的記憶體池,由於這個階段有嚴格的時效性,即在一段時間後,其必定會因正常處理、異常錯誤或超時等而結束,所以不會出現Nginx長時間佔據大量無用記憶體池的情況。

29.Nginx Hash資料結構的建立過程有點複雜,這從其初始函式ngx_hash_init()就佔去200多行可知一二,但這種複雜是源於Nginx對高效率的極致追求。

    

   

  30.基樹(Radix tree),是一種基於二進位制表示鍵值的二叉查詢樹,正是由於其鍵值的這個特點,所以只有在特定的情況下才會使用,典型的應用場景有檔案系統、路由表等。

   

31.配置檔案格式結構圖

   

32.Nginx利用ngx_command_s資料型別對所有的Nginx配置項進行了統一的描述。

程式碼片段5.2-2,檔名:ngx_conf_file.h
struct ngx_command_s {
    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指定與其對應的配置專案的名稱,欄位set指向配置指令處理回撥函式,而欄位offset指定轉換後控制值的存放位置。

33.ngx_conf_parse()函式體程式碼量不算太多,但是它照樣也將配置內容的解析過程分得很清楚,總體來看分成以下三個步驟:

    a.判斷當前解析狀態。

    b.讀取配置標記token。

    c.當讀取了合適數量的標記token後對其進行實際的處理,也就是將配置值轉換為Nginx內對應控制變數的值。

34.在判斷好當前解析狀態之後就開始讀取配置檔案內容,前面已經提到配置檔案都是由一個個token組成的,因此接下來就是迴圈從配置檔案裡讀取token,而ngx_conf_read_token()函式就是用來做這個事情的。

rc = ngx_conf_read_token(cf);

  函式ngx_conf_read_token()對配置檔案進行逐個字元掃描並解析出單個的token。當然,該函式並不會頻繁的去讀取配置檔案,它每次將從檔案內讀取足夠多的內容以填滿一個大小為NGX_CONF_BUFFER(4096)的快取區(除了最後一次,即配置檔案剩餘內容本來就不夠了),這個快取區在函式ngx_conf_parse()內申請並儲存引用到變數cf->conf_file->buffer內,函式ngx_conf_read_token()反覆使用該快取區,該快取區可能有如下一些狀態。

   初始狀態,即函式ngx_conf_parse()內申請快取區後的初始狀態,如下圖所示:

    

   處理過程中的中間狀態,有一部分配置內容已經被解析為一個個token並儲存起來,而有一部分內容正要被組合成token,還有一部分內容等待處理,如下圖所示:

    

    已解析字元和已掃描字元都屬於已處理字元,但它們又是不同的:已解析字元表示這些字元已經被作為token額外儲存起來了,所以這些字元已經完全沒用了;而已掃描字元表示這些字元還未組成一個完整的token,所以它們還不能被丟棄。

    當快取區裡的字元都處理完時,需要繼續從開啟的配置檔案中讀取新的內容到緩衝區,此時的臨界狀態為,如下圖所示:

    

      前面圖示說過,已解析字元已經沒用了,因此我們可以將已掃描但還未組成token的字元移動到快取區的前面,然後從配置檔案內讀取內容填滿快取區剩餘的空間,情況如下圖所示:

     

    如果最後一次讀取配置檔案內容不夠,那麼情況如下圖所示:

    

  35.下表列出了ngx_conf_parse()函式在解析nginx.conf配置檔案時每次呼叫ngx_conf_read_token()函式後的cf->args裡儲存的內容是什麼(這通過gdb除錯Nginx時在ngx_conf_file.c:185處加斷點就很容易看到這些資訊),這會大大幫助對後續內容的理解。

    cf->args裡儲存內容例項

次數 返回值rc cf->args儲存內容
第1次 NGX_OK

(gdb)p(*cf->args)->nelts

$43 = 2

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$44 = {len = 16, data = 0x80ec0c8 "worker_processes"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$45 = {len = 1, data = 0x80ec0da "2"}

第2次 NGX_OK

(gdb)p(*cf->args)->nelts

$46 = 3

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$47 = {len = 9, data = 0x80ec0dd "error_log"}

(gdb)p * (ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$48 = {len = 14, data = 0x80ec0e8 "logs/error.log"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts + 2 *sizeof(ngx_str_t))

$49 = {len = 5, data = 0x80ec0f8 "debug"}

第3次 NGX_CONF_BLOCK_START

(gdb)p(*cf->args)->nelts

$52 = 1

(gdb)p*((ngx_str_t *)((*cf->args)->elts))

$53 = {len = 6, data = 0x80ec11f"events"}

第...次 ... ...
第6次 NGX_CONF_BLOCK_DONE

(gdb)p(*cf->args)->nelts

$58 = 0

第...次 ... ...
第n次 NGX_CONF_BLOCK_START

(gdb)p(*cf->args)->nelts

$74 = 2

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$75 = {len = 8, data = 0x80f7392 "location"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$76 = {len = 1, data = 0x80f739c "/"}

第...次 ... ...
第末次 NGX_CONF_FILE_DONE

(gdb)p(*cf->args)->nelts

$65 = 0

36.Nginx的每一個配置指令都對應一個ngx_command_s資料型別變數,記錄著該配置指令的解析回撥函式、裝換值儲存位置等,而每一個模組又都把自身所相關的所有指令以陣列的形式組織起來,所以函式ngx_conf_handler()首先做的就是查詢當前指令所對應的ngx_command_s變數,這通過迴圈遍歷各個模組的指令陣列即可。由於Nginx所有模組也是以陣列的形式組織起來的,所以在ngx_conf_handler()函式體內我們可以看到有兩個for迴圈的遍歷查詢。

程式碼片段5.3-4,檔名:ngx_conf_file.c
static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
    ...
    for (i=0; ngx_modules[i]; i++) {
    ...
       cmd = ngx_modules[i]->commands;
       ...
       for (/*void */; cmd->name.len; cmd++) {     
}

37.看一個Nginx配置檔案解析的流程圖:

   

38.以http配置項的處理為例,我們知道ngx_http_module雖然是核心模組, 但是其配置儲存空間還沒有實際申請,所以看第384行給conf進行賦值的語句右值是陣列元素的地址,由於ngx_http_module模組對應7號陣列元素,所以conf指標的當前指向如下圖所示:

   

 

83: 程式碼片段5.4-8,檔名: ngx_http.c
84: { ngx_string("http"),
85:    NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
86:    ngx_http_block,
87:    0,
88:    0,
89:    NULL },
90:  ...
118: static char *
119: ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
120: {
121:     ...
125:     ngx_http_conf_ctx_t  *ctx;
126:     ...
132:     ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
133:     ...
137:     *(ngx_http_conf_ctx_t **) conf = ctx;
128:

   程式碼第132行申請了記憶體空間,而第137行通過conf引數間接地把這塊記憶體空間“掛載”在7號陣列元素下。經過ngx_http_block()函式的處理,我們能看到的配置資訊最基本的組織結構如下圖所示:

   

  39.配置繼承示例圖

    

  40.Nginx不會像Apache或Lighttpd那樣在編譯時生成so動態庫,然後在程式執行時再進行動態載入,Nginx模組原始檔會在生成Nginx時就直接被編譯到其二進位制執行檔案中,所以,如果要選用不同的功能模組,必須對Nginx做重新配置和編譯。對於功能模組的選擇,如果要修改預設值,需要在進行configure時主動指定,比如新增http_flv功能模組(預設是沒有這個功能的,各個選項的預設值可以在檔案auto/options內看到).

[[email protected] nginx-1.2.0]# ./configure --with-http_flv_module

  執行後,生成的objs/ngx_modules.c原始檔內就會包含對ngx_http_flv_module模組的引用,要再去掉http_flv功能模組,則需要重新configure,即不帶--with-http_flv_module配置後再編譯生成新的Nginx二進位制程式。

41.根據模組的主要功能性質,大體可以將它們分為四個類別:

    a.handlers:協同完成客戶端請求的處理、產生響應資料,比如ngx_http_rewrite_module模組,用於處理客戶端請求的地址重寫,ngx_http_static_module模組,負責處理客戶端的靜態頁面請求,ngx_http_log_module模組,負責記錄請求訪問日誌。

    b.filters: 對handlers產生的響應資料做各種過濾處理(即增/刪/改),比如模組ngx_http_not_modified_filter_module,對待響應資料進行過濾檢測,如果通過時間戳判斷出前後兩次請求的響應資料沒有發生任何實質改變,那麼可以直接響應"304 Not Modified"狀態標識,讓客戶端使用本地快取即可,而原本待發送的響應資料將被清除掉。

    c.upstream:如果存在後端真實伺服器,Nginx可利用upstream模組充當反向代理(Reverse Proxy)的角色,對客戶端發起的請求只負責進行轉發(當然也包括對後端真實伺服器響應資料的迴轉),比如ngx_http_proxy_module就為標準的upstraem模組。

    d.load-balance: 在Nginx充當中間代理角色時,由於後端真實伺服器往往多於一個,對於某一次客戶單的請求,如何選擇對應的後端真實伺服器來進行處理,有類似於ngx_http_upstream_ip_hash_module這樣的load balance模組來實現不同的負責均衡演算法

42.封裝Nginx模組的結構體為ngx_module_s,定義如下:

程式碼片段6-1, 檔名: ngx_conf_file.h
struct ngx_module_s {
    ngx_uint_t   ctx_index;   //當前模組在同類模組中的序號
    ngx_uint_t   index;       //當前模組在所有模組中的序號
    ...
    ngx_uint_t   version;     //當前模組版本號
    void *ctx;                //指向當前模組特有的資料
    ngx_command_t *commands;    //指向當前模組配置項解析陣列
    ngx_uint_t    type;        //模組型別
    //以下為模組回撥函式,回撥時機可根據函式名看出
    ngx_int_t     (*init_master)(ngx_log_t *log);
    ...
};
程式碼片段6-2,檔名:ngx_core.h
typedef struct ngx_module_s    ngx_module_t;

   結構體ngx_module_s值得關注的幾個欄位分別為ctx、commands和type,其中commands欄位標識當前模組可以解析的配置專案,表示模組型別的type只有5種可能的值,而同一型別模組的ctx指向的資料型別也相同,參見下表:

    type值的不同型別

序號 type值 ctx指向資料型別
1 NGX_CORE_MODULE ngx_core_module_t
2 NGX_EVENT_MODULE ngx_event_module_t
3 NGX_CONF_MODULE NULL
4 NGX_HTTP_MODULE ngx_http_module_t
5 NGX_MAIL_MODULE ngx_mail_module_t

43.Handler模組

    http的請求的整個處理過程一共被分為11個階段,每一個階段對應的處理功能都比較單一,這樣能儘量讓Nginx模組程式碼更為內聚。這11個階段是Nginx處理客戶端請求的核心所在。

    請求處理狀態機的11個階段

序號 階段巨集名 階段簡單描述
0 NGX_HTTP_POST_READ_PHASE 請求頭讀取完成之後的階段
1 NGX_HTTP_SERVER_REWRITE_PHASE Server內請求地址重寫階段
2 NGX_HTTP_FIND_CONFIG_PHASE 配置查詢階段
3 NGX_HTTP_REWRITE_PHASE Location內請求地址重寫階段
4 NGX_HTTP_POST_REWRITE_PHASE 請求地址重寫完成之後的階段
5 NGX_HTTP_PREACCESS_PHASE 訪問許可權檢查準備階段
6 NGX_HTTP_ACCESS_PHASE 訪問許可權檢查階段
7 NGX_HTTP_POST_ACCESS_PHASE 訪問許可權檢查完成之後的階段
8 NGX_HTTP_TRY_FILES_PHASE 配置項try_files處理階段
9 NGX_HTTP_CONTENT_PHASE 內容產生階段
10 NGX_HTTP_LOG_PHASE 日誌模組處理階段

 a.NGX_HTTP_POST_READ_PHASE階段。

當Nginx成功接收到一個客戶端請求後(即函式accept()正確返回對應的套介面描述符,連線建立),
針對該請求所做的第一個實際工作就是讀取客戶端發過來的請求頭內容,如果在這個階段掛上對應的回撥函式,
那麼在Nginx讀取並解析完客戶端請求頭內容後(階段名稱裡的POST有在...之後的含義),就會執行這些回撥函式。

 b.NGX_HTTP_SERVER_REWRITE_PHASE階段,和第3階段NGX_HTTP_REWRITE_PHASE都屬於地址重寫,也都是針對rewrite模組而設定的階段,前者用於server上下文裡的地址重寫,而後者用於location上下文裡的地址重寫。

    NGX_HTTP_SERVER_REWRITE_PHASE階段在NGX_HTTP_POST_READ_PHASE階段之後,所以具體的先後順序如下圖所示:

    

  c.NGX_HTTP_FIND_CONFIG_PHASE階段。

此階段上不能掛載任何回撥函式,因為它們永遠也不會被執行,該階段完成的是Nginx的特定任務,
即進行Location定位。只有把當前請求的對應location找到了,才能從該location上下文中取出
更多精確地使用者配置值,做後續的進一步請求處理。

   d.經過上一階段後,Nginx已經正確定位到當前請求的對應location,於是進入到NGX_HTTP_REWRITE_PHASE階段進行地址重寫,這和第1階段的地址重寫沒什麼特別。唯一的差別在於,定義在location裡的地址重寫規則只對被定位到當前location的請求才生效,用程式語言的說法就是,它們各自的作用域不一樣。

   e.NGX_HTTP_POST_REWRITE_PHASE階段。

該階段是指在進行地址重寫之後,當然,根據前面的列表來看,具體是在location請求地址重寫階段之後。
這個階段不會執行任何回撥函式,它本身也是為了完成Nginx的特定任務,即檢查當前請求是否做了過多的
內部跳轉(比如地址重寫、redirect等),我們不能讓對一個請求的處理在Nginx內部跳轉很多次甚至是死迴圈
(包括在server上下文或是在location上下文所進行的跳轉),畢竟跳轉一次,基本所有流程就得重新走一遍,這是非常消耗效能的。

    f.NGX_HTTP_PREACCESS_PHASE、NGX_HTTP_ACCESS_PHASE、NGX_HTTP_POST_ACCESS_PHASE階段

做訪問許可權檢查的前期、中期、後期工作,其中後期工作是固定的,判斷前面訪問許可權檢查的結果
(狀態碼存放在欄位r->access_code內),
如果當前請求沒有訪問許可權,那麼直接返回狀態403錯誤,所以這個階段也無法去掛載額外的回撥函式。

    g.NGX_HTTP_TRY_FILES_PHASE階段

    針對配置項try_files的特定處理階段

    h.NGX_HTTP_LOG_PHASE階段

       專門針對日誌模組所設定的處理階段。

   在一般條件下,我們的自定義模組回撥函式都掛載在NGX_HTTP_CONTENT_PHASE階段,畢竟大部分的業務需求都是修改http響應資料,Nginx自身的產生響應內容的模組,像ngx_http_statis_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_gzip_static_module、ngx_http_dav_module等也都掛載在這個階段。

44.各個功能模組將其自身的功能函式掛載在cmcf->phases之後,內部的情況如下圖所示:

   

45.在函式ngx_http_init_phase_handlers()裡對所有這些回撥函式進行一次重組,結果如下圖所示:

   

46.對http請求進行分階段處理核心函式ngx_http_core_run_phases

程式碼片段6.1-2,檔名:ngx_http_core_module.c
void
ngx_http_core_run_phases(ngx_http_request_t *r)
{
    ngx_int_t   rc;
    ngx_http_phase_handler_t *ph;
    ngx_http_core_main_conf_t *cmcf;

    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

    ph = cmcf->phase_engine.handlers;

    while(ph[r->phase_handler].checker) {
        
        rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
    
        if (rc == NGX_OK) {
            return;
        }
    }
}

 47.handler函式各種返回值的含義

序號 返回值 含義
1 NGX_OK 當前階段已經被成功處理,必須進入到下一個階段
2 NGX_DECLINED 當前回撥不處理當前情況,進入到下一個回撥處理
3 NGX_AGAIN 當前處理所需資源不足,需要等待所依賴事件發生
4 NGX_DONE 當前處理結束,仍需等待進一步事件發生後做處理
5 NGX_ERROR,NGX_HTTP_... 當前回撥處理髮生錯誤,需要進入到異常處理流程

48.所有的header過濾功能函式和body過濾功能函式會分別組成各自的兩條過濾鏈,如下圖所示:

 

49.Upstream模組與具體的協議無關,其除了支援HTTP以外,還支援包括FASTCGI、SCGI、UWSGI、MEMCACHED等在內的多種協議。Upstream模組的典型應用是反向代理。

50.對於任何一個Upstream模組而言,最核心的實現主要是7個回撥函式,Upstream代理模組自然也不例外:

  Upstream實現並註冊了7個回撥函式如下表所示:

回撥指標 函式功能 Upstream代理模組
create_request 根據Nginx與後端伺服器通訊協議(比如HTTP、Memcache),將客戶端的HTTP請求資訊轉換為對應的傳送到後端伺服器的真實請求。

ngx_http_proxy_create_request()

由於Nginx與後端伺服器通訊協議也為HTTP,所以直接拷貝客戶端的請求頭、請求體(如果有)到變數r->upstream->request_bufs內

process_header 根據Nginx和後端伺服器通訊協議,將後端伺服器返回的頭部資訊裝換為對客戶端響應的HTTP響應頭

ngx_http_proxy_process_status_line()

此時後端伺服器返回的頭部資訊已經儲存在變數r->upstream->buffer內,將這串字串解析為HTTP響應頭儲存到變數r->upstream->headers_in內

input_filter_init 根據前面獲得的後端伺服器返回的頭部資訊,為進一步處理後端伺服器將返回的響應體做初始準備工作

ngx_http_proxy_input_filter_init()

根據已解析的後端伺服器返回的頭部資訊,設定需進一步處理的後端伺服器將返回的響應體的長度,該值儲存在變數r->upstream->length內

input_filter 正式處理後端伺服器返回的響應體

ngx_http_proxy_buffered_copy_filter()

本次收到的響應體資料長度為bytes,資料長度儲存在r->upstream->buffer內,把它加入到r->upstream->out_bufs響應資料連等待發送給客戶端

finalize_request 正常結束與後端伺服器的互動,比如剩餘待取資料長度為0或讀到EOF等,之後就會呼叫該函式。由於Nginx會自動完成與後端伺服器互動的清理工作,所以該函式一般僅做下日誌,標識響應正常結束

ngx_http_proxy_finalize_request()

記錄一條日誌,標識正常結束語後端伺服器的互動,然後函式返回

reinit_request 對互動重新初始化,比如當Nginx發現一臺後端伺服器出錯無法正常完成處理,需要嘗試請求另一臺後端伺服器時就會呼叫該函式

ngx_http_proxy_reinit_request()

設定初始值,設定回撥指標,處理比較簡單

abort_request 異常結束與後端伺服器的互動後就會呼叫該函式。大部分情況下,該函式僅做下日誌,標識響應異常結束

ngx_http_proxy_abort_request()

記錄一條日誌,標識異常結束與後端伺服器的互動,然後函式返回

這5個函式執行的先後次序如下圖所示:

  

 要寫一個Upstream模組,我們只需要實現上面提到的這7個函式即可。當然,可以看到最主要的也就是create_request、process_header和input_filter這三個回撥,它們實現從HTTP協議到Nginx與後端伺服器之間互動協議的來回轉換。

51.Load-balance模組

  Load-balance模組可以稱為輔助模組,與前面介紹的以處理請求/響應資料為目標的三種模組完全不同,它主要為Upstream模組服務,目標明確且單一,即如何從多臺後端伺服器中選擇出一臺合適的伺服器來處理當前請求。

   要實現一個具體的Load-balance模組,需要實現如下4個回撥函式即可,見下表:

  Load-balance模組的4個回撥介面

回撥指標 函式功能 round_robin模組 IP_hash模組
uscf->peer.init_upstream 解析配置檔案過程中被呼叫,根據upstream裡各個server配置項做初始準備工作,另外的核心工作是設定回撥指標us->peer.init。配置檔案解析完後就不再被呼叫

ngx_http_upstream_init_

round_robin()

設定:us->peer.init = ngx_http

_upstream_init_

round_robin_peer;

ngx_http_upstream_init_

ip_hash()

設定:us->peer.init = ngx_http_upstream_init_

ip_hash_peer;

us->peer.init 在每一次Nginx準備轉發客戶端請求到後盾伺服器前都會呼叫該函式,該函式為本次轉發選擇合適的後端伺服器做初始準備工作,另外的核心工作是設定回撥指標r->upstream->peer.get和r->upstream->peer.free等

ngx_http_upstream_init_

round_robin_peer()

設定:r->upstream->peer.get = ngx_http_upstream_get_

round_robin_peer;

r->upstream->peer.free = ngx_http_upstream_free_

round_robin_peer;

ngx_http_upstream_init_

ip_hash_peer()

設定:r->upstream->peer.get = ngx_http_upstream_get_

ip_hash_peer;r->upstream->peer.free為空

r->upstream->peer.get 在每一次Nginx準備轉發客戶端請求到後端伺服器前都會呼叫該函式,該函式實現具體的為本次轉發懸則合適後端伺服器的演算法邏輯,即完成選擇獲取合適後端伺服器的功能

ngx_http_upstream_get_

round_robin_peer()

加權選擇當前全職最高

(即從各方面綜合比較更

有能力處理當前請求)的後端伺服器

ngx_http_upstream_get_

IP_hash_peer()

根據IP雜湊值選擇後端伺服器

r->upstream->peer.free 在每一次Nginx完成與後端伺服器之間的互動後都會呼叫該函式。如果選擇演算法有前後依賴性,比如加權選擇,那麼需要做一些數值更新操作;如果選擇演算法沒有前後依賴性,比如IP雜湊,那麼該函式可為空。

ngx_http_upstream_free_

round_robin_peer()

更新相關數值,比如rrp->current等

 

 52.Nginx是以事件驅動的,也就是說Nginx內部流程的向前推進基本都是靠各種事件的觸發來驅動,否則Nginx將一直阻塞在函式epoll_wait()或sigsuspend()這樣的系統呼叫上。

53.各種I/O事件處理機制

名稱 特點
select 標準的I/O複用模型,幾乎所有的類UNIX系統上都有提供,但效能相對較差。如果在當前系統平臺上找不到更優的I/O事件處理機制,那麼Nginx預設編譯並使用select複用模型,我們也可以通過使用--with-select_module或--without-select_module配置選項來啟用或禁用select複用模型模組的編譯
poll 標準的I/O複用模型,理論上比select複用模型要優。同select複用模型類似,可以通過使用--with-poll_module或--without-poll_module配置選項來啟用或禁用poll複用模型模組的編譯
epoll 系統Linux 2.6_上正式提供的效能更優秀的I/O複用模型
kqueue 在系統FreeBSD 4.1_, OpenBSD2.9_,NetBSD 2.0和MacOS X上特有的效能更優秀的I/O複用模型
eventport 在系統Solaris10上可用的高效能I/O複用模型
/dev/poll 在系統Solaris 7 11/99+,HP/UX 11.22+(eventport),IRIX 6.5.15+和Tru64 UNIX 5.1A+上可用的高效能I/O複用模型
rtsig 實時訊號(real time signals)模型,在Linux 2.2.19+系統上可用。可以通過使用--with-rtsig_module配置選項來啟用rtsig模組的編譯
aio 非同步I/O(Asynchronous Input and Output)模型,通過非同步I/O函式,如aio_read、aio_write、aio_cancel、aio_error、aio_fsync、aio_return等實現

54.在Nginx原始碼裡,I/O多路複用模型被封裝在一個名為ngx_event_actions_t的結構體裡,該結構體包含的欄位主要就是回撥函式,將各個I/O多路複用模型的功能介面進行統一,參見下表:

   I/O多路複用模型統一介面

ngx_event_actions_t介面 說明
init

初始化

add 將某描述符的某個事件(可讀/可寫)新增到多路複用監控裡
del 將某描述符的某個事件(可讀/可寫)從多路複用監控裡刪除
enable 啟用對某個指定事件的監控
disable 禁用對某個指定事件的監控
add_conn 將指定連線關聯的描述符加入到多路複用監控裡
del_conn 將指定連線關聯的描述符從多路複用監控裡刪除
process_changes 監控的事件發生變化,只有kqueue會用到這個介面
process_events 阻塞等待事件發生,對發生的事件進行逐個處理
done 回收資源

55.Nginx內對I/O多路複用模型的整體封裝

   

56.epoll介面作為poll介面的變體在Linux 核心2.5中被引入。相比於select實現的多路複用I/O模型,epoll模型最大的好處在於它不會隨著被監控描述符數目的增長而導致效率急速下降。

    epoll提供了三個系統呼叫介面,分別如下所示:

#include <sys/epoll.h>
int epoll_create(int size);//建立一個epoll的控制代碼(epoll模型專用的檔案描述符),
size用來告訴核心監聽的描述符數目的最大值,請求核心為儲存事件分配空間,
並返回一個描述符(在epoll使用完後,必須呼叫close()關閉這個描述符,否則可能導致系統描述符被耗盡)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//用來向核心註冊、刪除或修改事件
int epoll_wait(int epfd, struct *epoll_event *events, int maxevents, int timeout);//用來等待事件發生。
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, 
                const sigset_t *sigmask);//epoll_pwait()和函式epoll_wait()的差別在於其
可以通過最後一個引數設定阻塞過程中的訊號遮蔽字

57.Nginx關注事件以及對應的回撥處理函式變化過程

序號 關注事件型別 對應的回撥函式
1 ngx_http_init_request()
2 ngx_http_empty_handler()
3 ngx_http_process_request_line()
4 ngx_http_process_request_headers()
5 ngx_http_request_handler()
6 ngx_http_request_handler()
7 ngx_http_empty_handler()
8 ngx_http_keepalive_handler()

58.函式ngx_trylock_accept_mutex()的內部流程

   

59.Nginx在多核平臺上針對負載均衡和優化所做的工作,就是提供有worker_cpu_affinity配置指令,利用該指令可以將各個工作程序固定在指定的CPU核上執行。CPU親和性,簡單點說就是讓某一段程式碼/資料儘量在指定的某一個或幾個CPU核心上長時間執行/計算的機制。

60.事件超時意味著等待的事件沒有在指定的時間內到達,Nginx有必要對這些可能發生超時的事件進行統一管理,並在發生事件超時時做出相應的處理,比如回收資源,返回錯誤等。

  Nginx把事件封裝在一個名為ngx_event_s的結構體內,而該結構體有幾個欄位與Nginx的超時管理聯絡緊密。

程式碼片段7.5-1,檔名: ngx_event.h
struct ngx_event_s {
    ...
    unsigned timedout:1; //用於標識當前事件是否已經超過,0為沒有超時;
    unsigned timer_set:1;//用於標識當前事件是否已經加入到紅黑樹管理,需要對其是否超時做監控
    ...
    ngx_rbtree_node_t timer;//屬於紅黑樹節點型別變數,紅黑樹就是通過該欄位來組織所有的超時事件物件。

61.紅黑樹的初始化函式ngx_event_timer_init()是在ngx_event_process_init()函式內被呼叫,所以每一個工作程序都會在自身的初始化時建立這顆紅黑樹,如下圖所示:

  

62.通過紅黑樹,Nginx對那些需要關注其是否超時的事件物件就有了統一的管理,Nginx可以選擇在合適的時機對事件計時紅黑樹管理的事件進行一次超時檢測,對於超時了的事件物件進行相應的處理。

   

63.父子請求之間的可變變數值

   

64.主程序通過fork()函式建立子程序,也就是工作程序,它們將全部繼承這些已初始化好的監聽套接字。在每個工作程序的事件初始化函式ngx_event_process_init()內,對每一個監聽套接字建立對應的connection連線物件(為什麼不直接用一個event事件物件呢?主要是考慮到可以傳遞更多資訊到函式ngx_event_accept()內,並且這個連線物件雖然沒有對應的客戶端,但可以與accept()建立的連線套介面統一起來,因為連線套介面對應的是connection連線物件,所以可以簡化相關邏輯的程式碼實現而無需做複雜的判斷與區分),並利用該connection的read事件物件(因為在監聽套介面上觸發的肯定是讀事件)。

    可以看到Nginx主程序在建立完工作程序之後並沒有關閉這些監聽套介面,但主程序卻又沒有進行accept()客戶端連線請求,那麼是否會導致一些客戶端請求失敗呢?答案當然是否定的,雖然主程序也擁有那些監聽套介面,並且它也的確能收到客戶端的請求,但是主程序並沒有監控這些監聽套介面上的事件,沒有去讀取客戶端的請求資料。既然主程序沒有去讀監聽套介面上的資料,那麼資料就阻塞在那裡,等待任意一個工作程序捕獲到對應的可讀事件後,進而去處理並響應客戶端請求。至於主程序為什麼保留(不關閉)那些監聽套介面,是因為在後續再建立新工作程序(比如某工作程序異常退出,主程序收到SIGCHLD訊號)時,還要把這些監聽套介面傳承過去。

65.建立連線套介面

    當有客戶端發起連線請求,監控監聽套介面的事件管理機制就會捕獲到可讀事件,工作程序便執行對應的回撥函式ngx_event_accept(),從而開始連線套介面的建立工作。

    函式ngx_event_accept()的整體邏輯都比較簡單,但是有兩個需要注意的地方。首先是每次呼叫accept()函式接受客戶端請求的次數,預設情況下呼叫accept()函式一次,即工作程序每次捕獲到監聽套介面上的可讀事件後,只接受一個客戶端請求,如果同時收到多個客戶端請求,那麼除第一個以外的請求需等到再一次觸發事件才能被accept()接受。但是如果使用者配置有multi_accept on;,那麼工作程序每次捕獲到監聽套介面上的可讀事件後,將反覆呼叫accept()函式,即一次接受當前所有到達的客戶端連線請求。

程式碼片段9.2-1,檔名:ngx_event_accept.c
void 
ngx_event_accept(ngx_event_t *ev)
{
    ...
    ev->available = ecf->multi_accept;
    ...
    do {
        ...
        s