1 Nginx架構
Nginx 高性能,與其架構有關。
Nginx架構: nginx運行時,在unix系統中以daemon形式在后台運行,后台進程包含一個master進程和多個worker進程。Nginx以多進程形式工作,也支持多線程方式,丹nginx默認采用多進程方式,也是主流方式。
1.1 Nginx多進程模式
多進程模式,會有一個master進程和多個worker進程。
Master進程管理worker進程,包括:
接收來自外界的信號;
向各worker進程發送信號;
監控work進程狀態;
當worker退出后(異常情況下),自動重新啟動新worker進程。
多個worker進程之間對等,競爭來自客戶端的請求,一個請求,只會在一個worker中處理,一個worker進程不會處理其他進程的請求。
Worker進程個數的設置,一般設置與機器cpu核數一致。
進程模式的好處:
每個worker進程相互獨立,無需加鎖,節省鎖開銷;
采用獨立的進程,不會相互影響,一個進程退出,其他進程服務不會中斷;
Worker異常退出,會導致當前worker上的所有請求失敗,不過不會影響所有請求,降低了風險。
多進程模式對並發的支持
每個worker只有一個主線程,采用異步非阻塞方式來處理請求,使得nginx可以同時處理成千上萬個請求。相比Apache,每個請求會獨占一個工作線程,並發上千時,就同時有幾千的線程在處理請求,線程帶來的內存占用很大,線程的上下午切換帶來的cpu開銷也大,性能就上不去了。
異步非阻塞是什么呢?
一個請求的完整過程:請求過來,建立連接,然后接收數據,接收數據后,再發送數據。
具體到系統底層,就是讀寫事件,當讀寫時間沒有准備好時,如果不用非阻塞的方式來調用,就得阻塞調用了,事件沒准備好,就只能等,等事件准備好再繼續。阻塞調用會進入內核等待,讓出cpu,對單線程的worker來說,顯然不合適,當網絡事件越多時,等待很多,cpu利用率上不去。非阻塞就是,事件沒有准備好,馬上返回eagain,表示事件還沒准備好,過會兒再來,過一會,再來檢查一下事件,直到事件准備好為止,在這期間,你可以先去做其他事情,然后再來看看事件好了沒。這時,雖不阻塞了,但是還得不時來檢查事件的狀態,帶來的開銷也不小。所以有了異步非阻塞的事件處理機制,具體到系統調用就是像 select/poll/epoll/kquene這樣的系統調用。提供一種機制,讓你可以同時監控多個事件,調用他們是阻塞的,但是可以設置超時時間,在超時時間之內,如果有事件准備好了就返回。這種機制解決了上面的兩個問題,以epoll為例,當事假沒准備好時,放到epoll里,事件准備好了,就去讀寫,當讀寫返回eagain時,將它再次加入epoll,這樣,只要有事件准備好了,就去處理它,只有當所有事件都沒有准備好時,才在epoll里等着。這樣,就可以支持大量的並發,這里的並發請求,是指未處理完的請求,線程只有一個,同時處理的請求只有一個,只是在請求間不斷切換,切換是因為異步事件未准備好,主動讓出的。這里的切換沒有什么代價,可以理解為在循環處理多個准備好的事件,事實上也是。與多線程相比,這種事件處理方式有很大優勢,不需創建線程,每個請求占用的內存也很少,沒有上下文切換,事件處理非常輕量級,沒有上下文切換的開銷,更多並發,只會占更多的內存而已。現在的網絡服務器基本都采用這種方式,也是nginx性能高效的主要原因。
推薦設置worker數與cpu的核數一致,因為更多的worker,會導致進程競爭cpu資源,從而帶來不必要的上下文切換。
1.2 操作nginx
怎樣操作運行的nignx呢?master進程會接收來自外界發來的信號,因此要控制nginx,通過kill向master進程發送信號就可以了。如 kill –HUP pid,重啟nginx,或重新加載配置,而不中斷服務。Master進程在接到這個信號后,會先重新加載配置文件,然后再啟動新的worker進程,並向所有老的worker進程發信號,不再接收新的請求,並且在處理完所有未處理完的請求后,退出。新的worker啟動后,就開始接收新的請求。
直接給master發信號,是比較老的操作方法,在nginx0.8版本后,可以使用命令行參數,方便管理,如./nginx –s reload ,重啟nginx; ./nginx –s stop,停止nginx。這種方式的內部原理是,執行命令時,會啟動一個新的nginx進程,該進程在解析到reload參數后,知道目標是控制nginx重新加載配置文件,它會向master進程發送信號,接下來的處理,和直接向master進程發送信號一樣。
1.3 Nginx 處理請求
Worker進程是怎么處理請求的呢?
一個連接請求過來,每個進程都有可能處理這個連接。Worker進程是從master進程fork出來的,在master進程里,先建立好需要listen的socket(listenfd)后,然后再fork出多個worker進程。所有worker進程的listenfd會在新連接到來時變得可讀,為了保證只有一個進程處理該連接,所有worker進程在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程注冊listenfd讀事件,在讀事件里調用accept接受該連接。當一個worker進程在accept這個連接之后,開始讀取請求,解析請求,產生數據后,再返回給客戶端,最后才斷開連接,這就是一個完整的請求處理。一個請求,完全由worker處理,且只在一個worker里處理。
2 Nginx基礎概念
2.1 Connection
Nginx中connection是對tcp連接的封裝,包括連接的socket,讀事件,寫事件。
Nginx怎么處理一個連接的呢?nginx在啟動時,會解析配置文件,得到需要監聽的端口與ip,然后在nginx的master進程里,先初始化這個監控的socket,然后再fork出多個子進程,子進程競爭accept新的連接。此時,客戶端就可以像nginx發起連接了,當客戶端與服務器通過三次握手建立好一個連接,nginx的某一個子進程會accept成功,得到這個socket,然后創建nginx對連接的封裝,接着,設置讀寫事件處理函數並添加讀寫事件來與客戶端進行數據的交換。最后,nginx或客戶端主動關掉連接。
Nginx也可以作為客戶端來請求其他server的數據,此時,與其它server創建的連接,也封裝在ngx_connection中。
Nginx中,每個進程會有一個連接數的最大上限,這個上限與系統對fd的限制不一樣。操作系統中,使用ulimit -n,可以得到一個進程所能打開的fd的最大數,即nofile,因為每個socket會占用一個fd,所以這個會限制進程的最大連接數,fd用完后,再創建socket,就會失敗。Nginx通過設置worker_connections來設置每個進程支持的最大連接數,如果該值大於nofile,那么實際的最大連接數是nofile,nginx會有警告。Nginx在實現時,是通過一個連接池來管理的,每個worker進程都有一個獨立的連接池,連接池大小是worker_connections。這里連接池里面保存的其實不是真實的連接,只是一個worker_connections大小的ngx_connection_t結構的數組。Nginx通過一個鏈表free_connections來保存所有的空閑ngx_connection_t.每次獲取一個連接時,就從空閑連接鏈表中獲取一個,用完后,再放回空閑連接鏈表里面。
Worker_connections,表示每個worker所能建立連接的最大值,一個nginx能建立的最大連接數是:worker_connections * worker_processes.因此對於HTTP請求本地資源,最大並發可以是 worker_connections * worker_processes.而如果是HTTP作為反向代理來說,最大並發數是 worker_connections * worker_processes/2.因為作為反向代理服務器,每個並發會建立與客戶端的連接和與后端服務器的連接,占用2個連接。
如何保證worker進程競爭處理連接的公平呢?
如果某個進程得到accept的機會比較多,它的空閑連接會很快用完,如果不提前做一些控制,當accept到一個新的tcp連接后,因為無法得到空閑連接,而且無法將此連接轉交其他進程,最終導致此tcp連接得不到處理。而其他進程有空余連接,卻沒有處理機會。如何解決這個問題呢?
Nginx的處理得先打開accept_mutex,此時只有獲得了accept_mutex的進程才會去添加accept事件,nginx會控制進程是否添加accept事件。Nginx使用一個叫ngx_accept_disabled變量控制是否競爭accept_mutex鎖。這個變量與worker進程的剩余連接數有關,當該變量大於0時,就不去嘗試獲取鎖,等於讓出獲取連接的機會。這樣就可以控制多進程間連接的平衡了。
2.2 Keep alive
http請求是請求應答式的,如果我們知道每個請求頭與相應體的長度,那么我們可以在一個連接上面執行多個請求。即長連接。如果當前請求需要有body,那么nginx就需要客戶端在請求頭中指定content-length來表面body的大小,否則返回400錯誤。那么響應體的長度呢?http協議中關於響應body長度的確定:
1 對於http1.0 協議來說,如果響應頭中有content-length頭,則以content-length的長度就可以知道body的長度,客戶端在接收body時,可以依照這個長度接收數據,接收完后,就表示該請求完成。如果沒有content-length,客戶端會一直接收數據,直到服務端主動端口連接,才表示body接收完
2 對於http1.1 協議,如果響應頭中transfer-encoding為chunked傳輸,表示body是流式輸出,body被分成多個塊,每塊的開始會標示出當前塊的長度,此時,body不需要指定長度。如果是非chunked傳輸,而且有Content-length,則按照content-length來接收數據。否則,非chunked且沒有content-length,則客戶端接收數據,知道服務器主動斷開。
客戶端請求頭中connection為close,表示客戶端要關掉長連接,如果是keep-alive,則客戶端需要打開長連接。客戶端的請求中沒有connection這個頭,根據協議,如果是http1.0,默認是close,如果是http1.1,默認是keep-alive。如果要keep-alive,nginx在輸出完響應體后,會設置當前連接的keepalive屬性,然后等待客戶端下一次請求,nginx設置了keepalive的等待最大時間。一般來說,當客戶端需要多次訪問同一個server時,打開keepalive的優勢非常大。
2.3 Pipe
http1.1中引入Pipeline,就是流水線作業,可以看做是keepalive的升華。Pipeline也是基於長連接的。目前就是利用一個連接做多次請求,如果客戶端要提交多個請求,對於keepalive,第二個請求,必須要等到第一個請求的響應接收完后,才能發起。得到兩個響應的時間至少是2*RTT。而對於pipeline,客戶端不必等到第一個請求處理完,就可以發起第二個請求。得到兩個響應的時間可能能夠達到1*RTT。Nginx是直接支持pipeline的。Nginx對pipeline中的多個請求的處理不是並行的,而是一個接一個的處理,只是在處理第一個請求的時候,客戶端就可以發起第二個請求。這樣,nginx利用pipeline可以減少從處理完一個請求后到等待第二個請求的請求頭數據的時間。
3 Nginx怎么用(安裝與配置)
具體參見http://seanlook.com/2015/05/17/nginx-install-and-config/
或者http://blog.csdn.net/guodongxiaren/article/details/40950249
安裝nginx
yum install nginx-1.6.3
3.1 Nginx.conf 配置
Nginx配置文件主要有4部分,main(全局設置)、server(主機設置)、upstream(上游服務器設置,主要為反向代理,負載均衡相關配置)和location(url匹配特定位置的設置),每部分包含若干指令。
Main部分的設置影響其他所有部分的設置;
Server部分主要用於指定虛擬機主機域名,ip和端口;
Upstream的指令用於設置一系列的后端服務器,設置反向代理及后端服務器的負載均衡;
Location部分用於匹配網頁位置(如,跟目錄“/”,”/images”等)。
它們之間的關系是,server繼承main,location繼承server,upstream既不會繼承指令也不會被繼承。
4.1 為什么高並發重要
和十年前相比,目前的互聯網已經難以想象的廣泛應用和普及。從NCSA用Apache搭的web服務器提供的可點擊的文本HTML,已然進化成超過20億人在線的通信媒介。隨着永久在線的個人電腦,移動終端以及平板電腦的增多,互聯網在快速變化,經濟系統也完全數字有線化。提供實時可用信息和娛樂的在線服務變得更加復雜精巧。在線業務的安全需求也急劇變化。網站比從前更加復雜,需要在工程上做的更具有健壯性和可伸縮性。
並發總是網站架構最大的挑戰之一。由於web服務的興起,並發的數量級在不斷增長。熱門網站為幾十萬甚至幾百萬的同時在線用戶提供服務並不尋常。十年前,並發的主要原因是由於客戶端接入速度慢--用戶使用ADSL或者撥號商務。現在,並發是由移動終端和新應用架構所帶來,這些應用通常基於持久連接來為客戶端提供新聞,微博,通知等服務。另一個重要的因素就是現代瀏覽器行為變了,他們瀏覽網站的時候會同時打開4到6個連接來加快頁面加載速度。
舉例說明一下慢客戶端的問題,假設一個Apache網站產生小於100KB的響應--包含文本或圖片的網頁。生成這個頁面可能需要1秒鍾,但是如果網速只有80kbps(10KB/s),需要花10秒才能把這個頁面發送到客戶端。基本上,web服務器相對快速的推送100KB數據,然后需要等待10秒發送數據之后才能關閉連接。那么現在如果有1000個同時連接的客戶端請求相同的頁面,那么如果為每個客戶端分配1MB內存,就需要1000MB內存來為這1000個客戶端提供這個頁面。實際上,一個典型的基於Apache的web服務器通常為每個連接分配1MB內存,而移動通信的有效速度也通常是幾十kbps。雖然借助於增加操作系統內核socket緩沖區大小,可以優化發送數據給慢客戶端的場景,但是這並不是一個常規的解決方案,並且會帶來無法預料的副作用。
隨着持久連接的使用,並發處理的問題更加明顯。為了避免新建HTTP連接所帶來的延時,客戶端需要保持連接,這樣web服務器就需要為每個連接上的客戶端分配一定數量的內存。
因此,為了處理持續增長的用戶帶來的負載和更高量級的並發,網站需要大量高效的組件。而另一方面,web服務器軟件運行在諸如硬件(CPU,內存,磁盤),網絡帶寬,應用和數據存儲架構等之上,這些基礎設施顯然也很重要。因而,隨着同時在線數和每秒請求數的增長,web服務器性能也應該能夠非線性擴展。
Apache不再適用?
Apache web服務器軟件發源於1990年代,目前在互聯網網站上占有率第一。Apache的架構適合當時的操作系統和硬件,並且也符合當時的互聯網狀況:一個網站通常使用一台物理服務器運行一個Apache實例。2000年之后,顯然這種單服務器模型已經無法簡單擴展來滿足日益增長的web服務需求。雖然Apache為新功能開發提供了堅實的基礎,但他為每個新連接派生一個進程的做法(譯注:Apache從2.4版本起已經支持事件模型),不適合網站的非線性擴展。最終,Apache成為一個通用的web服務器軟件,聚焦於功能多樣化,第三方擴展開發,以及web應用開發的通用性。然而,當硬件成本越來越低,每個連接消耗的CPU和內存越來越多,使用這樣功能繁多的單一軟件不再具有可伸縮性。
因而,當服務器硬件、操作系統和網絡設施不再成為網站增長的主要限制因素時,網站開發者開始尋求更高效的手段來架設web服務器。大約十年前,著名軟件工程師Daniel Kegel提出:“是時候讓web服務器支持同時處理10000客戶端了”,並且預言了現在稱為雲服務的技術。Kegel的C10K設想明顯推動了許多人嘗試解決這個問題--通過優化web服務器軟件來支持大規模客戶端連接的並發處理,nginx是其中做的最成功者之一。
為了解決10000個並發連接的C10K問題,nginx基於一個完全不同的架構—更適合每秒同時連接數和請求數非線性增長。Nginx基於事件模型,而沒有模仿Apache為每個請求派生新進程或線程的做法。最終結果就是即使負載增加了,內存和CPU使用事件始終保持可預期。Nginx使用普通的硬件就能在一個服務器上處理數萬的並發連接。
Nginx的第一個版本發布之后,一般被用來同Apache一同部署,HTML、CSS、JavaScript腳本和圖片等靜態內容由nginx處理,來降低Apache應用服務器的並發和延時。隨着開發演進的過程,nginx增加了FastCGI、uswge和SCGI等協議的支持,以及對分布式內存對象緩存系統如memcached的支持。也增加了其他有用的功能,例如支持負載均衡和緩存的反向代理。這些附加功能使nginx成為一個高效的工具集,用於構建可伸縮的web基礎設施。
2012年2月,Apache 2.4.x版本發布。雖然增加了新的並發處理核心模塊和代理模塊,用於加強可伸縮性和性能,但要說性能、並發能力和資源利用率是否能趕上或超過純事件驅動模型的web服務器還為時尚早。Apache新版本具有了更好的性能值得高興,對於nginx+Apache的web網站架構,雖然這能夠緩解后端潛在的瓶頸,但並不能解決全部問題。
nginx有更多的優點嗎?
部署nginx最關鍵的好處就是能夠高性能高效的處理高並發。同時,還有更多有意思的好處。
最近幾年,web架構擁抱解耦的理念並且將應用層設施從web服務器中分離。雖然現在僅僅是將原先基於LAMP(Linux, Apache, MySQL, PHP, Python or Perl)所構建的網站,變為基於LEMP(E表示Engine x)的。但是,越來越多的實踐是將web服務器推入基礎設施的邊緣,並且用不同的方法整合這些相同或更新的應用和數據庫工具集。
Nginx很適合做這些工作。他提供了必要的關鍵功能用於方便將下列功能從應用層剝離到更高效的邊緣web服務器層:並發、長連接處理、SSL,靜態內容、壓縮和緩存、連接和請求限速,以及HTTP媒體流等。Nginx同時也允許直接整合memcached、Redis或者其他的NoSQL解決方案,增強為處理大規模並發用戶的性能。
隨着現代編程語言和開發包廣泛使用,越來越多的公司改變了應用開發和部署的方式。Nginx已經成為這些改變范例之中的最重要的部件之一,並且已經幫助許多公司在預算內快速啟動和開發他們的web服務。
Nginx開發始於2002年,2004年基於2-clause BSD授權正式對外發布。自發布起,Nginx用戶就在不斷增長,並且貢獻提議,提交bug報告、建議和評測報告,這極大的幫助和促進了整個社區的發展。
Nginx代碼完全用C語言從頭寫成,已經移植到許多體系結構和操作系統,包括:Linux、FreeBSD、Solaris、Mac OS X、AIX以及Microsoft Windows。Nginx有自己的函數庫,並且除了zlib、PCRE和OpenSSL之外,標准模塊只使用系統C庫函數。而且,如果不需要或者考慮到潛在的授權沖突,可以不使用這些第三方庫。
談談關於Windows版本nginx。當nginx在Windows環境下工作時,Windows版本的nginx更像是概念驗證版本,而不是全功能移植。這是由於目前nginx和Windows內核架構之間交互的某些限制導致。Windows版本ngnix已知的問題包括:低並發連接數、性能降低、不支持緩存和帶寬策略。未來Windows版本的nginx的功能會更接近主流版本。
4.2 Nginx架構綜覽
傳統基於進程或線程的模型使用單獨的進程或線程處理並發連接,因而會阻塞於網絡或I/O操作。根據不同的應用,就內存和CPU而言,這是非常低效的。派生進程或線程需要准備新的運行環境,包括在內存上分配堆和棧、生成一個新的運行上下文。創建這些東西還需要額外的CPU時間,而且過度的上下文切換引起的線程抖動最終會導致性能低下。所有這些復雜性在如Apache web服務器的老架構上一覽無遺。在提供豐富的通用應用功能和優化服務器資源使用之間需要做一個權衡。
最早的時候,nginx希望為動態增長的網站獲得更好的性能,並且密集高效的使用服務器資源,所以其使用了另外一個模型。受不斷發展的在不同操作系統上開發基於事件模型的技術驅動,最終一個模塊化,事件驅動,異步,單線程,非阻塞架構成為nginx代碼的基礎。
Nginx大量使用多路復用和事件通知,並且給不同的進程分配不同的任務。數量有限的工作進程(Worker)使用高效的單線程循環處理連接。每個worker進程每秒可以處理數千個並發連接、請求。
代碼結構
Nginx worker的代碼包含核心和功能模塊。核心負責維護一個緊湊的事件處理循環,並且在請求處理的每個階段執行對應的模塊代碼段。模塊完成了大部分展現和應用層功能。包括從網絡和存儲設備讀取、寫入,轉換內容,進行輸出過濾,SSI(server-side include)處理,或者如果啟用代理則轉發請求給后端服務器。
nginx模塊化的架構允許開發者擴展web服務器的功能,而不需要修改nginx核心。Nginx模塊可分為:核心、事件模塊,階段處理器,協議、變量處理器,過濾器,上游和負載均衡器等。目前,nginx不支持動態加載模塊,即模塊代碼是和nginx核心代碼一起編譯的。模塊動態加載和ABI已經計划在將來的某個版本開發。更多關於不同模塊角色的詳細信息可在14.4章找到。
Nginx在BSD、Linux和Solaris系統上使用kqueue、epoll和event ports等技術,通過事件通知機制來處理網絡連接和內容獲取,包括接受、處理和管理連接,並且大大增強了磁盤IO性能。目的在於盡可能的提供操作系統建議的手段,用於從網絡進出流量,磁盤操作,套接字讀取和寫入,超時等事件中及時異步地獲取反饋。Nginx為每個基於Unix的操作系統大量優化了這些多路復用和高級I/O操作的方法。
圖14.1展示了nginx架構的高層設計。
前面提到過,nginx不為每個連接派生進程或線程,而是由worker進程通過監聽共享套接字接受新請求,並且使用高效的循環來處理數千個連接。Nginx不使用仲裁器或分發器來分發連接,這個工作由操作系統內核機制完成。監聽套接字在啟動時就完成初始化,worker進程通過這些套接字接受、讀取請求和輸出響應。
事件處理循環是nginx worker代碼中最復雜的部分,它包含復雜的內部調用,並且嚴重依賴異步任務處理的思想。異步操作通過模塊化、事件通知、大量回調函數以及微調定時器等實現。總的來說,基本原則就是盡可能做到非阻塞。Nginx worker進程唯一會被阻塞的情形是磁盤性能不足。
由於nginx不為每個連接派生進程或線程,所以內存使用在大多數情況下是很節約並且高效的。同時由於不用頻繁的生成和銷毀進程或線程,所以nginx也很節省CPU時間。Nginx所做的就是檢查網絡和存儲的狀態,初始化新連接並添加到主循環,異步處理直到請求結束才從主循環中釋放並刪除。兼具精心設計的系統調用和諸如內存池等支持接口的精確實現,nginx在極端負載的情況下通常能做到中低CPU使用率。
nginx派生多個worker進程處理連接,所以能夠很好的利用多核CPU。通常一個單獨的worker進程使用一個處理器核,這樣能完全利用多核體系結構,並且避免線程抖動和鎖。在一個單線程的worker進程內部不存在資源匱乏,並且資源控制機制是隔離的。這個模型也允許在物理存儲設備之間進行擴展,提高磁盤利用率以避免磁盤I/O導致的阻塞。將工作負載分布到多個worker進程上最終能使服務器資源被更高效的利用。
針對某些磁盤使用和CPU負載的模式,nginx worker進程數應該進行調整。這里的規則比較基本,系統管理員應根據負載多嘗試幾種配置。通常推薦:如果負載模式是CPU密集型,例如處理大量TCP/IP協議,使用SSL,或者壓縮數據等,nginx worker進程應該和CPU核心數相匹配;如果是磁盤密集型,例如從存儲中提供多種內容服務,或者是大量的代理服務,worker的進程數應該是1.5到2倍的CPU核心數。一些工程師基於獨立存儲單元的數目來決定worker進程數,雖然這個方法的有效性取決於磁盤存儲配置的類型,。
Nginx開發者在下個版本中要解決的一個主要問題是怎么避免磁盤I/O引起的阻塞。目前,如果沒有足夠的存儲性能為一個worker進程的磁盤操作提供服務,這個進程就會阻塞在磁盤讀寫操作上。一些機制和配置指令用於緩解這個磁盤I/O阻塞的場景,最顯著的是sendfile和AIO指令,這通常可以大幅提升磁盤性能。應該根據數據集(data set),可用內存數,以及底層存儲架構等來規划安裝nginx。
當前的worker模型的另一個問題是對嵌入腳本的支持有限。舉例來說,標准的nginx發布版只支持Perl作為嵌入腳本語言。這個原因很簡單:嵌入腳本很可能會在任何操作上阻塞或者異常退出,這兩個行為都會導致worker進程掛住而同時影響數千個連接。將腳本更簡單,更可靠地嵌入nginx,並且更適合廣泛應用的工作已經列入計划。
nginx 進程角色
Nginx在內存中運行多個進程,一個master進程和多個worker進程。同時還有一些特殊用途的進程,例如緩存加載和緩存管理進程。在nginx 1.x版本,所有進程都是單線程的,使用共享內存作為進程間通信機制。Master進程使用root用戶權限運行,其他進程使用非特權用戶權限運行。
master進程負責下列工作:
- 讀取和校驗配置文件
- 創建、綁定、關閉套接字
- 啟動、終止、維護所配置的worker進程數目
- 不中斷服務刷新配置文件
- 不中斷服務升級程序(啟動新程序或在需要時回滾)
- 重新打開日志文件
- 編譯嵌入Perl腳本
Worker進程接受、處理來自客戶端的連接,提供反向代理和過濾功能以及其他nginx所具有的所有功能。由於worker進程是web服務器每日操作的實際執行者,所以對於監控nginx實例行為,系統管理員應該保持關注worker進程。
緩存加載進程負責檢查磁盤上的緩存數據並且在內存中維護緩存元數據的數據庫。基本上,緩存加載進程使用特定分配好的目錄結構來管理已經存儲在磁盤上的文件,為nginx提供准備,它會遍歷目錄,檢查緩存內容元數據,當所有數據可用時就更新相關的共享內存項。
緩存管理進程主要負責緩存過期和失效。它在nginx正常工作時常駐內存中,當有異常則由master進程重啟。
Nginx緩存簡介
Nginx在文件系統上使用分層數據存儲實現緩存。緩存主鍵可配置,並且可使用不同特定請求參數來控制緩存內容。緩存主鍵和元數據存儲在共享內存段中,緩存加載進程、緩存管理進程和worker進程都能訪問。目前不支持在內存中緩存文件,但可以用操作系統的虛擬文件系統機制進行優化。每個緩存的響應存儲到文件系統上的不同文件,Nginx配置指令控制存儲的層級(分幾級和命名方式)。如果響應需要緩存到緩存目錄,就從URL的MD5哈希值中獲取緩存的路徑和文件名。
將響應內容緩存到磁盤的過程如下:當nginx從后端服務器讀取響應時,響應內容先寫到緩存目錄之外的一個臨時文件。nginx完成請求處理后,就將這個臨時文件重命名並移到緩存目錄。如果用於代理功能的臨時目錄位於另外一個文件系統,則臨時文件會被拷貝一次,所以建議將臨時目錄和緩存目錄放到同一個文件系統上。如果需要清除緩存目錄,也可以很安全地刪除文件。一些第三方擴展可以遠程控制緩存內容,而且整合這些功能到主發布版的工作已經列入計划。
4.3 Nginx配置文件
Nginx配置系統來自於Igor Sysoev使用Apache的經驗。他認為可擴展的配置系統是web服務器的基礎。當維護龐大復雜的包括大量的虛擬服務器、目錄、位置和數據集等配置時,會遇到可伸縮性問題。對於一個相對大點的網站,系統管理員如果沒有在應用層進行恰當的配置,那么這將會是一個噩夢。
所以,nginx配置為簡化日常維護而設計,並且提供了簡單的手段用於web服務器將來的擴展。
配置文件是一些文本文件,通常位於/usr/local/etc/nginx
或/etc/nginx
。主配置文件通常命名為nginx.conf
。為了保持整潔,部分配置可以放到單獨的文件中,再自動地被包含到主配置文件。但應該注意的是,nginx目前不支持Apache風格的分布式配置文件(如.htaccess文件),所有和nginx行為相關的配置都應該位於一個集中的配置文件目錄中。
Master進程啟動時讀取和校驗這些配置文件。由於worker進程是從master進程派生的,所以可以使用一份編譯好、只讀的配置信息。配置信息結構通過常見的虛擬內存管理機制自動共享。
Nginx配置具有多個不同的上下文,如:main, http, server, upstream, location (以及用於郵件代理的 mail ) 等指令塊。這些上下文不重疊,例如,一個location 指令塊是不能放入main指令塊中。並且,為了避免不必要的歧義,不存在一個類似於“全局web服務器”的配置。Nginx配置特意做的整潔和富有邏輯性,允許用戶可以建立包含上千個指令的復雜的配置文件。在一次私人談話中,Sysoev說:“全局服務器配置中的位置、目錄和其他一些指令是Apache中我所不喜歡的特性,所以這就是不在nginx實現這些的原因。”
配置語法、格式和定義遵循一個所謂的C風格協定。這種構建配置文件的方法在開源軟件和商業軟件中有廣泛的應用。通過設計,C風格配置很適合嵌套描述,富有邏輯性,易於創建、讀取和維護,深受廣大工程師喜歡。同時nginx的C風格配置也易於自動化。
雖然一些nginx配置指令看起來像Apahce配置的一部分,但是設置一個nginx實例是完全不同的體驗。例如,雖然nginx支持重寫規則,但是系統管理員要手工的轉換Apache重寫配置使之適合nginx風格。同樣,重寫引擎的實現也是不一樣的。
通常來說,nginx設置也提供了幾種原始機制的支持,對於高效的web服務器配置很有幫助。有必要簡單了解下變量和try_files
指令,這些差不多是nginx所獨有的。Nginx開發了變量用於提供附加的更強大的機制來控制運行時的web服務器配置。變量為快速賦值做了優化,並且在內部預編譯為索引。賦值是按需計算的,例如,變量的值通常只在這個請求的生命周期中計算一次,而后緩存起來。變量可在不同的配置指令中使用,為描述條件請求處理行為提供了更多彈性。
try_files
指令對於用更適當的方式逐漸替換if 條件配置語句是很重要的,並且它設計用來快速高效的嘗試不同的URI與內容之間的映射。總的來說,try_files
指令很好用,並且及其高效和有用。推薦讀者完整的看看這個指令,並在任何能用的地方用上它。
4.4 深入nginx
前面提到過,nginx代碼包含核心和其他模塊。核心負責提供web服務器的基礎,web和郵件反向代理功能;實現底層網絡協議,構建必要的運行環境,並且保證不同模塊之間的無縫交互。但是,大部分協議相關以及應用相關的特性是由其他模塊完成,而不是核心模塊。
在內部,nginx通過模塊流水線或模塊鏈處理連接。換言之,每個操作都有一個模塊做對應的工作。例如:壓縮,修改內容,執行SSI,通過FastCGI或uwsgi協議同后端應用服務器通信,以及同memcached通信等。
在核心和實際功能模塊之間,有兩個模塊http和mail。這兩個模塊在核心和底層組件之間提供了附加抽象層。這些模塊處理同各自應用層協議相關的事件序列,如實現HTTP、SMTP或IMAP。與核心一起,這些上層模塊負責以正確的次序調用各自的功能模塊。雖然目前HTTP協議是作為http模塊的一部分實現的,但將來計划將其獨立為一個功能模塊,以支持其他協議,如SPDY(參考“SPDY: An experimental protocol for a faster web”)。
功能模塊可以分為事件模塊,階段處理器,輸出過濾器,變量處理器,協議模塊,上游和負載均衡器等類型。雖然事件模塊和協議也用於mail模塊,但是這些模塊大部分用於補充nginx的HTTP功能。事件模塊提供了基於操作系統的事件通知機制,如kqueue 或 epoll,這些取決於操作系統的能力和構建配置。協議模塊允許nginx通過HTTPS, TLS/SSL, SMTP, POP3 和 IMAP等協議通信。
一個典型的HTTP請求處理周期如下:1. 客戶端發送HTTP請求。2. nginx核心從配置文件查找匹配該請求的位置,根據這個位置信息選擇適當的階段處理器。3. 如果配置為反向代理,負載均衡器挑選一個上游服務器用於轉發請求。4. 階段處理器完成工作,並且傳遞每個輸出緩沖區給第一個過濾器。5. 第一個過濾器傳遞輸出給第二個過濾器。6. 第二個過濾器傳遞輸出給第三個等等。7. 最終響應發送給客戶端。
Nginx模塊是高度可定制化的。它通過一系列指向可執行函數的回調指針來工作。因而,帶來的副作用就是為第三方開發者加重了負擔,因為他們必須精確的定義模塊應怎么運行和何時運行。Nginx的API和開發者文檔都經過優化使之更具有可用性來減輕開發難度。
一些在nginx中插入模塊的例子:
- 配置文件讀取和處理之前
- Location和server的每個配置指令生效時
- Main配置初始化時
- Server配置初始化時
- Server配置合並到main配置時
- Location配置初始化或者合並到上級server配置時
- Master進程啟動或退出時
- 新的worker進程啟動或退出時
- 處理請求時
- 過濾響應頭和響應體時
- 挑選,初始化和重新初始化上游服務器時
- 處理上游服務器響應時
- 完成與上游服務器的交互時
在Worker內部,生成響應的過程如下:
- 開始
ngx_worker_process_cycle()
- 通過操作系統的機制處理事件(如 epoll 或 kqueue)。
- 接受事件並調用對應的動作。
- 處理或轉發請求頭和請求體。
- 生成響應內容,並流式發送給客戶端。
- 完成請求處理。
- 重新初始化定時器和事件。
事件循環自身(步驟5和6)確保增量產生響應並且流式發送給客戶端。
更詳細的處理HTTP請求過程如下:
- 初始化請求處理
- 處理請求頭
- 處理請求體
- 調用對應的處理器
- 執行所有的處理階段
當nginx處理一個HTTP請求時,會經過多個處理階段。每個階段都調用對應的處理器。通常,階段處理器處理一個請求后產生對應的輸出,階段處理器在配置文件的location中定義。
階段處理器一般做四件事情:獲取location配置,產生適當的響應,發送響應頭,發送響應體。處理器函數有一個參數:描述請求的結構體。請求結構體有許多關於客戶端請求的有用信息,例如:請求方法類型,URI和請求頭等。
當讀取完HTTP請求頭之后,nginx查找相關的虛擬服務器配置,如果找到虛擬服務器,請求會經過下面六個階段:
- server rewrite phase
- location phase
- location rewrite phase (which can bring the request back to the previous phase可以將請求帶回到前面的階段)
- access control phase
- try_files phase
- log phase
為了給請求生成必要的響應內容,nginx傳遞請求給匹配的內容處理器。根據location配置,nginx會先嘗試無條件處理器,如perl
,proxy_pass
,flv
,mp4
等。如果這個請求不匹配這幾個內容處理器,將會按下面順序挑選一個處理器:random index
,index
,autoindex
,gzip_static
,static
。
Nginx文檔中有Index模塊的詳細內容,這個模塊只處理結尾為斜杠的請求。如果不匹配mp4
或autoindex
模塊,則認為響應內容是磁盤上的一個文件或目錄(即靜態的),這由static
內容處理器完成服務。如果是目錄,將自動重寫URI保證結尾是一個斜杠(從而發起一個HTTP重定向)。
內容處理器產生的內容則被傳遞到過濾器。過濾器也同location相關,一個location可配置多個過濾器。過濾器加工處理器產生的輸出。處理器的執行順序在編譯時決定,對於原生過濾器,順序是已經定義好的,對於第三方過濾器,可以在編譯階段設置先后順序。當前的nginx實現中,過濾器只能修改輸出的數據,還不能編寫修改輸入的數據的過濾器。輸入過濾器將在將來的版本提供。
過濾器遵循一個特定的設計模式。過濾器被調用后開始工作,調用下一個過濾器直到過濾器鏈中的最后一個。完成之后,nginx結束響應。過濾器不用等待前面的過濾器結束。一旦前一個過濾器提供的輸入已經可用,下一個過濾器便可以啟動自己的工作(很像Unix中的管道)。因而,在從上游服務器接收到所有的響應之前,所生成的輸出響應已經被發送給客戶端。
過濾器有header filter和body filter,nginx將響應的header和body分別發送給相關的過濾器。
Header filter包含3個基本步驟:
- 決定是否處理這個響應
- 處理響應
- 調用下一個過濾器
body filter轉換所生成的內容。body filter的一些例子:
- SSI
- XSLT過濾
- 圖片過濾(例如調整圖片大小)
- 字符集轉換
- Gzip壓縮
- Chunked編碼
經過過濾器鏈之后,響應被發送到writer。有兩個額外的具有特定功能的過濾器與writer相關,copy filter和postpone filter。Copy filter負責將相關的響應內容填充到內存緩沖區,這些響應內容有可能存儲在反向代理的臨時目錄。Postpone filter用於子請求處理。
子請求是一個處理請求、響應很重要的機制,同時也是nginx最強大的功能之一。通過子請求,Nginx可以返回另一個URL的響應,這個URL與客戶端最初請求的URL不同。一些web框架稱之為內部跳轉,但nginx功能更強,不僅能運行多個子請求並將這些子請求的響應合並成一個,而且還能嵌套和分級。子請求可以產生子-子請求,子-子請求能產生子-子-子請求。子請求可以映射到磁盤文件,其他處理,或者上游服務器。子請求在根據原始響應數據插入附加內容時很有用。例如,SSI模塊使用一個過濾器解析返回文檔的內容,然后用指定URL的內容來替換include指令。或者做一個過濾器,能夠在一個URL產生的響應內容之后附加一些新的文檔內容。
上游(upstream)和負載均衡器同樣也值得簡單介紹一下。上游用於實現反向代理處理器(proxy_pass
處理器)。上游模塊組裝好請求發送給上游服務器(或稱為“后端”),然后接收上游服務器返回的響應。這個過程不調用輸出過濾器。上游模塊僅僅設置回調函數,用於當上游服務器可讀或可寫時調用。回調函數實現下列功能:
- 准備請求緩沖區(或緩沖區鏈),用於發送給上游服務器
- 重新初始化、重置與上游服務器之間的連接(應在重新發起請求之前)
- 處理上游服務器響應的首字節,並且保存響應內容的指針
- 放棄請求(當客戶端過早關閉連接時)
- 結束請求(當nginx完成讀取上游服務器響應時)
- 修整響應體內容(例如除去空白)
如果上游服務器大於一個,負載均衡器模塊可附加在proxy_pass
處理器上,用於提供選擇上游服務器的能力。負載均衡器注冊了一個配置文件指令,提供附加的上游服務器初始化功能(通過DNS解析上游服務器名字等),初始化連接結構體,決定如何路由請求,並且更新狀態信息。目前,nginx支持兩種標准的上游服務器負載均衡規則:輪詢和ip哈希。
上游模塊和負載均衡處理機制的算法能檢測上游服務器異常,並將新請求重新路由到可用的上游服務器,還有更多的工作計划加強這個功能。總之,負載均衡器的改進計划更多些,下個版本的nginx將大幅度提升在不同上游服務器之間分發負載和健康檢測的機制。
還有一些有意思的模塊在配置文件中提供了額外的變量供使用。這些變量通過不同的模塊生成和更新,有兩個模塊完全用於變量:geo
和map
。geo
模塊用於更方便的基於IP地址追蹤客戶端地址,這個模塊可以根據客戶端IP地址生成任意變量。另一個map
模塊允許從一個變量生成另一個變量,提供將主機名和其他變量方便的進行映射的基本能力。這類模塊稱為變量處理器。
nginx worker進程實現的內存分配機制從某方面來說來自於Apache。Nginx內存管理的高層描述:對於每個連接,必要的內存緩沖區是動態分配的,用於存儲或操縱請求、響應的頭和體,當連接關閉時釋放。很重要的一點是nginx盡可能的去避免在內存中拷貝數據,大部分的數據通過指針進行傳遞,而不是調用memcpy。
更深入一點,當一個模塊產生響應時,這些響應內容放入內存緩沖區,並被添加到一個緩沖區鏈。這個緩沖區鏈同樣適用於子請求處理。由於根據模塊類型不同存在多個處理場景,所以nginx中的緩沖區鏈相當復雜。例如,在實現body filter模塊時,精確的管理緩沖區是很棘手的。這個模塊同一時間只能處理緩沖區鏈中的一個緩沖區,它必須決定是否覆蓋輸入緩沖區,是否用新分配的緩沖區替換這個緩沖區,或者在這個緩沖區之前或之后插入一個新緩沖區。更復雜的情況,有時一個模塊收到的數據需要多個緩沖區存儲,因此它必須處理一個不完整的緩沖區鏈。但是由於目前nginx僅提供了底層API用於操縱緩沖區鏈,所以開發者應該真正掌握nginx這一晦澀難懂的部分之后,再去開發第三方模塊。
上面提到的內容中需要注意的一點,內存緩沖區是為連接的整個生命周期分配的,所以對於長連接需要消耗額外的內存。同時,對於空閑的keep alive連接,nginx僅消耗550字節內存。將來的nginx版本可能進行優化以使長連接重用和共用內存緩沖區。
內存分配管理的任務由nginx內存池分配器完成。共享內存區用於存放接受互斥鎖(accept mutex),緩存元數據,SSL會話緩存,以及和帶寬策略管理(限速)相關的信息。Nginx實現了slab分配器用於管理共享內存,提供了一系列鎖機制(互斥鎖和信號量),以允許安全地並發使用共享內存。為了組織復雜的數據結構,nginx也提供了紅黑樹的實現。紅黑樹用於在內存中保存緩存元數據,查找非正則location定義,以及其他一些任務。
不幸的是,上述內容從未以一致並且簡單的方式介紹過,以致開發第三方模塊的工作相當復雜。雖然有一些nginx內部實現的好文檔,例如,Evan Miller寫的,但是這些文檔需要做很多還原工作,nginx模塊的開發還是像變魔術一樣。
雖然開發第三方模塊是如此困難,nginx社區最近還是涌現大量有用的第三方模塊。例如,將Lua解釋器嵌入nginx,負載均衡附加模塊,完整的Web DAV支持,高級緩存控制,以及其他本章作者所鼓勵和將來支持的有趣的第三方工作。
4.5 優秀實踐
Igor Sysoev開始編寫nginx時,大部分構建互聯網的軟件都已經存在,這些軟件的架構一般遵循傳統服務器和網絡硬件、操作系統、以及過去互聯網架構的定義。但是這並未阻止Igor考慮在web服務器領域做進一步的工作。所以,顯然第一個優秀實踐是:總有提升空間。
帶着開發更好web軟件的想法,Igor花了很多時間開發原始代碼結構,並研究在多個操作系統下優化代碼的不同手段。十年后,考慮到1.0版本已經經過十年活躍開發,Igor開發了2.0版本原型。很明顯,這個新架構的初始原型和代碼結構,對於軟件的后續開發及其重要。
另外值得提到的一點是聚焦開發。Nginx 的windows版本是個好例子,說明無論在開發者的核心技能或應用目標上避免稀釋開發工作是值得的。同樣努力加強nginx重寫引擎對現存遺留配置的后向兼容能力,也是值得的。
最后特別值得提到的是,盡管nginx開發者社區並不大,nginx的第三方模塊和擴展還是成為nginx受歡迎的一個很重要的因素。Nginx用戶社區和作者們很感謝Evan Miller, Piotr Sikora, Valery Kholodkov, Zhang Yichun (agentzh)以及其他優秀軟件工程師所做的工作。