初探 Nginx 架構

2022-03-23 14:59 更新

眾所周知,Nginx 性能高,而 Nginx 的高性能與其架構是分不開的。那么 Nginx 究竟是怎么樣的呢?這一節(jié)我們先來初識一下 Nginx 框架吧。

Nginx 在啟動后,在 unix 系統(tǒng)中會以 daemon (守護進程)的方式在后臺運行,后臺進程包含一個 master 進程和多個 worker 進程。我們也可以手動地關掉后臺模式,讓 Nginx 在前臺運行,并且通過配置讓 Nginx 取消 master 進程,從而可以使 Nginx 以單進程方式運行。很顯然,生產(chǎn)環(huán)境下我們肯定不會這么做,所以關閉后臺模式,一般是用來調試用的,在后面的章節(jié)里面,我們會詳細地講解如何調試 Nginx。所以,我們可以看到,Nginx 是以多進程的方式來工作的,當然 Nginx 也是支持多線程的方式的,只是我們主流的方式還是多進程的方式,也是 Nginx 的默認方式。Nginx 采用多進程的方式有諸多好處,所以我就主要講解 Nginx 的多進程模式吧。

剛才講到,Nginx 在啟動后,會有一個 master 進程和多個 worker 進程。master 進程主要用來管理 worker 進程,包含:接收來自外界的信號,向各 worker 進程發(fā)送信號,監(jiān)控 worker 進程的運行狀態(tài),當 worker 進程退出后(異常情況下),會自動重新啟動新的 worker 進程。而基本的網(wǎng)絡事件,則是放在 worker 進程中來處理了。多個 worker 進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個 worker 進程中處理,一個 worker 進程,不可能處理其它進程的請求。worker 進程的個數(shù)是可以設置的,一般我們會設置與機器cpu核數(shù)一致,這里面的原因與 Nginx 的進程模型以及事件處理模型是分不開的。Nginx 的進程模型,可以由下圖來表示:

在 Nginx 啟動后,如果我們要操作 Nginx,要怎么做呢?從上文中我們可以看到,master 來管理 worker 進程,所以我們只需要與 master 進程通信就行了。master 進程會接收來自外界發(fā)來的信號,再根據(jù)信號做不同的事情。所以我們要控制 Nginx,只需要通過 kill 向 master 進程發(fā)送信號就行了。比如kill -HUP pid,則是告訴 Nginx,從容地重啟 Nginx,我們一般用這個信號來重啟 Nginx,或重新加載配置,因為是從容地重啟,因此服務是不中斷的。master 進程在接收到 HUP 信號后是怎么做的呢?首先 master 進程在接到信號后,會先重新加載配置文件,然后再啟動新的 worker 進程,并向所有老的 worker 進程發(fā)送信號,告訴他們可以光榮退休了。新的 worker 在啟動后,就開始接收新的請求,而老的 worker 在收到來自 master 的信號后,就不再接收新的請求,并且在當前進程中的所有未處理完的請求處理完成后,再退出。當然,直接給 master 進程發(fā)送信號,這是比較老的操作方式,Nginx 在 0.8 版本之后,引入了一系列命令行參數(shù),來方便我們管理。比如,./nginx -s reload,就是來重啟 Nginx,./nginx -s stop,就是來停止 Nginx 的運行。如何做到的呢?我們還是拿 reload 來說,我們看到,執(zhí)行命令時,我們是啟動一個新的 Nginx 進程,而新的 Nginx 進程在解析到 reload 參數(shù)后,就知道我們的目的是控制 Nginx 來重新加載配置文件了,它會向 master 進程發(fā)送信號,然后接下來的動作,就和我們直接向 master 進程發(fā)送信號一樣了。

現(xiàn)在,我們知道了當我們在操作 Nginx 的時候,Nginx 內部做了些什么事情,那么,worker 進程又是如何處理請求的呢?我們前面有提到,worker 進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供 80 端口的 http 服務時,一個連接請求過來,每個進程都有可能處理這個連接,怎么做到的呢?首先,每個 worker 進程都是從 master 進程 fork 過來,在 master 進程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多個 worker 進程。所有 worker 進程的 listenfd 會在新連接到來時變得可讀,為保證只有一個進程處理該連接,所有 worker 進程在注冊 listenfd 讀事件前搶 accept_mutex,搶到互斥鎖的那個進程注冊 listenfd 讀事件,在讀事件里調用 accept 接受該連接。當一個 worker 進程在 accept 這個連接之后,就開始讀取請求,解析請求,處理請求,產(chǎn)生數(shù)據(jù)后,再返回給客戶端,最后才斷開連接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由 worker 進程來處理,而且只在一個 worker 進程中處理。

