1.Nginx
架構
Nginx
高性能,與其架構有關。
Nginx
架構: nginx
運行時,在unix
系統中以daemon
形式在後臺運行,後臺進程包含一個master
進程和多個worker
進程。Nginx
以多進程形式工作,也支持多線程方式,丹nginx
默認採用多進程方式,也是主流方式。
1.1Nginx
多進程模式
多進程模式,會有一個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.3Nginx
處理請求
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.1Connection
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.2Keep alive
http
請求是請求應答式的,如果我們知道每個請求頭與相應體的長度,那麼我們可以在一個連接上面執行多個請求。即長連接。如果當前請求需要有body
,那麼nginx
就需要客戶端在請求頭中指定content-length
來表面body
的大小,否則返回400
錯誤。那麼響應體的長度呢?http
協議中關於響應body
長度的確定:
-
對於
http1.0
協議來說,如果響應頭中有content-length
頭,則以content-length
的長度就可以知道body
的長度,客戶端在接收body
時,可以依照這個長度接收數據,接收完後,就表示該請求完成。如果沒有content-length
,客戶端會一直接收數據,直到服務端主動端口連接,才表示body
接收完 -
對於
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.3Pipe
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.1Nginx.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
請求處理週期如下:
- 客戶端發送
HTTP
請求。 nginx
核心從配置文件查找匹配該請求的位置,根據這個位置信息選擇適當的階段處理器。- 如果配置爲反向代理,負載均衡器挑選一個上游服務器用於轉發請求。
- 階段處理器完成工作,並且傳遞每個輸出緩衝區給第一個過濾器。
- 第一個過濾器傳遞輸出給第二個過濾器。
- 第二個過濾器傳遞輸出給第三個等等。
- 最終響應發送給客戶端。
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)
以及其他優秀軟件工程師所做的工作。
英文原文:http://www.aosabook.org/en/nginx.html
中文參考:http://www.ituring.com.cn/article/4436