99re热视频这里只精品,久久久天堂国产精品女人,国产av一区二区三区,久久久精品成人免费看片,99久久精品免费看国产一区二区三区

初探 Nginx 架構(gòu)

2022-03-23 14:59 更新

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

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

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

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

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

那么,Nginx 采用這種進(jìn)程模型有什么好處呢?當(dāng)然,好處肯定會(huì)很多了。首先,對(duì)于每個(gè) worker 進(jìn)程來(lái)說(shuō),獨(dú)立的進(jìn)程,不需要加鎖,所以省掉了鎖帶來(lái)的開(kāi)銷,同時(shí)在編程以及問(wèn)題查找時(shí),也會(huì)方便很多。其次,采用獨(dú)立的進(jìn)程,可以讓互相之間不會(huì)影響,一個(gè)進(jìn)程退出后,其它進(jìn)程還在工作,服務(wù)不會(huì)中斷,master 進(jìn)程則很快啟動(dòng)新的 worker 進(jìn)程。當(dāng)然,worker 進(jìn)程的異常退出,肯定是程序有 bug 了,異常退出,會(huì)導(dǎo)致當(dāng)前 worker 上的所有請(qǐng)求失敗,不過(guò)不會(huì)影響到所有請(qǐng)求,所以降低了風(fēng)險(xiǎn)。當(dāng)然,好處還有很多,大家可以慢慢體會(huì)。

上面講了很多關(guān)于 Nginx 的進(jìn)程模型,接下來(lái),我們來(lái)看看 Nginx 是如何處理事件的。

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

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

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

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

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

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

我們可以用一段偽代碼來(lái)總結(jié)一下 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é)我們講了進(jìn)程模型,事件模型,包括網(wǎng)絡(luò)事件,信號(hào),定時(shí)器事件。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)