那么,Nginx 采用這種進程模型有什么好處呢?當然,好處肯定會很多了。首先,對于每個 worker 進程來說,獨立的進程,不需要加鎖,所以省掉了鎖帶來的開銷,同時在編程以及問題查找時,也會方便很多。其次,采用獨立的進程,可以讓互相之間不會影響,一個進程退出后,其它進程還在工作,服務不會中斷,master 進程則很快啟動新的 worker 進程。當然,worker 進程的異常退出,肯定是程序有 bug 了,異常退出,會導致當前 worker 上的所有請求失敗,不過不會影響到所有請求,所以降低了風險。當然,好處還有很多,大家可以慢慢體會。

上面講了很多關于 Nginx 的進程模型,接下來,我們來看看 Nginx 是如何處理事件的。

有人可能要問了,Nginx 采用多 worker 的方式來處理請求,每個 worker 里面只有一個主線程,那能夠處理的并發(fā)數(shù)很有限啊,多少個 worker 就能處理多少個并發(fā),何來高并發(fā)呢?非也,這就是 Nginx 的高明之處,Nginx 采用了異步非阻塞的方式來處理請求,也就是說,Nginx 是可以同時處理成千上萬個請求的。想想 apache 的常用工作方式(apache 也有異步非阻塞版本,但因其與自帶某些模塊沖突,所以不常用),每個請求會獨占一個工作線程,當并發(fā)數(shù)上到幾千時,就同時有幾千的線程在處理請求了。這對操作系統(tǒng)來說,是個不小的挑戰(zhàn),線程帶來的內存占用非常大,線程的上下文切換帶來的 cpu 開銷很大,自然性能就上不去了,而這些開銷完全是沒有意義的。

為什么 Nginx 可以采用異步非阻塞的方式來處理呢,或者異步非阻塞到底是怎么回事呢?我們先回到原點,看看一個請求的完整過程。首先,請求過來,要建立連接,然后再接收數(shù)據(jù),接收數(shù)據(jù)后,再發(fā)送數(shù)據(jù)。具體到系統(tǒng)底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操作,如果不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續(xù)吧。阻塞調用會進入內核等待,cpu 就會讓出去給別人用了,對單線程的 worker 來說,顯然不合適,當網(wǎng)絡事件越多時,大家都在等待呢,cpu 空閑下來沒人用,cpu利用率自然上不去了,更別談高并發(fā)了。好吧,你說加進程數(shù),這跟apache的線程模型有什么區(qū)別,注意,別增加無謂的上下文切換。所以,在 Nginx 里面,最忌諱阻塞的系統(tǒng)調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,馬上返回 EAGAIN,告訴你,事件還沒準備好呢,你慌什么,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了為止,在這期間,你就可以先去做其它事情,然后再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態(tài),你可以做更多的事情了,但帶來的開銷也是不小的。所以,才會有了異步非阻塞的事件處理機制,具體到系統(tǒng)調用就是像 select/poll/epoll/kqueue 這樣的系統(tǒng)調用。它們提供了一種機制,讓你可以同時監(jiān)控多個事件,調用他們是阻塞的,但可以設置超時時間,在超時時間之內,如果有事件準備好了,就返回。這種機制正好解決了我們上面的兩個問題,拿 epoll 為例(在后面的例子中,我們多以 epoll 為例子,以代表這一類函數(shù)),當事件沒準備好時,放到 epoll 里面,事件準備好了,我們就去讀寫,當讀寫返回 EAGAIN 時,我們將它再次加入到 epoll 里面。這樣,只要有事件準備好了,我們就去處理它,只有當所有事件都沒準備好時,才在 epoll 里面等著。這樣,我們就可以并發(fā)處理大量的并發(fā)了,當然,這里的并發(fā)請求,是指未處理完的請求,線程只有一個,所以同時能處理的請求當然只有一個了,只是在請求間進行不斷地切換而已,切換也是因為異步事件未準備好,而主動讓出的。這里的切換是沒有任何代價,你可以理解為循環(huán)處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優(yōu)勢的,不需要創(chuàng)建線程,每個請求占用的內存也很少,沒有上下文切換,事件處理非常的輕量級。并發(fā)數(shù)再多也不會導致無謂的資源浪費(上下文切換)。更多的并發(fā)數(shù),只是會占用更多的內存而已。 我之前有對連接數(shù)進行過測試,在 24G 內存的機器上,處理的并發(fā)請求數(shù)達到過 200 萬?,F(xiàn)在的網(wǎng)絡服務器基本都采用這種方式,這也是nginx性能高效的主要原因。

