《深入剖析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內看到).
[root@localhost 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 = accept(lc->fd, (struct sockaddr *)sa, &socklen);...if (s == -1) {...return;...} while(ev->available); }66.如下所示,在讀到NGX_AGAIN時,也就是需要的請求數據沒有全部到達,將事件對象rev加入到超時管理機制和事件監控機制,以等待后續數據可讀事件或超時事件。
代碼片段9.2-4,文件名:ngx_http_request.c static ssize_t ngx_http_read_request_header(ngx_http_request_t *r) {...if (n == NGX_AGAIN) {...ngx_add_timer(rev, cscf->client_header_timeout);...if (ngx_handle_read_event(rev, 0) != NGX_OK) {67.函數ngx_http_init_request(),正式開始對一個客戶端服務請求進行處理與響應工作。該函數的主要功能仍然只是做處理準備:建立http連接對象ngx_http_connection_t、http請求對象ngx_http_request_t、找到對應的server配置default_server、大量的初始化賦值操作,最后執行回調函數ngx_http_process_line(),進入到http請求頭的處理中。
代碼片段9.3-1,文件名:ngx_http_request.c static void ngx_http_init_request(ngx_event_t *rev) {...hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));...r = ngx_pcalloc(c->pool, sizeof(ngx_http_request_t));...addr = port->addrs;addr_conf = &addr[0].conf;.../* the default server configuration for the address:port */cscf = addr_conf->default_server;...rev->handler = ngx_http_process_request_line;...rev->handler(rev); }68.函數ngx_http_process_request_line()處理的數據就是客戶端發送過來得http請求頭中的Request-Line。這個過程可分為三步:讀取Request-Line數據、解析Request-Line、存儲解析結果并設置相關值。
? ? ?a.第一步,讀取Request-Line數據。通過函數ngx_http_read_request_header()將數據讀到緩存區r->header_in內。由于客戶端請求頭部數據可能分多次到達,所以緩存區r->header_in內可能還有一些上一次沒解析完的頭部數據,所以會存在數據的移動等操作。
? ? ?b.第二步,解析Request-Line。對讀取到的Request-Line數據進行解析的工作實現在函數ngx_http_parse_request_line()內。
? ? ?c.第三步,存儲解析結果并設置相關值。在Request-Line的解析過程中會有一些賦值操作,但更多的是在成功解析后,ngx_http_request_t對象r內的相關字段值都將被設置,比如uri(/)、method_name(GET)、http_protocol(HTTP/1.0)等。
? ? ? Request-Line解析成功,即函數ngx_http_parse_request_line()返回NGX_OK,意味著這初步算是一個合法的http客戶端請求。
代碼片段9.3-2,文件名: ngx_http_request.c static void ngx_http_process_request_line(ngx_event_t *rev) {...for (;;) {...n = ngx_http_read_request_header(t);...rc = ngx_http_parse_request_line(r, r->header_in);if (rc == NGX_OK) {...if (ngx_list_init(&r->headers_in.headers, r->pool, 20,sizeof(ngx_table_elt_t))...rev->handler = ngx_http_process_request_headers;ngx_http_rpocess_request_headers(rev);? 函數ngx_http_process_request_headers()內的具體實現如下:
代碼片段9.3-4,文件名:ngx_http_request.c static void ngx_http_process_request_headers(ngx_event_t *rev) {...rc = ngx_http_parse_header_line(r, r->header_in, cscf->underscores_in_headers);if (rc == NGX_OK) {...h = ngx_list_push(&r->headers_in.headers);...hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len);...if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {69.函數ngx_http_header_filter()完成響應頭字符串數據的組織工作。該函數申請一個buf緩存塊,然后根據最初設置以及經過過濾鏈的修改后的相關響應頭字段值,組織響應頭數據以字符串的形式存儲在該緩存塊內。? ??
代碼片段9.4-1,文件名:ngx_http_header_filter_module.c static ngx_int_t ngx_http_header_filter(ngx_http_request_t *r) {...ngx_chain_t out;...out.buf = b;out.next = NULL;return ngx_http_write_filter(r, &out); }? ? 該緩存塊被接入到發送鏈變量out(注意這是一個局部變量)內,之后進入到函數ngx_http_write_filter()進行"寫入"操作,打上引號是因為此處只有在滿足某些條件的情況下才會執行實際的數據寫出。
代碼片段9.4-2,文件名:ngx_http_write_filter_module.c ngx_int_t ngx_http_write_fitler(ngx_http_request_t *r, ngx_chain_t *in) {.../*avoid the output if there are no last buf, no flush point,*there are the incoming bufs and the size of all bufs* is smaller than "postpone_output" directive*/if (!last && !flush && in && size < (off_t) clcf->postponse_output) {return NGX_OK;}70.只有一塊待發送緩存塊的r->out鏈結構
? ??
? 函數依次返回后到函數ngx_http_static_handler()內繼續執行,看一下相關的完整代碼:
代碼片段9.4-3,文件名:ngx_http_static_module.c static ngx_int_t ngx_http_static_handler(ngx_http_request_t *r) {...b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));...b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));...rc = ngx_http_send_header(r);...b->last_buf = (r == r->main) ? 1: 0;...b->file->fd = of.fd;b->file->name = path;...return ngx_http_output_filter(r, &out); }? ? 有兩塊待發送緩存塊的r->out鏈結構
? ??
71.在進行實際的數據寫出操作時,關注我們的重點函數:
代碼片段9.4-4,文件名:ngx_http_write_filter_module.c chain = c->send_chain(c, r->out, limit);? 回調函數send_chain根據系統環境的不同而指向不同的函數,相關代碼如下:
代碼片段9.4-5, 文件名:ngx_linux_sendfile_chain.c ngx_chain_t * ngx_linux_sendfile_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit) {...for (;;) {...for (cl = in; cl && send < limit; cl = cl->next) {if (file) {...rc = sendfile(c->fd, file->file->fd, &offset, file_size);...} else {rc = writev(c->fd, header.elts, header.nelts);? ? 客戶端需要的數據都發送出去了,那么剩下的工作也就是進行連接關閉和一些連接相關資源的清理。
72.在進一步描述http連接關閉流程之前,有必要先介紹一下Nginx的子請求(sub request)概念,因為它的出現導致了http連接關閉流程的復雜化。所謂子請求,并不是由客戶端直接發起的,它是由于Nginx在處理客戶端的請求時,根據自身邏輯而內建的心情求,如下圖所示:
? ?
? 子請求幾乎具有主請求的所有特征(比如有對應完整的ngx_http_request_t結構體對象),并且子請求本身也可以發起新的子請求,即這是一個可以嵌套的概念。在默認情況下,在讀一個客戶端請求(即主請求)的處理過程中,可以發起的總子請求數目(即包括子請求、孫子請求等)大約為200個(由宏NGX_HTTP_MAX_SUBREQUESTS限定),這在一般情況下,已經足夠了。
?73.根據發起子請求的特征,即子請求可以遞歸發起子請求(樹結構)以及同一個子請求可以發起多個子請求(鏈表結構),按照樹加鏈表的形式對它們進行組織是自然而然的事情。而在結構體ngx_http_request_t內提供了兩個與此相對應的字段。
代碼片段9.5-2,文件名:ngx_http_request.h typedef struct ngx_http_postponed_request_s ngx_http_postponed_request_t;struct ngx_http_postponed_request_s {ngx_http_request_t *request;ngx_chain_t *out;ngx_http_postponed_request_t *next; }; ... struct ngx_http_request_s {...ngx_http_request_t *parent;ngx_http_postponed_request_t *postponed;74.在開始新的子請求、內部跳轉、命名location跳轉、開始upstream請求等多種情況下,都可能導致主請求request對象的引用計數count自增1,這意味著在對應的操作完成(比如內部跳轉處理結束)之前不能釋放主請求對象和連接對象。之所以需要做這樣的設計,原因仍然在于Nginx是通過事件觸發來向前推進的,資源相互關聯的各個請求對象在執行過程中誰先誰后不可預知,雖然在其他大部分地方不需要同步而各自自由前進,但在結束點上做資源釋放卻需要同步,否則導致的結果就可想而知了。
? ? 在HTTP 1.0協議里,客戶端通過發送Connection:Keep-Alive的請求頭來實現與服務器之間的keepalive;而在HTTP 1.1協議里,由于標準要求連接默認被保持,所以此時請求頭Connection:Keep-Alive也不再有意義,但通過請求頭Connection:Close可明確要求不進行keepalive連接保持,在Nginx內的具體判斷,首先是獲取Connection請求頭并設置connection_type變量。
代碼片段9.6.1-1,文件名:ngx_http_request.c if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE; } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) {r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE; }75.
代碼片段9.6.1-7,文件名:ngx_http_request.c static void ngx_http_keepalive_handler(ngx_event_t *rev) {...if (rev->timeout || c->close) {ngx_http_close_connection(c);return;}...n = c->recv(c, b->last, size);c->log_error = NGX_ERROR_INFO;if (n == NGX_AGAIN) {if (ngx_handle_read_event(rev, 0) != NGX_OK) {ngx_http_close_connection(c);}return;}...ngx_http_init_request(rev); }?下面SO_LINGER選項的全部相關代碼:
代碼片段9.6.2-2,文件名:ngx_http_request.c static void ngx_http_free_request(ngx_http_request_t *r, ngx_int_t rc) {...if (r->connection->timedout) {clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);if (clcf->reset_timedout_connection) {linger.l_onoff = 1;linger.l_linger = 0;if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER,(const void *)&linger, sizeof(struct linger)) == -1)? 76.函數ngx_http_core_find_location()的處理流程圖
? ??
77.Nginx+Fastcgi+PHP測試環境
? ?
? 客戶端通過HTTP/HTTPS協議對Web服務器端PHP資源的請求被Nginx的Fastcgi模塊接手處理(上圖中圓圈1),該模塊將客戶端請求數據的格式轉換為FASTCGI協議格式后(圖11-1中圓圈2),通過Upstream模塊與PHP引擎之間建立的連接,把它們轉發送到Php引擎(圖11-1中圓圈3).PHP引擎根據轉發請求進行處理后,通過同一條連接把數據再傳回給Nginx(圖11-1中圓圈4),Nginx通過Fastcgi模塊將收到的響應數據轉換回HTTP/HTTPS協議格式后(圖11-1中圓圈5),發回最終的客戶端(圖11-1中圓圈6)。
78.對于客戶端發送的PHP資源請求,Nginx在前期的處理和對HTML資源請求的處理沒什么兩樣,仍然還是創建request請求對象、解析請求頭、定位location并開始轉動請求處理狀態機等,真正出現分叉的地方在狀態機進入NGX_HTTP_CONTENT_PHASE階段后。
? ? Nginx針對HTML頁面請求與PHP頁面請求所做的不同處理
? ??
79.fastcgi模塊提出了5個核心功能函數,分別如下:
代碼片段11.2-2,文件名:ngx_http_fastcgi_module.c static ngx_int_t ngx_http_fastcgi_handler(ngx_http_request_t *r) {...if (ngx_http_upstream_create(r) != NGX_OK) {...u->create_request = ngx_http_fastcgi_create_request;u->reinit_request = ngx_http_fastcgi_reinit_request;u->process_header = ngx_http_fastcgi_process_header;u->abort_request = ngx_http_fastcgi_abort_request;u->finalize_request = ngx_http_fastcgi_finalize_request;...rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);? ? 重要的是回調函數create_request()和process_header(),它們分別表示向后端服務器發起請求和從后端服務器接收數據,這一去一來無非是Nginx與后端服務器交互的核心。
80.Nginx服務客戶端的請求體的函數調用流程
? ??
? ?從客戶端讀取的請求體存放在r->request_body內,受一些用戶設置的影響,請求體數據可能放置在一塊或多塊內存緩存區,或者是某個臨時文件內,但只要請求體大于一定的值(可通過指令client_body_buffer_size設置,默認情況下是2頁內存,一般也就是8KB),則都會主動寫到臨時文件,這實現在函數ngx_http_write_request_body()內,注意它一次可以寫入多個buf塊。
81.Nginx傳遞發送給PHP引擎的請求數據
| 名稱長度 | 值長度 | 名稱 | 值 |
| 12 | 0 | QUERY_STRING | 空 |
| 14 | 3 | REQUEST_METHOD | GET |
| 12 | 0 | CONTENT_TYPE | 空 |
| 14 | 0 | CONTENT_LENGTH | 空 |
| 11 | 6 | SCRIPT_NAME | /t.php |
Nginx要把請求數據發送到PHP引擎,首先得建立起Nginx到PHP引擎之間的通信連接,如果用戶在配置文件里設置的PHP引擎監聽地址是很明確的,即沒有帶上配置變量,(暫稱之為靜態變量),那么此時可直接調用函數ngx_http_upstream_connect()發起連接建立請求。
代碼片段11.3.1-1,文件名:ngx_htp_upstream.c if (u->resolved == NULL) {uscf = u->conf->upstream;...if (uscf->peer.init(r, uscf) != NGX_OK) {...}ngx_http_upstream_connect(r, u); }82.時刻記住Nginx的主要特性:非阻塞、事件驅動、異步。
83.連接建立準備工作
? ?
84.連接建立
? ??
85.接收并處理Fastcgi響應頭
? ? 函數ngx_http_upstream_process_header()用于讀取后端服務器的響應數據,而在我們這里,Nginx讀到的響應數據是FASTCGI協議格式的。
代碼片段11.4.1-1,文件名:ngx_http_upstream.c static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u) {...if (c->read->timeout) {ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);return;}if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);return;}? ? 這處理的是超時情況以及連接已斷開的情況,對于這兩種情況都將調用函數ngx_http_upstream_next()重新選擇后端服務器。
? ? 先來看函數ngx_http_fastcgi_process_header()里的處理邏輯,它循環處理每一條FASTCGI記錄。
代碼片段11.4.1-3,文件名:ngx_http_fastcgi_module.c static ngx_int_t ngx_http_fastcgi_process_header(ngx_http_request_t *r) {...for (;;) {if (f->state < ngx_http_fastcgi_st_data) {rc = ngx_http_fastcgi_process_record(r, f);...if (rc == NGX_AGAIN) {return NGX_AGAIN;}...if (f->state == ngx_http_fastcgi_st_padding) {return NGX_AGAIN;}...if (f->type == NGX_HTTP_FASTCGI_STDERR) {...continue;}...f->fastcgi_stdout = 1;start = u->buffer.pos;...for (;;) {...rc = ngx_http_parse_header_line(r, &u->buffer, 1);...if (rc == NGX_AGAIN) {break;}if (rc == NGX_OK) {...break;}if (rc == NGX_HTTP_PARSE_HEADER_DONE) {...break;}...return NGX_HTTP_UPSTREAM_INVALID_HEADER;}...if (rc == NGX_HTTP_PARSE_HEADER_DONE) {return NGX_OK;}if (rc == NGX_OK) {continue;}/* rc == NGX_AGAIN */...return NGX_AGAIN;} }?86.每一條Fastcgi記錄在NGINX內對應的結構體為ngx_http_fastcgi_header_t,具體來看其數據分別表示:如下表所示:
響應數據中Fastcgi記錄格式
| 字段名 | 字段值 | 說明 |
第一條記錄:總長度為57+7+8=72字節
| Version | 1 | 取固定值1 |
| Type | 6 | NGX_HTTP_FASTCGI_STDOUT |
| Request Id | 1 | 占兩個字節,對應請求序號1 |
| Content Length | 57 | 占兩個字節,內容長度 |
| Padding Length? | 7 | 即在該記錄末尾處填充了7個0 |
| Reserved | 0 | 取值0 |
第二條記錄:從地址0x930b320開始的8字節,這一條標記結束的記錄。
| Version | 1 | 取固定值·1 |
| Type | 3 | NGX_HTTP_FASTCGI_END_REQUEST |
| Request Id? | 1 | 占兩個字節,對應請求序號1 |
| Content Length | 8 | 占兩個字節,內容長度 |
| Padding Length | 0 | 無填充 |
| Reserved? | 0 | 取值0 |
? Fastcgi響應頭的最后一點處理代碼在函數ngx_http_upstream_process_header()內。
代碼片段11.4.1-4,文件名:ngx_http_upstream.c static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u) {../* rc == NGX_OK */if (u->headers_in.status_n > NGX_HTTP_SPECIAL_RESPONSE) {...}if (ngx_http_upstream_process_headers(r, u) != NGX_OK) {return;}if (!r->subrequest_in_memory) {ngx_http_upstream_send_response(r, u);return;}87.在函數ngx_http_upstream_send_response()內做了很多準備工作,其中最重要的就是給變量u->pipe做初始化處理。先看該變量所對應結構體ngx_event_pipe_t的具體定義。
代碼片段11.4.2-2,文件名:ngx_event_pipe.h struct ngx_event_pipe_s {ngx_connection_t *upstream; //表示Nginx與后端服務器之間的連接對象ngx_connection_t *downstream; //表示Nginx與客戶端之間的連接對象ngx_chain_t *free_raw_bufs;ngx_chain_t *in;ngx_chain_t **last_in;ngx_chain_t *out;ngx_chain_t *free;ngx_chain_t *busy; ...ngx_event_pipe_input_filter_pt input_filter;//Nginx接收到后端服務器的響應數據后所需執行的過濾回調,在這里也就是函數ngx_http_fastcgi_input_filter(),用于對Fastcgi記錄做解析void *input_ctx;ngx_event_pipe_output_filter_pt output_filter; //Nginx發送數據到客戶端的過濾函數,這里直接指向ngx_http_output_filter(),即走普通的HTTP響應體過濾鏈。void *output_ctx;89.ngx_event_pipe_write_to_downstream()函數:
代碼片段11.4.2-13,文件名:ngx_event_pipe.c static ngx_int_t ngx_event_pipe_write_to_downstream(ngx_event_pipe_t *p) {...for (;;) {...if (p->upstream_eof || p->upstream_error || p->upstream_done) {..if (p->out) {...rc = p->output_filter(p->output_ctx, p->out);...p->out = NULL;}if (p->in) {...rc = p->output_fitler(p->output_Ctx, p->in);...p->in = NULL;}...p->downstream_done = 1;break;}...}return NGX_OK; }90.Nginx的負載均衡
? ??
91.加權輪詢的流程圖
? ??
92.IP哈希的流程圖
? ??
93.兩種策略對比
? ? 顯而易見,加權輪詢策略的實用性更強,它不依賴與客戶端的任何信息,而完全依靠后端服務器的情況來進行選擇,優勢就是能把客戶端請求更合理更均勻地分配到各個后端服務器處理,但其劣勢也很明顯,同一個客戶端的多次請求可能會被分配到不同的后端服務器進行處理,所有無法滿足做會話保持的應用的需求。
? ? 與此同時,IP哈希策略能較好地把同一個客戶端的多次請求分配到同一臺后端服務器處理,所以避免了加權輪詢策略無法適用會話保持的需求,但是,因為IP哈希策略是根據客戶端的IP地址來對后端服務器做選擇,所以如果某個時刻,來自某個IP地址的請求特別多(比如大量用戶通過同一個NAT代理發起請求),那么將導致某臺后端服務器的壓力可能非常大,而其他后端服務器卻還很空閑的不均衡情況。
?
總結
以上是生活随笔為你收集整理的《深入剖析NGINX》学习记录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: getpwnam学习
- 下一篇: strace用法学习