[導(dǎo)讀]“Nginx(enginex)是一個高性能的HTTP和反向代理Web服務(wù)器,同時也提供了IMAP/POP3/SMTP服務(wù)。圖片來自PexelsNginx以高性能和高可用性備受廣大程序員的青睞,今天我們會從Nginx的整體架構(gòu)入手,介紹Nginx進程結(jié)構(gòu),進程之間的關(guān)系以及如何對進...
“ Nginx(engine x)是一個高性能的 HTTP 和反向代理 Web 服務(wù)器,同時也提供了 IMAP/POP3/SMTP 服務(wù)。
圖片來自 Pexels
Nginx 以高性能和高可用性備受廣大程序員的青睞,今天我們會從 Nginx 的整體架構(gòu)入手,介紹 Nginx 進程結(jié)構(gòu),進程之間的關(guān)系以及如何對進程進行控制和管理。
今天大家會學(xué)到如下內(nèi)容:
-
Nginx 總體架構(gòu)
-
Nginx 進程定義
-
Nginx 啟動過程
-
Master 啟動過程
-
進程之間的信號發(fā)送方式
-
進程協(xié)助處理網(wǎng)絡(luò)請求
Nginx 總體架構(gòu)
對于傳統(tǒng)的 HTTP 和反向代理服務(wù)器而言,在處理并發(fā)請求的時候會使用單進程或線程的模式處理,同時會止網(wǎng)絡(luò)或輸入/輸出操作。
這種方式會消耗大量的內(nèi)存和 CPU 資源。因為每產(chǎn)生一個單獨的進程或線程需要準(zhǔn)備一套新的運行時環(huán)境,包括分配堆和堆棧內(nèi)存,以及創(chuàng)建新的執(zhí)行上下文。
可以想象在處理多請求時會生成對應(yīng)數(shù)目的線程或進程,導(dǎo)致由于線程在不斷上下文切換上耗費大量資源。
由于上面的原因,Nginx 在設(shè)計之初就使用了模塊化、事件驅(qū)動、異步處理,非阻塞的架構(gòu)。
圖 1:Nginx 總體架構(gòu)
讓我們通過一張圖來了解 Nginx 的總結(jié)架構(gòu),如圖 1 所示:
①Nginx 啟動時,并不會馬上處理網(wǎng)絡(luò)請求,負(fù)責(zé)調(diào)度工作進程。包括 Load configuration(加載配置),Launch workers(啟動工作進程)以及 Non-stop upgrade(平滑升級)。
因此在 Nginx 啟動以后,在操作系統(tǒng)中會看到 Master 和 Worker 兩類進程。在圖上方的 Master 進程負(fù)責(zé)加載分析配置文件、啟動/管理 Worker 進程以及平滑升級。
一個 Master 進程可以管理多個 Worker 進程,而 Worker 進程負(fù)責(zé)處理并響應(yīng)用戶請求,也就是來自圖左邊的 HTTP/HTTPS 請求。
②由于網(wǎng)絡(luò)請求屬于 IO 請求,為了應(yīng)對高并發(fā) Nginx 采取了 kevent/epoll/ select 模式的多路復(fù)用技術(shù)。由于采取了這種技術(shù)每個 Worker 進程都可以同時處理數(shù)以千計的網(wǎng)絡(luò)請求。
③為了處理網(wǎng)絡(luò)請求在 Worker 中會包含模塊,分為核心模塊和功能性模塊。
核心模塊負(fù)責(zé)維持一個運行循環(huán)(run-loop),執(zhí)行網(wǎng)絡(luò)請求處理的不同階段的模塊功能,如網(wǎng)絡(luò)讀寫、存儲讀寫、內(nèi)容傳輸、外出過濾,以及將請求發(fā)往上游服務(wù)器等。
而圍繞著核心模塊會有一些功能模塊,就是實現(xiàn)具體的請求處理功能的。例如有處理 http 請求的 ht_core 模塊,有實現(xiàn)負(fù)載均衡的 ht_upstream 模塊,以及實現(xiàn) FastCGI 的 ht_fastcgi 模塊。
這些模塊會負(fù)責(zé)與后端的服務(wù)器進行交互,完成用戶請求,同時可以根據(jù)需要的功能自由加載模塊,甚至可以擴展第三方的模塊。
④Worker 進程可以和本地磁盤進行數(shù)據(jù)通信,支持 Advanced I/O(高級I/O)、sendfile 機制、AIO 機制、mmap 等機制等。
通過上面的介紹發(fā)現(xiàn) Nginx 不會為每個連接生成一個進程或線程,而是通過 Worker 進程使用多路復(fù)用的方式處理多個請求。
這里會使用到共享監(jiān)聽套接字的方式接受新請求,并在每個 Worker 內(nèi)執(zhí)行高效的運行循環(huán),從而達(dá)到每個 Worker 處理成千上萬的連接。
Work 啟動后將創(chuàng)建一組偵聽套接字,并且在處理 HTTP 請求和響應(yīng)過程中不斷接受,讀取和寫入套接字信息。
運行循環(huán)(run-loop)包括全面的內(nèi)部調(diào)用,并且在很大程度上依賴異步任務(wù)處理。
通過模塊化,事件通知,回調(diào)函數(shù)和定時器來支撐異步操作的實現(xiàn)。其目的是為了實現(xiàn)高并發(fā)請求下的不阻塞(盡可能不阻賽)。
基于上面的機制,Nginx 通過檢查網(wǎng)絡(luò)和存儲的狀態(tài)并初始化新連接,將其添加到運行循環(huán)中,并異步處理直到其完成,處理完畢的連接會被重新分配并從運行循環(huán)中刪除。因此 Nginx 可以在極端工作負(fù)載下實現(xiàn)較低的CPU使用率。
另外,由于 Work 會在磁盤上進行寫入操作,為了避免磁盤 I/O 上的阻塞請求,特別是磁盤滿的情況。
可以設(shè)置機制和配置文件指令來減輕此類磁盤 I/O 阻塞情況的發(fā)生。例如使用 sendfile 和 AIO 組合選項提升磁盤性能。
Nginx 進程定義
從架構(gòu)介紹我們知道 Nginx 是由不同的進程組成的,這些進程各司其職用來處理高并發(fā)下的網(wǎng)絡(luò)請求,接下來就看看他們的定義和如何工作的。
上面介紹了 Nginx 的總體架構(gòu),其中重點提到了 Master 進程和 Worker 進程,其實還有另外兩個進程在架構(gòu)中也起到了重要的作用。
圖 2:Nginx 的四類進程
這里我們一起通過圖 2 來認(rèn)識他們:
-
Master 進程:它作為父進程會在 Nginx 初始化的時候生成并啟動,其他的進程都是它子進程,Master 進程對其他進行進行創(chuàng)建和管理。
-
Worker 進程:是 Master 的子進程負(fù)責(zé)處理網(wǎng)絡(luò)請求。這里需要說明一下 Nginx 為什么采用了多進程而不是多線程的結(jié)構(gòu),其原因是為了保證高可用性,進程不像線程那樣共享地址空間,也避免了當(dāng)一個線程中的第三方模塊出錯引而影響其他其他線程的情況發(fā)生。
-
Cache Manager 和 Cache Loader 進程:Cache Loader 負(fù)責(zé)緩存載入,Cache Manager 負(fù)責(zé)緩存管理,每一個請求所使用的緩存還是由 Worker 來決定的,而進程間通信都是通過共享內(nèi)存來實現(xiàn)的。
從上圖大家一定注意到了只有 Worker 進程是多個,這是因為 Nginx 采用了事件驅(qū)動的模型。
為了提高處理請求的效率每個 Worker 進程會找那個一個 CPU內(nèi)核,提高 CPU 的緩存命中率,將某個 Worker 進程與一個 CPU 核綁定在一起。
需要說明的是,我們需要根據(jù)具體的應(yīng)用場景來定義 Worker 進程的數(shù)量:
-
CPU 密集型請求,例如,處理大量 TCP/IP,執(zhí)行 SSL 或壓縮,需要 Worker 數(shù)與 CPU 內(nèi)核數(shù)量相匹配。
-
IO 密集型請求,Worker 數(shù)需要是 CPU 內(nèi)核數(shù)量的一到兩倍。
上面提到了 Master 通過控制多個 Worker 進程來處理網(wǎng)絡(luò)請求,對于 Worker 的獨立進程來說使用資源的時候不需要考慮不需要加鎖的問題,節(jié)省了因為加鎖帶來的系統(tǒng)開銷。同時多進程的設(shè)計讓進程之間不會互相影響。
當(dāng)一個進程退出后,其它進程還在工作,Nginx 所提供的網(wǎng)絡(luò)請求服務(wù)不會因為其中一個進程的退出而中斷,Master 進程一旦發(fā)現(xiàn)有 Worker 進程退出會啟動新的 Worker 進程。
這里我們會發(fā)現(xiàn) Master 進程為了控制 Worker 需要對其進行通信,同時 Worker 進程也需要與 Master 進程交換信息。
Nginx 啟動過程
談到了 Master 進程如此的重要,那么一起來看看 Nginx 進程的啟動過程。
圖 3:Nginx 啟動 Master 進程
如圖 3 所示,在 Nginx 啟動的時候會根據(jù)配置文件進行解析和初始化的工作,同時會從主進程中 fork 出一個 Master 進程作為自己的子進程,也就是啟動 Master 進程,此時 Master 進程就誕生了。
Nginx 的主進程在 fork 出 Master 進程以后就退出了。然后 Master 進程會 fork 并啟動 Worker 進程,以及 Cache Manager 、Cache Loader 進程,接著 Master 進程會進入主循環(huán)。
需要注意的是,這里使用的 fork 會復(fù)制一個和當(dāng)前啟動進程具有相同代碼段、數(shù)據(jù)段、堆和棧、fd 等信息的子進程。
也就是說我們說的四類進程都是通過 Nginx 啟動進程復(fù)制出來的子進程。
Master 啟動過程
接著上面的流程 Master 進程被 fork 之后,會執(zhí)行 ngx_master_process_cycle 函數(shù)。
圖 4:Master 進程執(zhí)行 ngx_master_process_cycle 函數(shù)
如圖 4 所示,這個函數(shù)主要進行如下操作:
-
設(shè)置進程的初始信號掩碼,屏蔽相關(guān)信號。
-
Master 進程 fork 出 Worker 、Cache Manager 以及 Cache Loader 等子進程。
-
進入主循環(huán),通過 sigsuspend 系統(tǒng)調(diào)用,等待著信號的到來。
-
一旦信號到來,會進入信號處理程序 ngx_signal_handler。
-
信號處理程序執(zhí)行之后,程序執(zhí)行流程會判斷各種狀態(tài)位,來執(zhí)行不同的操作。
上面的流程中提到了幾個概念,這里對其進行說明,以便我們更好的理解 Master 進程的執(zhí)行過程。
①信號:用來完成進程中信息傳遞的媒介。Master 進程的主循環(huán)里面,一直通過等待各種信號事件,來處理不同的指令。
這個信號可以傳遞給 Master 進程,也可以從 Master 進程傳遞給其他的進程。
信號分為標(biāo)準(zhǔn)信號和實時信號,標(biāo)準(zhǔn)信號是從 1-31,實時信號是從 32-64。例如:INT、QUIT、KILL 就是標(biāo)準(zhǔn)信號。Master 進程監(jiān)聽的信號也是標(biāo)準(zhǔn)信號。
標(biāo)準(zhǔn)信號和實時信號的區(qū)別是:標(biāo)準(zhǔn)信號,是基于位的標(biāo)記,假設(shè)在阻塞等待的時候,多個相同的信號到來,最終解除阻塞時,只會傳遞一次信號,無法統(tǒng)計等待期間信號的計數(shù)。
而實時信號是通過隊列來實現(xiàn),在阻塞等待的時候,多個相同的實時信號會存放到隊列中。一旦解除阻塞的時候,會將隊列中的信號都進行傳遞,結(jié)果會收到多次信號。
②信號處理器:信號處理器是指當(dāng)捕獲指定信號時(傳遞給進程)時將會調(diào)用的一個函數(shù),它存在與進程中,它可以隨時打斷進程的主程序流程。
③發(fā)送信號:發(fā)送信號的操作可以使用 kill 這個 shell 命令完成。比如 kill -9 pid,就是發(fā)送 KILL 信號。
kill -INT pid 就是發(fā)送 INT 信號。與 shell 命令類似,可以使用 kill 系統(tǒng)調(diào)用來向進程發(fā)送信號。
④信號掩碼:用來控制信號阻賽的編碼方式。每個進程都有一個信號掩碼(signal mask),也稱為信號屏蔽字,它規(guī)定了當(dāng)前要屏蔽或要阻塞遞送到該進程的信號集。
對于每種可能的信號,該掩碼中都有一位與之對應(yīng)。對于某種信號,若其對應(yīng)位(bit)已設(shè)置,則它當(dāng)前是被阻塞的。
簡單地說,信號掩碼是一個“位圖”,其中每一位都對應(yīng)著一種信號。如果位圖中的某一位為 1,就表示在執(zhí)行當(dāng)前信號集的處理程序期間相應(yīng)的信號暫時被“屏蔽”或“阻塞”,使得在執(zhí)行的過程中不會嵌套地響應(yīng)那個信號。
說白了就是使用信號編碼來阻賽信號,告訴其他發(fā)送信號的進程說:“我在忙著處理事情,你先等等,等會我再處理你的信號。”
進程之間的信號發(fā)送方式
有了上面的基礎(chǔ)以后再回頭看看 Nginx 中進程中是如何進行信息交互的,以及 Master 是如何通過信號與 Worker 進行溝通的。
圖 5:Master 與 Worker 通信
從圖 5 中可以看出,Master 可以接受 TERM,INT、QUIT、HUP、USR1、USR2、WINCH 這些信號,這些信號的含義會在后面提供一張表給大家解釋。
同時 Master 進程也可以給Worker進程傳遞信號,于是 Worker 進程可以接收以下信號:TERM,INT、QUIT、HUP、USR1、WINCH。之后 Worker 再去響應(yīng) Client 的請求。
這里先將信號的對應(yīng)的命令和含義通過表格的方式列出來,再來對其進行講解。
從表格上面看,每個信號都有特定的含義。比如 QUIT 信號表示優(yōu)雅地關(guān)閉服務(wù),并且對應(yīng) quit 命令。
這里的 quit 命令指的是可以通過命令行的方式對 Master 進程下命令,從而達(dá)到發(fā)送信號的效果,當(dāng)然 Master 接受到命令以后會轉(zhuǎn)化為信號發(fā)送給對應(yīng)的 Worker 達(dá)到關(guān)閉服務(wù)的效果。
需要說明的是,Worker 是不會接受命令的,而是通過 Worker 接受命令來統(tǒng)一管理所有 Worker 的行為。
進程協(xié)助處理網(wǎng)絡(luò)請求
知道 Master 與 Worker 之間如何通信之后再來看看它們是如何合作完成客戶端請求的。
圖 6:Client 請求流程
如圖 6 所示,這里描繪了 Master 創(chuàng)建 Listen 以及 fork 出 Worker 的過程,以及客戶端請求和 Worker 響應(yīng)請求的過程。
①先從最上面開始看,順著從上至下紅色的箭頭看,Master 進程創(chuàng)建以后會通過 socket 方法創(chuàng)建 socket 的 IO 通道。
接著執(zhí)行 bind 方法將其與監(jiān)聽器 listen 進行綁定,然后通過 fork 方法 fork 出多個 Worker 進程(綠色虛線)。
②在每個 Worker 進程中的 accept 方法就監(jiān)聽 socket 請求了,一旦 listen 監(jiān)聽到 socket 請求 Worker 進程就可以通過 accept 接受到。
③再看最下面的 client 模塊,當(dāng) client 通過 connect 方法與 Nginx 發(fā)生連接時,所有擁有 accept 方法的 Worker 進程都會接受到來自 listen 的通知,但是只有一個 Worker 進程能夠成功 accept 到,其他的進程則會失敗。
這里 Nginx 提供了一把共享鎖 accept_mutex 來保證同一時刻只有一個 Worker 進程在 accept 連接,從而解決驚群問題。
④當(dāng) Worker 進程 accept 到 socket 請求以后,client 會通過 send 方法發(fā)送請求(綠色虛線)給 Worker。
而 Worker 使用 recv 方法接受請求你,同時通過 parse(解析)、process(處理)、generate(生成響應(yīng))幾個步驟將返回的響應(yīng)通過 send 方法傳送給 client,而 client 會使用 recv 方法接受響應(yīng)。
最后 Worker 調(diào)用 close 方法斷開和 client 的連接。
總結(jié)
本文從 Nginx 總體架構(gòu)開始,介紹了 Nginx 的主要組成部分和處理流程。然后介紹 Nginx 的 4 個進程,以及 Nginx 在啟動過程中這些進程都是如何產(chǎn)生的。
然后聚焦到最為主要的 Master 進程的啟動過程做了哪些具體的事情,特別是 Master 進程和 Worker、Cache Manager、Cache Load 之間的關(guān)系。
在進程之間的信號發(fā)送方式的章節(jié)中,我們建立了信號、發(fā)送信號、信號處理、信號掩碼的概念,這有助于理解進程之間的通信。
最后,趁熱打鐵把 Nginx 接受網(wǎng)絡(luò)請求以及進程之間如何合作處理請求的過程進行了講解。
作者:崔皓
簡介:十六年開發(fā)和架構(gòu)經(jīng)驗,曾擔(dān)任過惠普武漢交付中心技術(shù)專家,需求分析師,項目經(jīng)理,后在創(chuàng)業(yè)公司擔(dān)任技術(shù)/產(chǎn)品經(jīng)理。善于學(xué)習(xí),樂于分享。目前專注于技術(shù)架構(gòu)與研發(fā)管理。
本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。