前幾天在圖書館看書,恰好看到這本《深入剖析nginx》,花了快一周的時間看完了這本書,寫點筆記心得便於以后復習。
以前對nginx的認識就只是停留在一個反向代理服務器上。百度了一下nginx也很火,僅次於apache和微軟的iis。nginx的主要特點就是占用系統資源少,並發能力強,穩定性好。
第1,2章主要講了下基本的代碼分析的准備工作,介紹了一些便於調試代碼的工具,以及在linux環境下運用gdb對其代碼進行調試,這里不多描述。
第3章主要介紹了Nginx的進程模型。一般情況下,在啟動Nginx后,將會出現多個Nginx進程,各個進程各司其職共同完成對客戶端處理響應的任務。從整體架構上來看,有監控進程(也叫做主進程),也有工作進程以及緩存進程。監控進程大部分時間都處於掛起等待狀態,直到監控進程收到信號為止,通俗點來說也就是有客戶端發送請求。對於nginx的工作進程,此時它充當了客戶端與后端服務器之間的代理服務器,它的重心主要就是客戶端與后端服務器之間的I數據讀寫的I/O交互事件,而不是進程信號。關於緩存進程,它不處理客戶端的請求,就是它不管數據的讀寫操作,它只管超時操作。如果開啟緩存功能,則會同時開啟兩個緩存進程,分別是緩存管理進程和緩存加載進程。緩存管理進程的主要任務就是清理超時緩存文件,限制緩存文件的總大小,此后這個過程來回往復,一直到整個進程退出為止。緩存加載進程一般是在nginx正常啟動后(一般為60秒)將磁盤中上次緩存的對象加載到內存中,這個過程一般是一次性的,當緩存加載進程完成它的任務以后它就自動退出了。
進程間通信,這里介紹了第一種channel方式的通信,其實channel就是一個元素個數為2的一個數組,里面存放着一對socket描述符,對於繼承了父進程的子進程,他們都擁有了這一對socket描述符,而Nginx將channel[0]給父進程使用,channel[1]給子進程使用,利用數組下標的不同來錯開的使用不同的描述符,從而來實現父子進程之間的通信。但是子進程並沒有向父進程發送任何消息,子進程之間也沒有相互進行通信的邏輯,即channel通信目前只作為父進程來給子進程發送消息使用。
進程間通信的第二種方式,也是在linux下最有效的進程通信的方式之一,共享內存。在nginx中,共享內存使用一個結構體來表示,其中封裝的有共享內存的名字(主要用作共享內存的唯一標識,便於讓nginx知道我們想使用的哪一塊共享內存),大小,標簽(主要用於解決不同模塊使用相同名字的共享內存而引發的沖突,使用標簽后,比如模塊A和模塊B以相同的名稱sa去獲得模塊A所創建的共享內存sa,模塊A將獲得它之間創建的共享內存sa的引用,而模塊B則將獲得一個共享內存sa已有他用的錯誤提示)以及分配內存的起始地址。在Nginx配置解析完成后,所有共享內存結構體將連在一起構成一個全局鏈表。Nginx此時通過遍歷鏈表來實現實際的分配內存,管理機制初始化(例如鎖,slab)等。一個完整的共享內存的初始化主要基於slab高效的訪問機制,而關於共享內存的使用,由於是多進程共同使用共享內存,則要考慮到進程間的互斥問題,記得以前在上java課的時候老師講多線程的時候也講過鎖的機制,運用在這里大概就是強制同一時刻只能有一個進程去訪問共享內存。 關於Nginx的slab機制主要是兩點:緩存和對齊,緩存意味着提前申請好內存並對內存進行划分形成內存池,當我們需要一塊內存空間時,Nginx直接從內存池中取出一塊大小合適的內存空間即可,而內存釋放也是把內存又重新返還給內存池,而不是操作系統。對齊意味着內存的申請總是遵循2的冪次方,8,16,32,64,如果只申請33個字節的內存,也將獲得64字節的內存,雖然存在內存的浪費,但有利於提升性能。Nginx的共享內存與slab機制共同使用,對於共享內存(一個以結構體為節點的全局鏈表),在初始化完成之后,就由slab機制來對它進行內部的划分以及管理。信號處理部分,通過對signal信號的處理,使得Nginx支持與用戶進行信息交互(比如在不終止Nginx服務的情況下更新配置)。
第四章主要講的是Nginx里用到的稍微復雜一點的數據結構(內存池,哈希)。內存池的實現用到了指針以及鏈表,鏈表節點也就是相當於是內存池節點。這里要區別一下大小內存的分配問題,對於小內存的分配,新鏈表節點的加入都是在鏈表尾部進行的,通過遍歷鏈表的方式來搜索可以進行分配的內存,對與不可分配的內存池節點,下次再次進行內存分配時可以直接跳過。對於分配大塊內存,Nginx里可以調用系統API接口向系統申請內存,一般對於連續的大塊內存的分配,會掛載在鏈表節點的頭部,這一點區別於小塊內存的分配。Nginx僅提供對大塊內存的釋放,而沒有對小塊內存的釋放。即從內存池中分配出去的內存不會再回收到內存池中來,而只有在銷毀整個內存池時,所有這些內存才會回收到系統中來。Nginx的這種特性取決於它作為webserver的特殊性,即階段和時效,對於其處理的業務邏輯有明確的階段,而對於每一個階段又有明確的時效性,因此Nginx可以針對階段來分配內存,針對時效來銷毀內存池。比如當一個階段(一個request請求處理)開始(或其過程中)就創建內存池,在一段時間后,必定會因為正常處理,異常錯誤或超時等而結束,即不會出現Nginx長時間占據大量無用內存池的情況,所以在其階段過程中回收不用的小塊內存自然是不必要的,等到時間一起回收就好了。但是內存池的使用也會帶來諸如使valgrind無法正確捕獲內存中異常的問題(Valgrind是一款用於內存調試、內存泄漏檢測以及性能分析的軟件開發工具),當遇到一些奇怪的內存問題無法處理時,可以禁用Nginx的內存池,直接采用mallc/free接口來直接使用系統內存,避免內存池的干擾。Hash,以前數據結構課上了解到主要用於元素的查找,而影響查找效率的因素就是沖突。Nginx解決沖突主要有兩種,第一種是采用好的映射函數,這里采用的是兩重計算,先計算hash鍵值key(這是一個字符串)的哈希碼hashcode,然后對hashcode做一個對一個按實際hash內存空間大小取模運算就得到其在這個內存空間的具體位置。第二種是選一種合適的空間大小。hash結構創建之后就不可再修改,只供高效查找。
第五章主要講的是Nginx的配置解析,使用近似於key-value對的形式,這種形式只針對對配置文件靜態格式上的描述。Nginx的配置文件可以認為是一種上下文相關,高度可擴展的,有作用域以及可自定義變量等諸多高級語言特性的腳本語言。Nginx的配置文件是由多個配置項組成的,每一個配置項都有一個項目名和對應的項目值,項目名又稱為指令,而項目值可能是簡單的字符串(以分號結尾),也可能是由簡單字符串和多個配置項組合而成配置塊的復合結構(以大括號}結尾),我們可以將配置項歸納為兩種,簡單配置項以及復雜配置項。
1 error log /var/log/nginx.error_log info;
這一條指令不帶大括號,所以是一個簡單配置項。
1 location ~ \.php${ fastcgi_pass 127.0.0.1:1025; }
這一條指令帶大括號,所以是一個復雜配置項。
Nginx配置文件里面的注釋信息用#作為開頭標記。
第六章模塊綜述主要介紹了四大模塊,Nginx的模塊不像apache或者lighttpd在編譯時生成so動態庫,然后在程序執行時動態加載,而是在生成Nginx時就直接被編譯到二進制可執行文件中,即如果要選用不同的功能模塊,則需要重新對nginx進行配置和編譯。①handlers:協同完成客戶端請求的處理、產生響應數據,比如ngx_http_rewrite_moudle模塊,用於處理客戶端請求的地址重寫,ngx_http_static_moudle模塊,負責處理客戶端的靜態頁面請求,ngx_http_log_moudle模塊,負責記錄請求訪問日志。在客戶端的請求被Nginx接收后,首先做server的查找與定位。 如果直接訪問的是一個目錄,Nginx先是查看當前目錄是否存在index..html/index.htm/index.php等這樣的默認頁面<ngx_http_index_handler()的工作>,如果不存在默認頁面,就返回一個文件列表頁面<ngx_http_autoindex_handler()的工作>,而ngx_http_static_handler()是根據客戶端靜態頁面請求查找對應的頁面文件並組成待相應內容。②filters:對handlers產生的響應數據做各種過濾處理(即增/刪/改),比如模塊ngx_http_not_modified_filter_moudle,對等待響應數據進行過濾檢測,如果通過時間戳判斷出前后兩次請求的響應數據沒有發生任何實質改變,那么可以直接響應“304 Not Modified”狀態標識,讓客戶端使用本地緩存即可,而原本待發送的響應數據將被清除掉。所有的header過濾功能函數和body過濾功能函數會分別組成各自的過濾鏈(鏈上含有針對各種元素的過濾函數)。根據HTTP協議具備的響應頭影響或決定響應體內容的特點,一般先是對響應頭進行過濾,根據對響應頭過濾的返回值再對響應體進行過濾,如果對響應頭的過濾中出錯或某些特定情況下,則響應體過濾可不再進行。③upstream:如果存在后端真實服務器,Nginx可利用upstream模塊充當反向代理(Reverse Proxy)的角色,對客戶端發起的請求只負責進行轉發(當然也包括對后端真實服務器響應數據的回轉),比如ngx_http_proxy_moudle就為標准的upstream模塊。upstream模塊與具體的協議無關,其除了支持HTTP協議外,還支持包括FASTCGI,SCGI,(這兩種都是對標准cgi的優化替代)UWSGI(一種web服務器,它實現了WSGI協議、uwsgi、http等協議),MEMCACHED(Memcached 是一個高性能的分布式內存對象緩存系統,用於動態Web應用以減輕數據庫負載。它通過在內存中緩存數據和對象來減少讀取數據庫的次數,從而提高動態、數據庫驅動網站的速度。Memcached基於一個存儲鍵/值對的hashmap。)等在內的多種協議。upstream模塊的典型應用是反向代理,例如ngx_http_proxy_moudle模塊,客戶端對服務器80端口的請求都被Nginx Proxy Server 轉發到另外兩個真實的Nginx web server 實例上進行處理(web server 和proxy server都只是兩個進程,並且運行在同一服務器)。如果禁用了反向代理的緩存功能,則每次客戶端的請求都被轉發到后端真實服務器進行處理,更便於對每次Nginx的執行流程進行分析④load-banlance:主要為upstream模塊服務,在Nginx充當中間代理角色時,由於后端真實服務器往往多於一個,對於某一次客戶端的請求,有ngx_http_upstream_ip_hash_moudle這樣的load banlance模塊來解決如何選擇對應的后端真實服務器的問題,這里的ip_hash也是一種負載均衡算法。
第10章是請求定位,對於任何一個客戶端的請求,在Nginx內都必須有與之對應的server以及location來匹配,以提供處理該請求的上下文環境,否則Nginx將無法進行正常處理而返回錯誤。在一般的應用中,Nginx內的server和location會有多個,這一章主要解決如何將客戶端的請求正確定位到對應的server和location。即便是同一個server里,Nginx的location一般也會有多個,以便於靈活地處理客戶端的各種請求。location可以簡單理解成就是我們在客戶端向服務端所請求的一個地址,而服務端用location來進行相應的匹配,Nginx一般采用最佳匹配。對於在server內部的不同location,如果沒有上下層的嵌套,則在配置文件解析后,以隊列形式存在。如果有多個location的層次嵌套,則形成一棵樹,樹的節點為隊列。形成這樣的樹的好處在於在整個http配置解析完以后,所有的location都成功收集並已根據各自所屬server形成多棵不同的樹(不同的server的location不會相互干擾,因為對於請求的處理,先定位到具體server,再在這個server之內去查找對應的location)。為一台服務器配置不同的網站也就是開了不同的虛擬主機,Nginx可以通過不同的域名來決定把用戶引到哪一台虛擬主機來進行操作。