前言
在學(xué)習(xí) Nginx 之前,我們首先有必要搞清楚下面幾個問題:
1. Web服務(wù)器是怎么工作的?
2. Apache 與 Nginx 有何異同?
3. Nginx 工作模式是怎樣的?
下面就圍繞這幾個問題,進行解釋(內(nèi)容來自網(wǎng)絡(luò)及個人理解)
常見 Web 服務(wù)器服務(wù)方式
三種工作模式比較:
Web 服務(wù)器主要為用戶提供服務(wù),必須以某種方式,工作在某個套接字上,一般Web服務(wù)器在處理用戶請求時,一般有如下三種方式:
(1)多進程
(2)多線程
(3)異步
1. 多進程
為每個請求分配一個進程來處理。由于操作系統(tǒng)中,生成進程、銷毀進程、進程間切換都很消耗CPU和內(nèi)存,當(dāng)負載高時,性能會明顯下降。
優(yōu)點:采用獨立進程處理進程的方式,進程之間是獨立的,單個進程的異常不會影響到其他進程的工作,因此穩(wěn)定性最好
缺點:在高負載的時候,操作系統(tǒng)不可能無限制的為用戶請求創(chuàng)建進程,CPU在眾多進程之間切換的開銷也會增加,而且進程之間的獨立性,資源無法共享,造成內(nèi)存的重復(fù)利用
2. 多線程
一個進程中生成多個線程來響應(yīng)用戶請求。由于線程開銷相對進程來說小的多,而且線程共享進程的部分資源,因此線程比進程更輕量級,更高效。
優(yōu)點:線程比較進程來說,更輕量級,使用一個進程多個線程的方式,而且多個線程可以共享進程的資源,可以支持更多的請求。
缺點:高負載時,多線程之間頻繁的切換會造成線程的抖動,如果某個進程中的某個線程異常,則會造成整個進程內(nèi)的線程都無法正常工作,因此穩(wěn)定性較差。
3. 異步
異步是開發(fā)中采用的一種編程方式(比如 Python 中的 協(xié)程)是三種方式中開銷最小的,但是這種方式對編程要求較高,因此多任務(wù)之間的調(diào)度如果出現(xiàn)問題,一般都是整體故障。
優(yōu)點:性能最好。一個進程或線程響應(yīng)用戶請求,不需要額外的開銷,資源占用較低
缺點:某個進程或線程出錯,會造成整個程序異常,甚至服務(wù)宕機,穩(wěn)定性在三者中最差
一個 Web 請求的處理過程
(1)客戶發(fā)起請求到服務(wù)器網(wǎng)卡
(2)服務(wù)器網(wǎng)卡接受到請求后交給內(nèi)核處理
(3)內(nèi)核根據(jù)請求對應(yīng)的套接字,將請求交給工作在用戶空間中web服務(wù)器進程
(4)web服務(wù)器進程根據(jù)用戶請求,向內(nèi)核進程系統(tǒng)調(diào)用,申請獲取響應(yīng)資源,如用戶訪問 index.html
(5)內(nèi)核發(fā)現(xiàn)web服務(wù)器進程請求的是一個存放在硬盤上的資源,因此通過驅(qū)動程序連接磁盤
(6)內(nèi)核調(diào)度磁盤,獲取需要的資源
(7)內(nèi)核將資源存放在自己的緩沖區(qū)中,并通知web服務(wù)器進程
(8)web服務(wù)器進程通過系統(tǒng)調(diào)用獲取資源,并將其復(fù)制到進程自己的緩沖區(qū)中
(9)web服務(wù)器進程形成響應(yīng),通過系統(tǒng)調(diào)用再次發(fā)給內(nèi)核以響應(yīng)用戶請求
(10)內(nèi)核將響應(yīng)發(fā)送至網(wǎng)卡
(11)網(wǎng)卡發(fā)送響應(yīng)給用戶
通過這樣的一個復(fù)雜過程,一次請求就完成了。
簡單來講:用戶請求 -> 送達至用戶空間 -> 系統(tǒng)調(diào)用 -> 內(nèi)核空間 -> 內(nèi)核到磁盤上讀取網(wǎng)頁資源 -> 返回用戶空間 -> 響應(yīng)給用戶
在這個過程中,有兩次 I/O調(diào)用:
(1)客戶端請求的網(wǎng)絡(luò)I/O
(2)web服務(wù)器請求頁面的磁盤I/O
各種I/O模型詳解
通過上面對連接的處理分析,我們知道工作在用戶空間的web服務(wù)器進程是無法直接操作IO的,需要通過系統(tǒng)調(diào)用進行,其關(guān)系如下:
用戶空間中的web服務(wù)器進程向內(nèi)核進行系統(tǒng)調(diào)用申請IO,內(nèi)核將資源從IO調(diào)度到內(nèi)核的buffer中(wait階段),內(nèi)核還需將數(shù)據(jù)從內(nèi)核buffer中復(fù)制(copy階段)到web服務(wù)器進程所在的用戶空間,才算完成一次IO調(diào)度。這幾個階段都是需要時間的。根據(jù)wait和copy階段處理等待的機制不同,可將I/O動作分為如下五種模式:
(1)阻塞
(2)非阻塞
(3)IO復(fù)用(select、poll)
(4)事件驅(qū)動的 IO
(5)異步 IO
I/O模式簡介#
首先,需要解釋下:同步和異步、阻塞和非阻塞的概念。
同步:
發(fā)出一個功能調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回。也就是必須一件一件事做,等前一件做完了才能做下一件事。
異步:
異步的概念和同步相對。當(dāng)一個異步過程調(diào)用發(fā)出后,調(diào)用者不能立即得到結(jié)果。實際處理這個調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者
阻塞:
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起(線程進入非可執(zhí)行狀態(tài),在這個狀態(tài)下,cpu不會給線程分配時間片,即線程暫停運行),函數(shù)只有得到結(jié)果之后才會返回。
阻塞調(diào)用和同步調(diào)用是不同的,對于同步調(diào)用來說,很多時候當(dāng)前線程還是激活的,只是從邏輯上當(dāng)前函數(shù)沒有返回,它還會搶占cpu去執(zhí)行其他邏輯,也會主動監(jiān)測IO是否準備好
非阻塞:
非阻塞和阻塞的概念相對應(yīng),指在不能立刻得到結(jié)果之前,該函數(shù)不會阻塞當(dāng)前線程,而會立即返回。
再簡單點理解就是:
1. 同步,就是調(diào)用一個功能,功能沒有結(jié)束前,死等結(jié)果;
2. 異步,我調(diào)用一個功能,不需要知道該功能結(jié)果,該功能有結(jié)果后通知我;
3. 阻塞,就是調(diào)用函數(shù),函數(shù)沒有接收完數(shù)據(jù)或沒有得到結(jié)果之前,函數(shù)不會返回;
4. 非阻塞,就是調(diào)用函數(shù),函數(shù)立即返回,等到處理結(jié)果后通過select通知調(diào)用者
網(wǎng)絡(luò)上有一個很好例子:
老張愛喝茶,廢話不說,煮開水。
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
1 老張把水壺放到火上,立等水開。(同步阻塞)
老張覺得自己有點傻
2 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞)
老張還是覺得自己有點傻,于是變高端了,買了把會響笛的那種水壺。水開之后,能大聲發(fā)出嘀~~~~的噪音。
3 老張把響水壺放到火上,立等水開。(異步阻塞)
老張覺得這樣傻等意義不大
4 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。(異步非阻塞)
老張覺得自己聰明了。
所謂同步異步,只是對于水壺而言。
普通水壺,同步;響水壺,異步。
雖然都能干活,但響水壺可以在自己完工之后,提示老張水開了。這是普通水壺所不能及的。
同步只能讓調(diào)用者去輪詢自己(情況2中),造成老張效率的低下。
所謂阻塞非阻塞,僅僅對于老張而言。
立等的老張,阻塞;看電視的老張,非阻塞。
情況1和情況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對于立等的老張沒有太大的意義。所以一般異步是配合非阻塞使用的,這樣才能發(fā)揮異步的效用。
同步IO 和 異步IO 的區(qū)別在于:數(shù)據(jù)拷貝的時候進程是否阻塞;
阻塞IO 和 非阻塞IO 的區(qū)別在于:應(yīng)用程序的調(diào)用是否立即返回。
五種 I/O 模型
(1)阻塞IO
(2)非阻塞IO
(3)IO復(fù)用
(4)事件驅(qū)動IO
(5)異步IO
其中前 4 種都是同步,最后一種異步。
(1)阻塞IO
應(yīng)用程序調(diào)用一個IO函數(shù),導(dǎo)致應(yīng)用程序阻塞,等待數(shù)據(jù)準備好。如果數(shù)據(jù)沒有準備好,一直等待數(shù)據(jù)準備好了,從內(nèi)核拷貝到用戶空間,IO函數(shù)返回成功指示。
阻塞IO模型圖:在調(diào)用recv()/recvfrom()函數(shù)時,發(fā)生在內(nèi)核中等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的過程。
當(dāng)調(diào)用 recvfrom 函數(shù)時,系統(tǒng)首先檢查是否有準備好的數(shù)據(jù)。如果數(shù)據(jù)沒有準備好,那么系統(tǒng)就處于等待狀態(tài)。當(dāng)數(shù)據(jù)準備好后,將數(shù)據(jù)沖系統(tǒng)緩沖區(qū)復(fù)制到用戶空間,然后該函數(shù)返回。在應(yīng)用程序中,調(diào)用 recvfrom 函數(shù)時,未必用戶空間就已經(jīng)存在數(shù)據(jù),那么此時 recvfrom 函數(shù)就會處于等待狀態(tài)。
(2)非阻塞IO
非阻塞IO通過進程反復(fù)調(diào)用IO(多次系統(tǒng)調(diào)用,并馬上返回);在數(shù)據(jù)拷貝的過程中,進程是阻塞的。
我們把一個socket接口設(shè)置為非阻塞就是告訴內(nèi)核,當(dāng)所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的I/O操作函數(shù)將不斷的檢測數(shù)據(jù)是否準備好,如果沒有準備好,繼續(xù)檢查,直到數(shù)據(jù)準備好為止。在這個不斷檢查的過程中,會大量的占用 CPU 的時間。因此一般的 WEB 服務(wù)器不會再用這種I/O 模式。
(3)I/O復(fù)用
主要模式是 select 和 epoll ; 對一個IO端口,兩次調(diào)用,兩次返回,比阻塞io并沒有什么優(yōu)越性;關(guān)鍵是能實現(xiàn)同時對多個IO端口進行監(jiān)聽;IO復(fù)用模型會用到 select、poll、epoll 函數(shù),這幾個函數(shù)也會使進程阻塞,但是和阻塞IO所不同的是,這三個函數(shù)可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的IO函數(shù)進行檢測,直到有數(shù)據(jù)可讀或可寫,才真正調(diào)用I/O操作函數(shù)。
******(4)信號驅(qū)動I/O******
首先我們運行套接字進行信號驅(qū)動IO,并安裝一個信號處理函數(shù),進程繼續(xù)運行并不阻塞。當(dāng)數(shù)據(jù)準備好時,進程會收到一個SIGIO信號,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。
在 wait 階段不阻塞,在 copy 階段阻塞。
******(5)異步I/O******
當(dāng)一個異步過程調(diào)用發(fā)出后,調(diào)用者不能立即得到結(jié)果。實際處理這個調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者的輸入輸出操作。
在 wait 和 copy 階段都不會阻塞。
最后,總結(jié)比較五種IO模型:
Linux I/O模型的具體實現(xiàn)
主要實現(xiàn)方式有以下幾種:
- select
- poll
- epoll
- kqueue
- /dev/poll
- iocp
select、poll、epoll 是 Linux 實現(xiàn)的,kqueue是FreeBSD實現(xiàn)的,/dev/poll 是SUN是Solaris實現(xiàn)的,iocp是windows 實現(xiàn)的。
select、poll 對應(yīng)第三種(IO復(fù)用)模型
iocp對應(yīng)第五種(異步IO)模型
epoll、kqueue、/dev/poll 其實也同select屬于同一種模型,只是更高級些,可以看做有了第4種(信號驅(qū)動IO)模型的某些特性,如callback機制
select、poll、epoll 簡介
epoll 和 select
都是能提供IO多路復(fù)用的解決方案。在現(xiàn)在的Linux內(nèi)核里都能夠支持,其中 epoll 是 Linux 所特有的,而 select 則應(yīng)該是 POSIX 所規(guī)定,一般操作系統(tǒng)均有實現(xiàn)。
select:
select 本質(zhì)上是通過設(shè)置或者檢查存放fd(文件描述符)標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來進行下一步處理。這樣所帶來的缺點是:
1. 單個進程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽端口的大小有限
一般來說這個數(shù)目和系統(tǒng)內(nèi)存大小有關(guān)系,具體數(shù)目可以 cat /proc/sys/fs/file-max 查看。
2. 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低
當(dāng)套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調(diào)函數(shù),當(dāng)他們活躍時,自動完成相關(guān)操作,那就避免了輪詢,這 正是epoll與kqueue做的。
3. 需要維護一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復(fù)制開銷大
poll:
poll 本質(zhì)上和 select 沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊列中加入一項并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進程,直到設(shè)備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經(jīng)歷了多次重復(fù)的遍歷。
它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的,但是同樣有一個缺點:
1. 大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義
2. poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
epoll:
epoll 是 nginx 采用的默認模式
epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認的模式,ET是“高速”模式。LT模式下,只要這個fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發(fā))模式中,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小于請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知。
上面所說的 LT模式 和 ET 模式,就是 水平觸發(fā) 和 邊緣觸發(fā)
水平觸發(fā)(LT模式):只要fd可讀,每次select 輪詢的時候,都會讀取。第一次輪詢沒有讀取完成,第二次輪詢還會提示
邊緣觸發(fā)(ET模式):只提示1次,不管有沒有讀完數(shù)據(jù)。從性能上來說,邊緣觸發(fā)的性能要好于水平觸發(fā)
epoll為什么要有EPOLLET觸發(fā)模式?
如果采用EPOLLLT模式的話,系統(tǒng)中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調(diào)用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率。而采用EPOLLET這種邊沿觸發(fā)模式的話,當(dāng)被監(jiān)控的文件描述符 上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知 你!!!這種模式比水平觸發(fā)效率高,系統(tǒng)不會充斥大量你不關(guān)心的就緒文件描述符。
epoll 的優(yōu)點:
1. 沒有最大并發(fā)連接的限制,能打開FD的上限大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口);
2. 效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會調(diào)用callback函數(shù);
epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān),因此在實際的網(wǎng)絡(luò)環(huán)境中,epoll的效率就會遠遠高于select和poll
3. 內(nèi)存拷貝,利用mmap() 文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷。
select、poll、epoll 區(qū)別總結(jié):
1. 支持一個進程所能打開的最大連接數(shù)
select
單個進程所能打開的最大連接數(shù)有 FD_SETSIZE 宏定義,其大小是32個整數(shù)的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當(dāng)然我們可以對進行修改,然后重新編譯內(nèi)核,但是性能可能會受到影響,這需要進一 步的測試。
poll
poll本質(zhì)上和select沒有區(qū)別,但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的。
epoll
雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機器上可以打開10萬左右的連接,2G內(nèi)存的機器可以打開20萬左右的連接
2. FD劇增后帶來的IO效率問題
select
因為每次調(diào)用時都會對連接進行線性遍歷,所以隨著FD的增加會造成遍歷速度慢的“線性下降性能問題”。
poll
poll 同 select 是一致的。
epoll
因為epoll內(nèi)核中實現(xiàn)是根據(jù)每個fd上的callback函數(shù)來實現(xiàn)的,只有活躍的socket才會主動調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。
3. 消息傳遞方式
select
內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動作
poll
poll 同 select 是一致的。
epoll
epoll通過內(nèi)核和用戶空間共享一塊內(nèi)存來實現(xiàn)的。
總結(jié):
綜上,在選擇select,poll,epoll時要根據(jù)具體的使用場合以及這三種方式的自身特點。
1. 表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調(diào)
2. select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設(shè)計改善
Apache Httpd的工作模式
apache三種工作模式
我們都知道Apache有三種工作模塊,分別為prefork、worker、event。
- prefork:多進程,每個請求用一個進程響應(yīng),這個過程會用到select機制來通知。
- worker:多線程,一個進程可以生成多個線程,每個線程響應(yīng)一個請求,但通知機制還是select不過可以接受更多的請求。
- event:基于異步I/O模型,一個進程或線程,每個進程或線程響應(yīng)多個用戶請求,它是基于事件驅(qū)動(也就是epoll機制)實現(xiàn)的。
prefork的工作原理
如果不用“--with-mpm”顯式指定某種MPM,prefork就是Unix平臺上缺省的MPM.它所采用的預(yù)派生子進程方式也是Apache1.3中采用的模式。prefork本身并沒有使用到線程,2.0版使用它是為了與1.3版保持兼容性;另一方面,prefork用單獨的子進程來 處理不同的請求,進程之間是彼此獨立的,這也使其成為最穩(wěn)定的MPM之一。
worker的工作原理
相對于prefork,worker是2.0版中全新的支持多線程和多進程混合模型的MPM。由于使用線程來處理,所以可以處理相對海量的請求,而系統(tǒng)資源的開銷要小于基于進程的服務(wù)器。但是,worker也使用了多進程,每個進程又生成多個線程,以獲得基于 進程服務(wù)器的穩(wěn)定性,這種MPM的工作方式將是Apache2.0的發(fā)展趨勢。
event 基于事件機制的特性
一個進程響應(yīng)多個用戶請求,利用callback機制,讓套接字復(fù)用,請求過來后進程并不處理請求,而是直接交由其他機制來處理,通過epoll機制來通知請求是否完成;在這個過程中,進程本身一直處于空閑狀態(tài),可以一直接收用戶請求。可以實現(xiàn)一個 進程程響應(yīng)多個用戶請求。支持持海量并發(fā)連接數(shù),消耗更少的資源。
** 如何提高Web服務(wù)器的并發(fā)連接處理能力?**
有幾個基本條件:
- 基于線程,即一個進程生成多個線程,每個線程響應(yīng)用戶的每個請求。
- 基于事件的模型,一個進程處理多個請求,并且通過epoll機制來通知用戶請求完成。
- 基于磁盤的AIO(異步I/O)
- 支持mmap內(nèi)存映射,mmap傳統(tǒng)的web服務(wù)器,進行頁面輸入時,都是將磁盤的頁面先輸入到內(nèi)核緩存中,再由內(nèi)核緩存中復(fù)制一份到web服務(wù)器上,mmap機制就是讓內(nèi)核緩存與磁盤進行映射,web服務(wù)器,直接復(fù)制頁面內(nèi)容即可。不需要先把磁盤的上的頁面先輸入到內(nèi)核緩存去。
剛好,Nginx 支持以上所有特性。所以Nginx官網(wǎng)上說,Nginx支持50000并發(fā),是有依據(jù)的。
Nginx優(yōu)異之處
簡介
nginx的主要著眼點就是其高性能以及對物理計算資源的高密度利用,因此其采用了不同的架構(gòu)模型。受啟發(fā)于多種操作系統(tǒng)設(shè)計中基于“事件”的高級處理機制,nginx采用了模塊化、事件驅(qū)動、文件異步、單線程及非阻塞的架構(gòu),并大量采用了多路復(fù)用及事件通知機制。在nginx中,連接請求由為數(shù)不多的幾個僅包含一個線程的進程worker以高效的回環(huán)(run-loop)機制進行處理,而每個worker可以并行處理數(shù)千個的并發(fā)連接及請求。
Nginx 工作原理
Nginx會按需同時運行多個進程:一個主進程(master)和幾個工作進程(worker),配置了緩存時還會有緩存加載器進程(cache loader)和緩存管理器進程(cache manager)等。所有進程均是僅含有一個線程,并主要通過“共享內(nèi)存”的機制實現(xiàn)進程間通信。主進程以root用戶身份運行,而worker、 cache loader和cache manager均應(yīng)以非特權(quán)用戶身份運行。
Nginx 架構(gòu)
Nginx 的整體架構(gòu)圖:
主進程(Master)主要完成如下工作:
- 讀取并驗正配置信息
- 創(chuàng)建、綁定及關(guān)閉套接字
- 啟動、終止及維護worker進程的個數(shù)
- 無須中止服務(wù)而重新配置工作特性
- 控制非中斷式程序升級,啟用新的二進制程序并在需要時回滾至老版本
- 重新打開日志文件
- 編譯嵌入式perl腳本
worker進程主要完成的任務(wù)包括:
- 接收、傳入并處理來自客戶端的連接;
- 提供反向代理及過濾功能;
- nginx任何能完成的其它任務(wù);
注:如果負載以CPU密集型應(yīng)用為主,如SSL或壓縮應(yīng)用,則worker數(shù)應(yīng)與CPU數(shù)相同;如果負載以IO密集型為主,如響應(yīng)大量內(nèi)容給客戶端,則worker數(shù)應(yīng)該為CPU個數(shù)的1.5或2倍。
為了更好地理解設(shè)計,你需要了解NGINX是如何工作的。NGINX之所以能在性能上如此優(yōu)越,是由于其背后的設(shè)計。許多web服務(wù)器和應(yīng)用服務(wù)器使用簡單的線程的(threaded)、或基于流程的(process-based)架構(gòu), NGINX則以一種復(fù)雜的事件驅(qū)動(event-driven)的架構(gòu)脫穎而出,這種架構(gòu)能支持現(xiàn)代硬件上成千上萬的并發(fā)連接。
設(shè)置場景——NGINX進程模型
為了更好的理解設(shè)計,你需要了解Nginx是如何工作的。NGINX有一個主線程(master process)(執(zhí)行特權(quán)操作,如讀取配置、綁定端口)和一系列工作進程(worker process)和輔助進程(helper process)。
任何Unix應(yīng)用程序的根本基礎(chǔ)都是線程或進程(從Linux操作系統(tǒng)的角度看,線程和進程基本上是相同的,主要區(qū)別是他們共享內(nèi)存的程度)。進程或線程,是一組操作系統(tǒng)可調(diào)度的、運行在CPU內(nèi)核上的獨立指令集。大多數(shù)復(fù)雜的應(yīng)用程序都并行運行多個線程或進程,原因有兩個:
- 可以同時使用更多的計算機內(nèi)核
- 線程和進程使并行操作很容易實現(xiàn)(例如,同時處理多個連接)。
進程和線程都消耗資源。它們都使用內(nèi)存和其他OS資源,導(dǎo)致內(nèi)核頻繁切換(被稱作上下文切換(context switch)的操作)。大多數(shù)現(xiàn)代服務(wù)器可以同時處理數(shù)百個小的、活躍的(active)線程或進程,但一旦內(nèi)存耗盡,或高I/O負載導(dǎo)致大量的上下文切換時,服務(wù)器的性能就會嚴重下降。
對于網(wǎng)絡(luò)應(yīng)用,通常會為每個連接(connection)分配一個線程或進程。這種架構(gòu)易于實現(xiàn),但是當(dāng)應(yīng)用程序需要處理成千上萬的并發(fā)連接時,這種架構(gòu)的擴展性就會出現(xiàn)問題。
NGINX是如何工作的?
NGINX使用一個了可預(yù)見式的(predictable)進程模型,調(diào)度可用的硬件資源:
主進程執(zhí)行特權(quán)操作,如讀取配置和綁定端口,還負責(zé)創(chuàng)建子進程(下面的三種類型)。
- 緩存加載進程(cache loader process)在啟動時運行,把基于磁盤的緩存(disk-based cache)加載到內(nèi)存中,然后退出。對它的調(diào)度很謹慎,所以其資源需求很低。
- 緩存管理進程(cache manager process)周期性運行,并削減磁盤緩存(prunes entries from the disk caches),以使其保持在配置范圍內(nèi)。
- 工作進程(worker processes)才是執(zhí)行所有實際任務(wù)的進程:處理網(wǎng)絡(luò)連接、讀取和寫入內(nèi)容到磁盤,與上游服務(wù)器通信等。
多數(shù)情況下,NGINX建議每1個CPU核心都運行1個工作進程,使硬件資源得到最有效的利用。你可以在配置中設(shè)置如下指令: worker_processes auto,當(dāng)NGINX服務(wù)器在運行時,只有工作進程在忙碌。每個工作進程都以非阻塞的方式處理多個連接,以削減上下文切換的開銷。每個工作進程都是單線程且獨立運行的,抓取并處理新的連接。進程間通過共享內(nèi)存的方式,來共享緩存數(shù)據(jù)、會話持久性數(shù)據(jù)(session persistence data)和其他共享資源。
NGINX內(nèi)部的工作進程
每一個NGINX的工作進程都是NGINX配置(NGINX configuration)初始化的,并被主進程設(shè)置了一組監(jiān)聽套接字(listen sockets)。
NGINX工作進程會監(jiān)聽套接字上的事件(accept_mutex和kernel socketsharding),來決定什么時候開始工作。事件是由新的連接初始化的。這些連接被會分配給狀態(tài)機(statemachine)—— HTTP狀態(tài)機是最常用的,但NGINX還為流(原生TCP)和大量的郵件協(xié)議(SMTP,IMAP和POP3)實現(xiàn)了狀態(tài)機。
狀態(tài)機本質(zhì)上是一組告知NGINX如何處理請求的指令。大多數(shù)和NGINX具有相同功能的web服務(wù)器也使用類似的狀態(tài)機——只是實現(xiàn)不同。
調(diào)度狀態(tài)機
把狀態(tài)機想象成國際象棋的規(guī)則。每個HTTP事務(wù)(HTTP transaction)都是一局象棋比賽。棋盤的一邊是web服務(wù)器——坐著一位可以迅速做出決定的大師級棋手。另一邊是遠程客戶端——在相對較慢的網(wǎng)絡(luò)中,訪問站點或應(yīng)用程序的web瀏覽器。 然而,比賽的規(guī)則可能會很復(fù)雜。例如,web服務(wù)器可能需要與各方溝通(代理一個上游的應(yīng)用程序),或者和認證服務(wù)器交流。web服務(wù)器的第三方模塊也可以拓展比賽規(guī)則。
阻塞狀態(tài)機
回憶一下我們之前對進程和線程的描述:是一組操作系統(tǒng)可調(diào)度的、運行在CPU內(nèi)核上的獨立指令集。大多數(shù)web服務(wù)器和web應(yīng)用都使用一個連接/一個進程或一個連接/一個線程的模型來進行這局國際象棋比賽。每個進程或線程都包含一個將比賽玩到最后的指令。在這個過程中,進程是由服務(wù)器來運行的,它的大部分時間都花在“阻塞(blocked)”上,等待客戶端完成其下一個動作。
- web服務(wù)器進程(web server process)在監(jiān)聽套接字上,監(jiān)聽新的連接(客戶端發(fā)起的新比賽)
- 一局新的比賽發(fā)起后,進程就開始工作,每一步棋下完后都進入阻塞狀態(tài),等待客戶端走下一步棋
- 一旦比賽結(jié)束,web服務(wù)器進程會看看客戶是否想開始新的比賽(這相當(dāng)于一個存活的連接)。如果連接被關(guān)閉(客戶端離開或者超時),web服務(wù)器進程會回到監(jiān)聽狀態(tài),等待全新的比賽。
記住重要的一點:每一個活躍的HTTP連接(每局象棋比賽)都需要一個專用的進程或線程(一位大師級棋手)。這種架構(gòu)非常易于擴展第三方模塊(“新規(guī)則”)。然而,這里存在著一個巨大的不平衡:一個以文件描述符(file descriptor)和少量內(nèi)存為代表的輕量級HTTP連接,會映射到一個單獨的進程或線程——它們是非常重量級的操作系統(tǒng)對象。這在編程上是方便的,但它造成了巨大的浪費。
NGINX是真正的大師
也許你聽說過車輪表演賽,在比賽中一個象棋大師要在同一時間對付幾十個對手。
這就是NGINX工作進程玩“國際象棋”的方式。每一個工作進程都是一位大師(記住:通常情況下,每個工作進程占用一個CPU內(nèi)核),能夠同時對戰(zhàn)上百棋手(實際上是成千上萬)。
1. 工作進程在監(jiān)聽套接字和連接套接字上等待事件。
2. 事件發(fā)生在套接字上,工作進程會處理這些事件。
監(jiān)聽套接字上的事件意味著:客戶端開始了一局新的游戲。工作進程創(chuàng)建了一個新的連接套接字。
連接套接字上的事件意味著:客戶端移動了棋子。工作進程會迅速響應(yīng)。
工作進程從不會在網(wǎng)絡(luò)上停止,它時時刻刻都在等待其“對手”(客戶端)做出回應(yīng)。當(dāng)它已經(jīng)移動了這局比賽的棋子,它會立即去處理下一局比賽,或者迎接新的對手。
為什么它會比阻塞式多進程的架構(gòu)更快?
NGINX的規(guī)模可以很好地支持每個工作進程上數(shù)以萬計的連接。每個新連接都會創(chuàng)建另一個文件描述符,并消耗工作進程中少量的額外內(nèi)存。每一個連接的額外消耗都很少。NGINX進程可以保持固定的CPU占用率。當(dāng)沒有工作時,上下文切換也較少。
在阻塞式的、一個連接/一個進程的模式中,每個連接需要大量的額外資源和開銷,并且上下文切換(從一個進程到另一個進程)非常頻繁。通過適當(dāng)?shù)南到y(tǒng)調(diào)優(yōu),NGINX能大規(guī)模地處理每個工作進程數(shù)十萬并發(fā)的HTTP連接,并且能在流量高峰期間不丟失任何信息(新比賽開始)。
Nginx 模塊化設(shè)計
高度模塊化的設(shè)計是 Nginx 的架構(gòu)基礎(chǔ)。Nginx 服務(wù)器被分解為多個模塊,每個模塊就是一個功能模塊,只負責(zé)自身的功能,模塊之間嚴格遵循“高內(nèi)聚,低耦合”的原則。
核心模塊
核心模塊是 Nginx 服務(wù)器正常運行必不可少的模塊,提供錯誤日志記錄、配置文件解析、事件驅(qū)動機制、進程管理等核心功能。
標(biāo)準 HTTP 模塊
標(biāo)準 HTTP 模塊提供 HTTP 協(xié)議解析相關(guān)的功能,如:端口配置、網(wǎng)頁編碼設(shè)置、HTTP 響應(yīng)頭設(shè)置等。
可選 HTTP 模塊
可選 HTTP 模塊主要用于擴展標(biāo)準的 HTTP 功能,讓 Nginx 能處理一些特殊的服務(wù),如:Flash 多媒體傳輸、解析 GeoIP 請求、SSL 支持等。
郵件服務(wù)模塊
郵件服務(wù)模塊主要用于支持 Nginx 的郵件服務(wù),包括對 POP3 協(xié)議、IMAP 協(xié)議和 SMTP 協(xié)議的支持。
第三方模塊
第三方模塊是為了擴展 Nginx 服務(wù)器應(yīng)用,完成開發(fā)者自定義功能,如:Json 支持、Lua 支持等。
Nginx的請求方式處理
Nginx 是一個 高性能 的 Web 服務(wù)器,能夠同時處理 大量的并發(fā)請求 。它結(jié)合 多進程機制和 異步機制 ,異步機制使用的是 異步非阻塞方式。
多進程機制
服務(wù)器每當(dāng)收到一個客戶端時,就有 服務(wù)器主進程 ( master process )生成一個 子進程( worker process )出來和客戶端建立連接進行交互,直到連接斷開,該子進程就結(jié)束了。
使用 進程 的好處是 各個進程之間相互獨立 , 不需要加鎖 ,減少了使用鎖對性能造成影響,同時降低編程的復(fù)雜度,降低開發(fā)成本。其次,采用獨立的進程,可以讓 進程互相之間不會影響 ,如果一個進程發(fā)生異常退出時,其它進程正常工作, master 進程則很快啟動新的 worker 進程,確保服務(wù)不會中斷,從而將風(fēng)險降到最低。
缺點是操作系統(tǒng)生成一個 子進程 需要進行 內(nèi)存復(fù)制 等操作,在 資源 和 時間 上會產(chǎn)生一定的開銷。當(dāng)有 大量請求 時,會導(dǎo)致 系統(tǒng)性能下降 。
Nginx事件驅(qū)動模型
在 Nginx 的異步非阻塞機制中, 工作進程在調(diào)用 IO 后,就去處理其他的請求,當(dāng) IO 調(diào)用返回后,會通知該工作進程 。對于這樣的系統(tǒng)調(diào)用,主要使用 Nginx 服務(wù)器的事件驅(qū)動模型來實現(xiàn)。
如上圖所示, Nginx 的 事件驅(qū)動模型 由 事件收集器 、 事件發(fā)送器 和 事件處理器 三部分基本單元組成。
- 事件收集器:負責(zé)收集 worker 進程的各種 IO 請求;
- 事件發(fā)送器:負責(zé)將 IO 事件發(fā)送到 事件處理器 ;
- 事件處理器:負責(zé)各種事件的 響應(yīng)工作 。
事件發(fā)送器將每個請求放入一個 待處理事件列表 ,使用非阻塞 I/O 方式調(diào)用 事件處理器 來處理該請求。其處理方式稱為 “多路 IO 復(fù)用方法” ,常見的包括以下三種: select 模型、 poll模型、 epoll 模型。
Nginx進程處理模型
Nginx 服務(wù)器使用 master/worker 多進程模式 。多線程啟動和執(zhí)行的流程如下:
- 主程序 Master process 啟動后,通過一個 for 循環(huán)來 接收 和 處理外部信號 ;
- 主進程通過 fork() 函數(shù)產(chǎn)生 worker 子進程 ,每個 子進程 執(zhí)行一個 for 循環(huán)來實現(xiàn) Nginx 服務(wù)器 對事件的接收 和 處理 。
一般推薦 worker 進程數(shù) 與 CPU 內(nèi)核數(shù) 一致,這樣一來不存在 大量的子進程 生成和管理任務(wù),避免了進程之間 競爭 CPU 資源 和 進程切換 的開銷。而且 Nginx 為了更好的利用 多核特性 ,提供了 CPU 親緣性 的綁定選項,我們可以將某 一個進程綁定在某一個核 上,這樣就不會因為 進程的切換 帶來 Cache 的失效。
對于每個請求,有且只有一個 工作進程 對其處理。首先,每個 worker 進程都是從 master進程 fork 過來。在 master 進程里面,先建立好需要 listen 的 socket(listenfd) 之后,然后再 fork 出多個 worker 進程。
所有 worker 進程的 listenfd 會在 新連接 到來時變得 可讀 ,為保證只有一個進程處理該連接,所有 worker 進程在注冊 listenfd 讀事件 前 搶占 accept_mutex ,搶到 互斥鎖 的那個進程 注冊 listenfd 讀事件 ,在 讀事件 里調(diào)用 accept 接受該連接。
當(dāng)一個 worker 進程在 accept 這個連接之后,就開始 讀取請求 , 解析請求 , 處理請求 ,產(chǎn)生數(shù)據(jù)后,再 返回給客戶端 ,最后才 斷開連接 ,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由 worker 進程來處理,而且只在一個 worker 進程中處理。
在 Nginx 服務(wù)器的運行過程中, 主進程 和 工作進程 需要進程交互。交互依賴于 Socket 實現(xiàn)的 管道 來實現(xiàn)。
主進程與工作進程交互
這條管道與普通的管道不同,它是由 主進程 指向 工作進程 的 單向管道 ,包含主進程向工作進程發(fā)出的 指令 , 工作進程 ID 等;同時 主進程 與外界通過 信號通信 ;每個 子進程 具備 接收信號 ,并處理相應(yīng)的事件的能力。
工作進程與工作進程交互
這種交互是和 主進程-工作進程 交互是基本一致的,但是會通過 主進程 間接完成。 工作進程之間是 相互隔離 的,所以當(dāng)工作進程 W1 需要向工作進程 W2 發(fā)指令時,首先找到 W2 的 進程ID ,然后將正確的指令寫入指向 W2 的 通道 。 W2 收到信號采取相應(yīng)的措施。
作者: hukey
出處:https://www.cnblogs.com/hukey/p/10443898.html
版權(quán):本作品采用「署名-非商業(yè)性使用-相同方式共享 4.0 國際」許可協(xié)議進行許可。