我們之前說過,推薦設置 worker 的個數(shù)為 cpu 的核數(shù),在這里就很容易理解了,更多的 worker 數(shù),只會導致進程來競爭 cpu 資源了,從而帶來不必要的上下文切換。而且,nginx為了更好的利用多核特性,提供了 cpu 親緣性的綁定選項,我們可以將某一個進程綁定在某一個核上,這樣就不會因為進程的切換帶來 cache 的失效。像這種小的優(yōu)化在 Nginx 中非常常見,同時也說明了 Nginx 作者的苦心孤詣。比如,Nginx 在做 4 個字節(jié)的字符串比較時,會將 4 個字符轉換成一個 int 型,再作比較,以減少 cpu 的指令數(shù)等等。

現(xiàn)在,知道了 Nginx 為什么會選擇這樣的進程模型與事件模型了。對于一個基本的 Web 服務器來說,事件通常有三種類型,網(wǎng)絡事件、信號、定時器。從上面的講解中知道,網(wǎng)絡事件通過異步非阻塞可以很好的解決掉。如何處理信號與定時器?

首先,信號的處理。對 Nginx 來說,有一些特定的信號,代表著特定的意義。信號會中斷掉程序當前的運行,在改變狀態(tài)后,繼續(xù)執(zhí)行。如果是系統(tǒng)調用,則可能會導致系統(tǒng)調用的失敗,需要重入。關于信號的處理,大家可以學習一些專業(yè)書籍,這里不多說。對于 Nginx 來說,如果nginx正在等待事件(epoll_wait 時),如果程序收到信號,在信號處理函數(shù)處理完后,epoll_wait 會返回錯誤,然后程序可再次進入 epoll_wait 調用。

另外,再來看看定時器。由于 epoll_wait 等函數(shù)在調用的時候是可以設置一個超時時間的,所以 Nginx 借助這個超時時間來實現(xiàn)定時器。nginx里面的定時器事件是放在一顆維護定時器的紅黑樹里面,每次在進入 epoll_wait前,先從該紅黑樹里面拿到所有定時器事件的最小時間,在計算出 epoll_wait 的超時時間后進入 epoll_wait。所以,當沒有事件產(chǎn)生,也沒有中斷信號時,epoll_wait 會超時,也就是說,定時器事件到了。這時,nginx會檢查所有的超時事件,將他們的狀態(tài)設置為超時,然后再去處理網(wǎng)絡事件。由此可以看出,當我們寫 Nginx 代碼時,在處理網(wǎng)絡事件的回調函數(shù)時,通常做的第一個事情就是判斷超時,然后再去處理網(wǎng)絡事件。

我們可以用一段偽代碼來總結一下 Nginx 的事件處理模型:

    while (true) {
        for t in run_tasks:
            t.handler();
        update_time(&now);
        timeout = ETERNITY;
        for t in wait_tasks: /* sorted already */
            if (t.time <= now) {
                t.timeout_handler();
            } else {
                timeout = t.time - now;
                break;
            }
        nevents = poll_function(events, timeout);
        for i in nevents:
            task t;
            if (events[i].type == READ) {
                t.handler = read_handler;
            } else { /* events[i].type == WRITE */
                t.handler = write_handler;
            }
            run_tasks_add(t);
    }

好,本節(jié)我們講了進程模型,事件模型,包括網(wǎng)絡事件,信號,定時器事件。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號