NGINX 能在 web 性能中取得領(lǐng)先地位,這是由于其軟件設(shè)計所決定的。許多 web 服務(wù)器和應(yīng)用程序服務(wù)器使用一個簡單的基于線程或進程的架構(gòu),NGINX 立足于一個復(fù)雜的事件驅(qū)動的體系結(jié)構(gòu),使它能夠在現(xiàn)代硬件上擴展到成千上萬的并發(fā)連接。
這張深入 NGINX 的信息圖從高層次的進程架構(gòu)上深度挖掘說明了 NGINX 如何在單一進程里保持多個連接。這篇博客進一步詳細地解釋了這一切是如何工作的。
知識 – NGINX 進程模型
為了更好的理解這個設(shè)計,你需要理解 NGINX 如何運行的。NGINX 有一個主進程(它執(zhí)行特權(quán)操作,如讀取配置和綁定端口)和一些工作進程與輔助進程。
# service nginx restart
* Restarting nginx
# ps -ef --forest | grep nginx
root 32475 1 0 13:36 ? 00:00:00 nginx: master process /usr/sbin/nginx \
-c /etc/nginx/nginx.conf
nginx 32476 32475 0 13:36 ? 00:00:00 \_ nginx: worker process
nginx 32477 32475 0 13:36 ? 00:00:00 \_ nginx: worker process
nginx 32479 32475 0 13:36 ? 00:00:00 \_ nginx: worker process
nginx 32480 32475 0 13:36 ? 00:00:00 \_ nginx: worker process
nginx 32481 32475 0 13:36 ? 00:00:00 \_ nginx: cache manager process
nginx 32482 32475 0 13:36 ? 00:00:00 \_ nginx: cache loader process
在四核服務(wù)器,NGINX 主進程創(chuàng)建了4個工作進程和兩個管理磁盤內(nèi)容緩存的緩存輔助進程。
為什么架構(gòu)很重要?
任何 Unix 應(yīng)用程序的根本基礎(chǔ)是線程或進程。(從 Linux 操作系統(tǒng)的角度來看,線程和進程大多是相同的,主要的區(qū)別是他們共享內(nèi)存的程度。)一個線程或進程是一個自包含的指令集,操作系統(tǒng)可以在一個 CPU 核心上調(diào)度運行它們。大多數(shù)復(fù)雜的應(yīng)用程序并行運行多個線程或進程有兩個原因:
它們可以同時使用更多的計算核心。
線程或進程可以輕松實現(xiàn)并行操作。(例如,在同一時刻保持多連接)。
進程和線程消耗資源。他們每個都使用內(nèi)存和其他系統(tǒng)資源,他們會在 CPU 核心中換入和換出(這個操作叫做上下文切換)。大多數(shù)現(xiàn)代服務(wù)器可以并行保持上百個小型的、活動的線程或進程,但是一旦內(nèi)存耗盡或高 I/O 壓力引起大量的上下文切換會導(dǎo)致性能嚴重下降。
網(wǎng)絡(luò)應(yīng)用程序設(shè)計的常用方法是為每個連接分配一個線程或進程。此體系結(jié)構(gòu)簡單、容易實現(xiàn),但是當(dāng)應(yīng)用程序需要處理成千上萬的并發(fā)連接時這種結(jié)構(gòu)就不具備擴展性。
NGINX 如何工作?
NGINX 使用一種可預(yù)測的進程模式來分配可使用的硬件資源:
主進程(master)執(zhí)行特權(quán)操作,如讀取配置和綁定端口,然后創(chuàng)建少量的子進程(如下的三種類型)。
緩存加載器進程(cache loader)在加載磁盤緩存到內(nèi)存中時開始運行,然后退出。適當(dāng)?shù)恼{(diào)度,所以其資源需求很低。
緩存管理器進程(cache manager)定期裁剪磁盤緩存中的記錄來保持他們在配置的大小之內(nèi)。
工作進程(worker)做所有的工作!他們保持網(wǎng)絡(luò)連接、讀寫內(nèi)容到磁盤,與上游服務(wù)器通信。
在大多數(shù)情況下 NGINX 的配置建議:每個 CPU 核心運行一個工作進程,這樣最有效地利用硬件資源。你可以在配置中包含worker_processes auto指令配置:
worker_processes auto;
當(dāng)一個 NGINX 服務(wù)處于活動狀態(tài),只有工作進程在忙碌。每個工作進程以非阻塞方式保持多連接,以減少上下文交換。
每個工作進程是一個單一線程并且獨立運行,它們會獲取新連接并處理之。這些進程可以使用共享內(nèi)存通信來共享緩存數(shù)據(jù)、會話持久性數(shù)據(jù)及其它共享資源。(在 NGINX 1.7.11 及其以后版本,還有一個可選的線程池,工作進程可以轉(zhuǎn)讓阻塞的操作給它。更多的細節(jié),參見“NGINX 線程池可以爆增9倍性能!”。對于 NGINX Plus 用戶,該功能計劃在今年晚些時候加入到 R7 版本中。)
NGINX 工作進程內(nèi)部
每個 NGINX 工作進程按照 NGINX 配置初始化,并由主進程提供一組監(jiān)聽端口。
NGINX 工作進程首先在監(jiān)聽套接字上等待事件(accept_mutex 和內(nèi)核套接字分片)。事件被新進來的連接初始化。這些連接被分配到一個狀態(tài)機 – HTTP 狀態(tài)機是最常用的,但 NGINX 也實現(xiàn)了流式(原始 TCP )狀態(tài)機和幾種郵件協(xié)議(SMTP、IMAP和POP3)的狀態(tài)機。
狀態(tài)機本質(zhì)上是一組指令,告訴 NGINX 如何處理一個請求。大多數(shù) web 服務(wù)器像 NGINX 一樣使用類似的狀態(tài)機來實現(xiàn)相同的功能 - 區(qū)別在于實現(xiàn)。
調(diào)度狀態(tài)機
把狀態(tài)機想象成國際象棋的規(guī)則。每個 HTTP 事務(wù)是一個象棋游戲。一方面棋盤是 web 服務(wù)器 —— 一位大師可以非常迅速地做出決定。另一方面是遠程客戶端 —— 在一個相對較慢的網(wǎng)絡(luò)下 web 瀏覽器訪問網(wǎng)站或應(yīng)用程序。
不管怎樣,這個游戲規(guī)則很復(fù)雜。例如,web 服務(wù)器可能需要與各方溝通(代理一個上游的應(yīng)用程序)或與身份驗證服務(wù)器對話。web 服務(wù)器的第三方模塊甚至可以擴展游戲規(guī)則。
一個阻塞狀態(tài)機
回憶我們之前的描述,一個進程或線程就像一套獨立的指令集,操作系統(tǒng)可以在一個 CPU 核心上調(diào)度運行它。大多數(shù) web 服務(wù)器和 web 應(yīng)用使用每個連接一個進程或者每個連接一個線程的模式來玩這個“象棋游戲”。每個進程或線程都包含玩完“一個游戲”的指令。在服務(wù)器運行該進程的期間,其大部分的時間都是“阻塞的” —— 等待客戶端完成它的下一步行動。
web 服務(wù)器進程在監(jiān)聽套接字上監(jiān)聽新連接(客戶端發(fā)起新“游戲”)
當(dāng)它獲得一個新游戲,就玩這個游戲,每走一步去等待客戶端響應(yīng)時就阻塞了。
游戲完成后,web 服務(wù)器進程可能會等待是否有客戶機想要開始一個新游戲(這里指的是一個“保持的”連接)。如果這個連接關(guān)閉了(客戶端斷開或者發(fā)生超時),web 服務(wù)器進程會返回并監(jiān)聽一個新“游戲”。
要記住最重要的一點是每個活動的 HTTP 連接(每局棋)需要一個專用的進程或線程(象棋高手)。這個結(jié)構(gòu)簡單容并且易擴展第三方模塊(“新規(guī)則”)。然而,還是有巨大的不平衡:尤其是輕量級 HTTP 連接其實就是一個文件描述符和小塊內(nèi)存,映射到一個單獨的線程或進程,這是一個非常重量級的系統(tǒng)對象。這種方式易于編程,但太過浪費。
NGINX是一個真正的象棋大師
也許你聽過車輪表演賽游戲,有一個象棋大師同時對戰(zhàn)許多對手?
列夫·吉奧吉夫在保加利亞的索非亞同時對陣360人。他的最終成績是284勝70平6負。
這就是 NGINX 工作進程如何“下棋”的。每個工作進程(記住 - 通常每個CPU核心上有一個工作進程)是一個可同時對戰(zhàn)上百人(事實是,成百上千)的象棋大師。
工作進程在監(jiān)聽和連接套接字上等待事件。
事件發(fā)生在套接字上,并且由工作進程處理它們:
在監(jiān)聽套接字的事件意味著一個客戶端已經(jīng)開始了一局新棋局。工作進程創(chuàng)建了一個新連接套接字。
在連接套接字的事件意味著客戶端已經(jīng)下了一步棋。工作進程及時響應(yīng)。
一個工作進程在網(wǎng)絡(luò)流量上從不阻塞,等待它的“對手”(客戶端)做出反應(yīng)。當(dāng)它下了一步,工作進程立即繼續(xù)其他的游戲,在那里工作進程正在處理下一步,或者在門口歡迎一個新玩家。
為什么這個比阻塞式多進程架構(gòu)更快?
NGINX 每個工作進程很好的擴展支撐了成百上千的連接。每個連接在工作進程中創(chuàng)建另外一個文件描述符和消耗一小部分額外內(nèi)存。每個連接有很少的額外開銷。NGINX 進程可以固定在某個 CPU 上。上下文交換非常罕見,一般只發(fā)生在沒有工作要做時。
在阻塞方式,每個進程一個連接的方法中,每個連接需要大量額外的資源和開銷,并且上下文切換(從一個進程切換到另一個)非常頻繁。
更詳細的解釋,看看這篇關(guān)于 NGINX 架構(gòu)的文章,它由NGINX公司開發(fā)副總裁及共同創(chuàng)始人 Andrew Alexeev 寫的。
通過適當(dāng)?shù)南到y(tǒng)優(yōu)化,NGINX 的每個工作進程可以擴展來處理成千上萬的并發(fā) HTTP 連接,并能臉不紅心不跳的承受峰值流量(大量涌入的新“游戲”)。
更新配置和升級 NGINX
NGINX 的進程體系架構(gòu)使用少量的工作進程,有助于有效的更新配置文件甚至 NGINX 程序本身。
更新 NGINX 配置文件是非常簡單、輕量、可靠的操作。典型的就是運行命令 nginx –s reload,所做的就是檢查磁盤上的配置并發(fā)送 SIGHUP 信號給主進程。
當(dāng)主進程接收到一個 SIGHUP 信號,它會做兩件事:
重載配置文件和分支出一組新的工作進程。這些新的工作進程立即開始接受連接和處理流量(使用新的配置設(shè)置)
通知舊的工作進程優(yōu)雅的退出。工作進程停止接受新的連接。當(dāng)前的 http 請求一旦完成,工作進程就徹底關(guān)閉這個連接(那就是,沒有殘存的“保持”連接)。一旦所有連接關(guān)閉,這個工作進程就退出。
這個重載過程能引發(fā)一個 CPU 和內(nèi)存使用的小峰值,但是跟活動連接加載的資源相比它一般不易察覺。每秒鐘你可以多次重載配置(很多 NGINX 用戶都這么做)。非常罕見的情況下,有很多世代的工作進程等待關(guān)閉連接時會發(fā)生問題,但即使是那樣也很快被解決了。
NGINX 的程序升級過程中拿到了高可用性圣杯 —— 你可以隨時更新這個軟件,不會丟失連接,停機,或者中斷服務(wù)。
程序升級過程類似于平滑重載配置的方法。一個新的 NGINX 主進程與原主進程并行運行,然后他們共享監(jiān)聽套接字。兩個進程都是活動的,并且各自的工作進程處理流量。然后你可以通知舊的主進程和它的工作進程優(yōu)雅的退出。
整個過程的詳細描述在 NGINX 管理。
結(jié)論
深入 NGINX 信息圖提供一個 NGINX 功能實現(xiàn)的高層面概覽,但在這簡單的解釋的背后是超過十年的創(chuàng)新和優(yōu)化,使得 NGINX 在廣泛的硬件上提供盡可能最好的性能同時保持了現(xiàn)代 Web 應(yīng)用程序所需要的安全性和可靠性。