Nginx 采用的是多進(jìn)程(單線程) & 多路IO復(fù)用模型,使用了 I/O 多路復(fù)用技術(shù)的 Nginx,就成了”并發(fā)事件驅(qū)動“的服務(wù)器,同時使用sendfile等技術(shù),最終實現(xiàn)了高性能。主要從以下幾個方面講述Nginx高性能機(jī)制:
- Nginx master-worker進(jìn)程機(jī)制。
- IO多路復(fù)用機(jī)制。
- Accept鎖及REUSEPORT機(jī)制。
- sendfile零拷貝機(jī)制
1、Nginx進(jìn)程機(jī)制
1.1、Nginx進(jìn)程機(jī)制概述
許多web服務(wù)器和應(yīng)用服務(wù)器使用簡單的線程的(threaded)、或基于流程的(process-based)架構(gòu), NGINX則以一種復(fù)雜的事件驅(qū)動(event-driven)的架構(gòu)脫穎而出,這種架構(gòu)能支持現(xiàn)代硬件上成千上萬的并發(fā)連接。
NGINX有一個主進(jìn)程(master process)(執(zhí)行特定權(quán)限的操作,如讀取配置、綁定端口)和一系列工作進(jìn)程(worker process)和輔助進(jìn)程(helper process)。如下圖所示:

如下所示:
# 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
如上4核服務(wù)器所示,NGINX主進(jìn)程創(chuàng)建了4個工作進(jìn)程和2個緩存輔助進(jìn)程(cachehelper processes)來管理磁盤內(nèi)容緩存(on-disk content cache)。如果我們不配置緩存,那么就只會有master、worker兩個進(jìn)程,worker進(jìn)程的數(shù)量,通過配置文件worker_process進(jìn)行配置(一般worker_process數(shù)與服務(wù)器CPU核心數(shù)一致),如下所示:
$ cat nginx.conf|grep process worker_processes 3; $ ps -ef|grep nginx 501 33758 1 0 四03上午 ?? 0:00.02 nginx: master process ./bin/nginx 501 56609 33758 0 11:58下午 ?? 0:00.00 nginx: worker process 501 56610 33758 0 11:58下午 ?? 0:00.00 nginx: worker process 501 56611 33758 0 11:58下午 ?? 0:00.00 nginx: worker process
NGINX根據(jù)可用的硬件資源,使用一個可預(yù)見式的(predictable)進(jìn)程模型:
- Master主進(jìn)程執(zhí)行特權(quán)操作,如讀取配置和綁定端口,還負(fù)責(zé)創(chuàng)建少量的子進(jìn)程(以下三種進(jìn)程)。
- Cache Loader緩存加載進(jìn)程在啟動時運行,把基于磁盤的緩存(disk-based cache)加載到內(nèi)存中,然后退出。緩存加載進(jìn)程的調(diào)度很謹(jǐn)慎,所以其資源需求很低。
- Cache Manager緩存管理進(jìn)程周期性運行,并削減磁盤緩存(prunes entries from the disk caches)來使緩存保持在配置的大小范圍內(nèi)。
- Worker工作進(jìn)程才是執(zhí)行所有實際任務(wù)的進(jìn)程:處理網(wǎng)絡(luò)連接、讀取和寫入內(nèi)容到磁盤,與upstream服務(wù)器通信等。
多數(shù)情況下,NGINX建議每1個CPU核心都運行1個工作進(jìn)程,使硬件資源得到最有效的利用。你可以在配置中設(shè)置如下指令:
worker_processes auto;
當(dāng)NGINX服務(wù)器運行時,只有Worker工作進(jìn)程在忙碌。每個工作進(jìn)程都以非阻塞的方式處理多個連接,以減少上下文切換的開銷。 每個工作進(jìn)程都是單線程且獨立運行的,抓取并處理新的連接。進(jìn)程間通過共享內(nèi)存的方式,來共享緩存數(shù)據(jù)、持久性會話數(shù)據(jù)(session persistence data)和其他共享資源。
1.2、Master進(jìn)程
nginx啟動后,系統(tǒng)中會以daemon的方式在后臺運行,后臺進(jìn)程包含一個master進(jìn)程和多個worker進(jìn)程。當(dāng)然nginx也是支持多線程的方式的,只是我們主流的方式還是多進(jìn)程的方式,也是nginx的默認(rèn)方式。我們可以手動地關(guān)掉后臺模式,讓nginx在前臺運行,并且通過配置讓nginx取消master進(jìn)程,從而可以使nginx以單進(jìn)程方式運行。生產(chǎn)環(huán)境下肯定不會這么做,所以關(guān)閉后臺模式,一般是用來調(diào)試用的。
master進(jìn)程主要用來管理worker進(jìn)程,包含:接收來自外界的信號,向各worker進(jìn)程發(fā)送信號,監(jiān)控worker進(jìn)程的運行狀態(tài),當(dāng)worker進(jìn)程退出后(異常情況下),會自動重新啟動新的worker進(jìn)程。
1.3、Worker進(jìn)程
每一個Worker工作進(jìn)程都是使用NGINX配置文件初始化的,并且主節(jié)點會為其提供一套監(jiān)聽套接字(listen sockets)。 worker進(jìn)程之間是平等的,每個進(jìn)程,處理請求的機(jī)會也是一樣的。當(dāng)我們提供80端口的http服務(wù)時,一個連接請求過來,每個進(jìn)程都有可能處理這個連接,怎么做到的呢?首先,每個worker進(jìn)程都是從master進(jìn)程fork過來,在master進(jìn)程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多個worker進(jìn)程。
所有worker進(jìn)程的listenfd會在新連接到來時變得可讀,為保證只有一個進(jìn)程處理該連接,所有worker進(jìn)程在注冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進(jìn)程注冊listenfd讀事件,在讀事件里調(diào)用accept接受該連接。當(dāng)一個worker進(jìn)程在accept這個連接之后,就開始讀取請求,解析請求,處理請求,產(chǎn)生數(shù)據(jù)后,再返回給客戶端,最后才斷開連接,這樣一個完整的請求就是這樣的了。所以Worker工作進(jìn)程通過等待在監(jiān)聽套接字上的事件(accept_mutex和kernel socketsharding)開始工作。事件是由新的incoming connections初始化的。這些連接被會分配給狀態(tài)機(jī)(statemachine)—— HTTP狀態(tài)機(jī)是最常用的,但NGINX還為流(原生TCP)和大量的郵件協(xié)議(SMTP,IMAP和POP3)實現(xiàn)了狀態(tài)機(jī)。

狀態(tài)機(jī)本質(zhì)上是一組告知NGINX如何處理請求的指令。大多數(shù)和NGINX具有相同功能的web服務(wù)器也使用類似的狀態(tài)機(jī)——只是實現(xiàn)不同。如下圖所示,就是一個HTTP請求的生命周期:

關(guān)于Nginx的處理流程,也可以如下圖所示:

1.4、 Nginx信號管理
Nginx可以通過信號的方式進(jìn)行管理,信號在Master-Worker的關(guān)系以及對應(yīng)的命令行,如下圖所示:
其具體使用方式為:
# 以下SIG有無前綴含義一樣,如SIGHUP和HUP等同 kill -信號 進(jìn)程號
Nginx主要是通過信號量來控制Nginx,所以我們常用的Nginx命令也都可以通過信號的方式進(jìn)行執(zhí)行。具體含義如下所示:
The master process of nginx can handle the following signals:
SIGINT, SIGTERM Shut down quickly. //nginx的進(jìn)程馬上被關(guān)閉,不能完整處理正在使用的nginx的用戶的請求,等同于 nginx -s stop
SIGHUP Reload configuration, start the new worker process with a new con‐figuration, and gracefully shut down
old worker processes. //nginx進(jìn)程不關(guān)閉,但是重新加載配置文件。等同于nginx -s reload
SIGQUIT Shut down gracefully. //優(yōu)雅的關(guān)閉nginx進(jìn)程,在處理完所有正在使用nginx用戶請求后再關(guān)閉nginx進(jìn)程,等同于nginx -s quit
SIGUSR1 Reopen log files. //不用關(guān)閉nginx進(jìn)程就可以重讀日志,此命令可以用于nginx的日志定時備份,按月/日等時間間隔分割有用等同于nginx -s reopen
SIGUSR2 Upgrade the nginx executable on the fly. //nginx的版本需要升級的時候,不需要停止nginx,就能對nginx升級
SIGWINCH Shut down worker processes gracefully. //配合USR2對nginx升級,優(yōu)雅的關(guān)閉nginx舊版本的進(jìn)程。
While there is no need to explicitly control worker processes normally, they support some signals too:
SIGTERM Shut down quickly.
SIGQUIT Shut down gracefully.
SIGUSR1 Reopen log files.
1.4.1、配置熱加載Reload
如下圖所示,NGINX的進(jìn)程體系結(jié)構(gòu)具有少量的Worker工作進(jìn)程,因此可以非常有效地更新配置,甚至可以更新NGINX二進(jìn)制文件本身。

更新NGINX配置是一個非常簡單,輕量且可靠的操作。它通常僅意味著運行nginx -s reload命令,該命令檢查磁盤上的配置并向主進(jìn)程發(fā)送SIGHUP信號。當(dāng)主進(jìn)程收到SIGHUP時,它將執(zhí)行以下兩項操作:
- 重新加載配置并派生一組新的工作進(jìn)程。這些新的工作進(jìn)程立即開始接受連接和處理流量(使用新的配置設(shè)置)。
- 指示舊工作進(jìn)程正常退出。工作進(jìn)程停止接受新連接。當(dāng)前的每個HTTP請求完成后,工作進(jìn)程就會干凈地關(guān)閉連接(即,沒有持久的keepalive)。一旦所有連接都關(guān)閉,工作進(jìn)程將退出。
此重新加載過程可能會導(dǎo)致CPU和內(nèi)存使用量的小幅上升,但是與從活動連接中加載資源相比,這通常是察覺不到的。您可以每秒多次重載配置(許多NGINX用戶正是這樣做的)。即使當(dāng)有許多版本NGINX工作進(jìn)程等待連接關(guān)閉時也很少有問題出現(xiàn),而且實際上這些連接很快被處理完并關(guān)閉掉。
詳細(xì)過程如下:
- 向master進(jìn)程發(fā)送HUP信號(reload)命令
- master進(jìn)程校驗配置語法是否正確
- master進(jìn)程打開新的監(jiān)聽端口
- master進(jìn)程用新配置啟動新的worker子進(jìn)程
- master進(jìn)程向老worker子進(jìn)程發(fā)送QUIT信號
- 老woker進(jìn)程關(guān)閉監(jiān)聽句柄,處理完當(dāng)前連接后結(jié)束進(jìn)程
如下圖所示,所有的Worker進(jìn)程都是新開啟的進(jìn)程:

1.4.2、Nginx平滑升級
Nginx可以支撐無縫平滑升級,即通過信號SIGUSR2和SIGWINCH,NGINX的二進(jìn)制升級過程實現(xiàn)了高可用性:您可以動態(tài)升級軟件,而不會出現(xiàn)斷開連接,停機(jī)或服務(wù)中斷的情況。
二進(jìn)制升級過程與正常配置熱加載的方法類似。一個新的NGINX主進(jìn)程與原始主進(jìn)程并行運行,并且它們共享偵聽套接字。這兩個進(jìn)程都是活動的,并且它們各自的工作進(jìn)程都處理流量。然后,您可以向舊的主機(jī)及其工作人員發(fā)出信號,使其優(yōu)雅地退出,如下圖所示:

如下圖所示示例:
第一階段新老Master、Worker共存,同時新Master也是老Master的一個子進(jìn)程。

第二階段,新Nginx替代老Nginx提供服務(wù)。如下所示通過SIGWINCH信號停老Worker服務(wù),但是老Master還在,可以直接kill SIGQUIT掉master。

也可以通過直接發(fā)送SIGQUIT信號,同時殺掉所有老Master、Worker:

2、IO多路復(fù)用(NIO)
2.1、IO模型
Nginx是基于NIO事件驅(qū)動的,是非阻塞的,還有很多中間件也是基于NIO的IO多路復(fù)用,如redis、tomcat、netty、nginx。

I/O復(fù)用模型會用到select、poll、epoll函數(shù),在這種模型中,這時候并不是進(jìn)程直接發(fā)起資源請求的系統(tǒng)調(diào)用去請求資源,進(jìn)程不會被“全程阻塞”,進(jìn)程是調(diào)用select或poll函數(shù)。進(jìn)程不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll幫助用戶進(jìn)程去輪詢那些IO操作是否完成。
- select:基于數(shù)組實現(xiàn),最多支持1024路IO。
- poll:基于鏈表實現(xiàn)IO管理無限制,可以超過1024,沒有IO量的限制。
- epoll:linux 2.6以上才支持,是基于紅黑樹+鏈表,擁有更高的IO管理性能。
- kqueue:unix的內(nèi)核支持,如Mac。
2.2、epoll
2.3.1、epoll概述
我們重點看看epoll,epoll提供了三個函數(shù),epoll_create, epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄(也就是一個epoll instance);epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。
int epoll_create(int size);//創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
① 執(zhí)行 epoll_create
內(nèi)核在epoll文件系統(tǒng)中建了個file結(jié)點,(使用完,必須調(diào)用close()關(guān)閉,否則導(dǎo)致fd被耗盡)
在內(nèi)核cache里建了紅黑樹存儲epoll_ctl傳來的socket,
在內(nèi)核cache里建了rdllist雙向鏈表存儲準(zhǔn)備就緒的事件。
?、?執(zhí)行 epoll_ctl
如果增加socket句柄,檢查紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊回調(diào)函數(shù),告訴內(nèi)核如果這個句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所有添加到epoll中的事件都會與設(shè)備(如網(wǎng)卡)驅(qū)動程序建立回調(diào)關(guān)系,相應(yīng)的事件發(fā)生時,會調(diào)用回調(diào)方法。
?、?執(zhí)行 epoll_wait
立刻返回準(zhǔn)備就緒表里的數(shù)據(jù)即可(將內(nèi)核cache里雙向列表中存儲的準(zhǔn)備就緒的事件 復(fù)制到用戶態(tài)內(nèi)存),當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時將事件數(shù)量返回給用戶。
在io活躍數(shù)比較少的情況下使用epoll更有優(yōu)勢:因為鏈表時間復(fù)雜度o(n)。epoll同select、poll比較:

2.3.2、epoll水平觸發(fā)與邊緣觸發(fā)
- Level_triggered(水平觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時,它還會通知你在上沒讀寫完的文件描述符上繼續(xù)讀寫,當(dāng)然如果你一直不去讀寫,它會一直通知你?。。∪绻到y(tǒng)中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率!!!
- Edge_triggered(邊緣觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知你?。。∵@種模式比水平觸發(fā)效率高,系統(tǒng)不會充斥大量你不關(guān)心的就緒文件描述符??!
select(),poll()模型都是水平觸發(fā)模式,信號驅(qū)動IO是邊緣觸發(fā)模式,epoll()模型即支持水平觸發(fā),也支持邊緣觸發(fā),默認(rèn)是水平觸發(fā)。
具體可以參考博文:7層網(wǎng)絡(luò)以及5種Linux IO模型以及相應(yīng)IO基礎(chǔ)
3、Accept鎖及REUSEPORT機(jī)制
3.1、Accept鎖
Nginx這種多進(jìn)程的服務(wù)器,在fork后同時監(jiān)聽同一個端口時,如果有一個外部連接進(jìn)來,會導(dǎo)致所有休眠的子進(jìn)程被喚醒,而最終只有一個子進(jìn)程能夠成功處理accept事件,其他進(jìn)程都會重新進(jìn)入休眠中。這就導(dǎo)致出現(xiàn)了很多不必要的schedule和上下文切換,而這些開銷是完全不必要的。
在Linux內(nèi)核的較新版本中,accept調(diào)用本身所引起的驚群問題已經(jīng)得到了解決,但是在Nginx中,accept是交給epoll機(jī)制來處理的,epoll的accept帶來的驚群問題并沒有得到解決(應(yīng)該是epoll_wait本身并沒有區(qū)別讀事件是否來自于一個Listen套接字的能力,所以所有監(jiān)聽這個事件的進(jìn)程會被這個epoll_wait喚醒。),所以Nginx的accept驚群問題仍然需要定制一個自己的解決方案。
accept鎖就是nginx的解決方案,本質(zhì)上這是一個跨進(jìn)程的互斥鎖,以這個互斥鎖來保證只有一個進(jìn)程具備監(jiān)聽accept事件的能力。
3.2、Accept鎖實現(xiàn)(accept_mutex)
實現(xiàn)上accept鎖是一個跨進(jìn)程鎖,其在Nginx中是一個全局變量,聲明如下:
ngx_shmtx_t ngx_accept_mutex;
nginx是一個 1(master)+N(worker) 多進(jìn)程模型:master在啟動過程中負(fù)責(zé)讀取nginx.conf中配置的監(jiān)聽端口,然后加入到一個cycle->listening數(shù)組中。init_cycle函數(shù)中會調(diào)用init_module函數(shù),init_module函數(shù)會調(diào)用所有注冊模塊的module_init函數(shù)完成相關(guān)模塊所需資源的申請以及其他一些工作;其中event模塊的module_init函數(shù)申請一塊共享內(nèi)存用于存儲accept_mutex鎖信息以及連接數(shù)信息。
因此這是一個在event模塊初始化時就分配好的鎖,放在一塊進(jìn)程間共享的內(nèi)存中,以保證所有進(jìn)程都能訪問這一個實例,其加鎖解鎖是借由linux的原子變量來做CAS,如果加鎖失敗則立即返回,是一種非阻塞的鎖。加解鎖代碼如下:
ngx_uint_t ngx_shmtx_trylock(ngx_shmtx_t *mtx) { return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)); } #define ngx_shmtx_lock(mtx) ngx_spinlock((mtx)->lock, ngx_pid, 1024) #define ngx_shmtx_unlock(mtx) (void) ngx_atomic_cmp_set((mtx)->lock, ngx_pid, 0)
可以看出,調(diào)用ngx_shmtx_trylock失敗后會立刻返回而不會阻塞。那么accept鎖如何保證只有一個進(jìn)程能夠處理新連接呢?要解決epoll帶來的accept鎖的問題也很簡單,只需要保證同一時間只有一個進(jìn)程注冊了accept的epoll事件即可。Nginx采用的處理模式也沒什么特別的,大概就是如下的邏輯:
嘗試獲取accept鎖 if 獲取成功: 在epoll中注冊accept事件 else: 在epoll中注銷accept事件 處理所有事件 釋放accept鎖
我們知道,所有的worker進(jìn)程均是由master進(jìn)程通過fork() 函數(shù)啟動的,所以所有的worker進(jìn)程也就繼承了master進(jìn)程所有打開的文件描述符(包括之前創(chuàng)建的共享內(nèi)存的fd)以及變量數(shù)據(jù)(這其中就包括之前創(chuàng)建的accept_mutex鎖)。worker啟動的過程中會調(diào)用各個模塊的process_init函數(shù),其中event模塊的process_init函數(shù)中就會將master配置好的listening數(shù)組加入到epoll監(jiān)聽的events中,這樣初始階段所有的worker的epoll監(jiān)聽列表中都包含listening數(shù)組中的fd。
當(dāng)各個worker實際運行時,對于accept鎖的處理和epoll中注冊注銷accept事件的的處理都是在ngx_trylock_accept_mutex中進(jìn)行的。而這一系列過程則是在nginx主體循環(huán)中反復(fù)調(diào)用的void ngx_process_events_and_timers(ngx_cycle_t *cycle)中進(jìn)行。
也就是說,每輪事件的處理都會首先競爭accept鎖,競爭成功則在epoll中注冊accept事件,失敗則注銷accept事件,然后處理完事件之后,釋放accept鎖。由此只有一個進(jìn)程監(jiān)聽一個listen套接字,從而避免了驚群問題。
那么如果某個獲取accept_mutex鎖的worker非常忙,有非常多事件要處理,一直沒輪到釋放鎖,那么某一個進(jìn)程長時間占用accept鎖,而又無暇處理新連接;其他進(jìn)程又沒有占用accept鎖,同樣無法處理新連接,這是怎么處理的呢?為了解決這個問題,Nginx采用了將事件處理延后的方式。即在ngx_process_events的處理中,僅僅將事件放入兩個隊列中:
ngx_thread_volatile ngx_event_t *ngx_posted_accept_events;
ngx_thread_volatile ngx_event_t *ngx_posted_events;
處理的網(wǎng)絡(luò)事件主要牽扯到2個隊列,一個是ngx_posted_accept_events,另一個是ngx_posted_events。其中,一個隊列用于放accept的事件,另一個則是普通的讀寫事件;
ngx_event_process_posted會處理事件隊列,其實就是調(diào)用每個事件的回調(diào)函數(shù),然后再讓這個事件出隊。
那么具體是怎么實現(xiàn)的呢?其實就是在static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)的flags參數(shù)中傳入一個NGX_POST_EVENTS的標(biāo)志位,處理事件時檢查這個標(biāo)志位即可。
這里只是避免了事件的消費對于accept鎖的長期占用,那么萬一epoll_wait本身占用的時間很長呢?這方面的處理也很簡單,epoll_wait本身是有超時時間的,限制住它的值就可以了,這個參數(shù)保存在ngx_accept_mutex_delay這個全局變量中。
核心代碼如下:
ngx_process_events_and_timers(ngx_cycle_t *cycle) { ngx_uint_t flags; ngx_msec_t timer, delta; /* 省略一些處理時間事件的代碼 */ // 這里是處理負(fù)載均衡鎖和accept鎖的時機(jī) if (ngx_use_accept_mutex) { // 如果負(fù)載均衡token的值大于0, 則說明負(fù)載已滿,此時不再處理accept, 同時把這個值減一 if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { // 嘗試拿到accept鎖 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } // 拿到鎖之后把flag加上post標(biāo)志,讓所有事件的處理都延后 // 以免太長時間占用accept鎖 if (ngx_accept_mutex_held) { flags |= NGX_POST_EVENTS; } else { if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { // 最多等ngx_accept_mutex_delay個毫秒,防止占用太久accept鎖 timer = ngx_accept_mutex_delay; } } } } delta = ngx_current_msec; // 調(diào)用事件處理模塊的process_events,處理一個epoll_wait的方法 (void) ngx_process_events(cycle, timer, flags); // 計算處理events事件所消耗的時間 delta = ngx_current_msec - delta; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "timer delta: %M", delta); // 如果有延后處理的accept事件,那么延后處理這個事件 ngx_event_process_posted(cycle, &ngx_posted_accept_events); // 釋放accept鎖 if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } // 處理所有的超時事件 if (delta) { ngx_event_expire_timers(); } // 處理所有的延后事件 ngx_event_process_posted(cycle, &ngx_posted_events); }
整個流程如下所示:
3.3、Accept鎖開啟是否一定性能高
上述分析的主要是accept_mutex打開的情況。對于不打開的情況,比較簡單,所有worker的epoll都會監(jiān)聽listening數(shù)組中的所有fd,所以一旦有新連接過來,就會出現(xiàn)worker“搶奪資源“的情況。對于分布式的大量短鏈接來講,打開accept_mutex選項較好,避免了worker爭奪資源造成的上下文切換以及try_lock的鎖開銷。但是對于傳輸大量數(shù)據(jù)的tcp長鏈接來講,打開accept_mutex就會導(dǎo)致壓力集中在某幾個worker上,特別是將worker_connection值設(shè)置過大的時候,影響更加明顯。因此對于accept_mutex開關(guān)的使用,根據(jù)實際情況考慮,不可一概而論。
一般來說,如果采用的是長tcp連接的方式,而且worker_connection也比較大,這樣就出現(xiàn)了accept_mutex打開worker負(fù)載不均造成QPS下降的問題。
目前新版的Linux內(nèi)核中增加了EPOLLEXCLUSIVE選項,nginx從1.11.3版本之后也增加了對NGX_EXCLUSIVE_EVENT選項的支持,這樣就可以避免多worker的epoll出現(xiàn)的驚群效應(yīng),從此之后accept_mutex從默認(rèn)的on變成了默認(rèn)off。
3.4、reuseport機(jī)制
3.4.1、reuseport機(jī)制描述
NGINX發(fā)布的1.9.1版本引入了一個新的特性:允許使用SO_REUSEPORT套接字選項,該選項在許多操作系統(tǒng)的新版本中是可用的,包括Bsd和Linux(內(nèi)核版本3.9及以后)。該套接字選項允許多個套接字監(jiān)聽同一IP和端口的組合。內(nèi)核能夠在這些套接字中對傳入的連接進(jìn)行負(fù)載均衡。對于NGINX而言,啟用該選項可以減少在某些場景下的鎖競爭而改善性能。
如下圖描述,當(dāng)SO_REUSEPORT未開啟時,一個單獨的監(jiān)聽socket通知工作進(jìn)程接入的連接,并且每個工作線程都試圖獲得連接。

當(dāng)SO_REUSEPORT選項啟用時,存在對每一個IP地址和端口綁定連接的多個socket監(jiān)聽器,每一個工作進(jìn)程都可以分配一個。系統(tǒng)內(nèi)核決定哪一個有效的socket監(jiān)聽器(通過隱式的方式,給哪一個工作進(jìn)程)獲得連接。這可以減少工作進(jìn)程之間獲得新連接時的封鎖競爭(譯者注:工作進(jìn)程請求獲得互斥資源加鎖之間的競爭),同時在多核系統(tǒng)可以提高性能。然而,這也意味著當(dāng)一個工作進(jìn)程陷入阻塞操作時,阻塞影響的不僅是已經(jīng)接受連接的工作進(jìn)程,也同時讓內(nèi)核發(fā)送連接請求計劃分配的工作進(jìn)程因此變?yōu)樽枞?nbsp;

3.4.2、開啟reuseport
要開啟SO_REUSEPORT,需要為HTTP或TCP(流模式)通信選項內(nèi)的listen項直接添加reuseport參數(shù),就像下例這樣:
http { server { listen 80 reuseport; server_name localhost; # ... } } stream { server { listen 12345 reuseport; # ... } }
引用reuseport參數(shù)后,accept_mutex參數(shù)將會無效,因為互斥鎖對reuseport來說是多余的。如果沒有開啟reuseport,設(shè)置accept_mutex仍然是有效的。accept_mutex默認(rèn)是開啟的。
3.4.3、reuseport的性能測試
我在一個36核的AWS實例運行wrk基準(zhǔn)測試工具,測試4個NGINX工作進(jìn)程。為了減少網(wǎng)絡(luò)的影響,客戶端和NGINX都運行在本地,并且讓NGINX返回OK字符串而不是一個文件。我比較三種NGINX配置:默認(rèn)(等同于accept_mutex on),accept_mutex off和reuseport。如圖所示,reuseport的每秒請求是其余的兩到三倍,同時延遲和延遲標(biāo)準(zhǔn)差也是減少的。

也運行了另一個相關(guān)的性能測試——客戶端和NGINX分別在不同的機(jī)器上且NGINX返回一個HTML文件。如下表所示,用reuseport減少的延遲和之前的性能測試相似,延遲的標(biāo)準(zhǔn)差減少的更為顯著(接近十分之一)。其他結(jié)果(沒有顯示在表格中)同樣令人振奮。使用reuseport ,負(fù)載被均勻分離到了worker進(jìn)程。在默認(rèn)條件下(等同于 accept_mutex on),一些worker分到了較高百分比的負(fù)載,而用accept_mutex off所有worker都受到了較高的負(fù)載。

在這些性能測試中,連接請求的速度是很高的,但是請求不需要大量的處理。其他的基本的測試應(yīng)該指出——當(dāng)應(yīng)用流量符合這種場景時 reuseport 也能大幅提高性能。(reuseport 參數(shù)在 mail 上下文環(huán)境下不能用在 listen 指令下,例如email,因為email流量一定不會匹配這種場景。)我們鼓勵你先測試而不是直接大規(guī)模應(yīng)用。關(guān)于測試NGNIX性能的一些技巧,看看Konstantin Pavlov在nginx2014大會上的演講。
原文地址:Socket Sharding in NGINX Release 1.9.1
Nginx reuseport的原理參考另一篇博文:nginx源碼分析—reuseport的使用
4、Sendfile機(jī)制
在Nginx作為WEB服務(wù)器使用的時候,會訪問大量的本地磁盤文件,在以往,訪問磁盤文件會經(jīng)歷多次內(nèi)核態(tài)、用戶態(tài)切換,造成大量的資源浪費(如下圖右邊部分),而Nginx支持sendfile(也就是零拷貝),實現(xiàn)文件fd到網(wǎng)卡fd的直接映射(如下圖左側(cè)部分),跳過了大量的用戶態(tài)、內(nèi)核態(tài)切換。如下圖所示:

注:用戶態(tài)、內(nèi)核態(tài)切換一次大約耗費是5ms,而一次CPU時間片大約才10ms-100ms,因此在大量并發(fā)的情況下不斷進(jìn)行內(nèi)核切換相當(dāng)浪費CPU資源,建議配置打開sendfile。
配置文件如截圖所示:

附錄、模塊化體系結(jié)構(gòu)
如下圖所示,就是Nginx的模塊體系化結(jié)構(gòu):
nginx的模塊根據(jù)其功能基本上可以分為以下幾種類型:
- event module: 搭建了獨立于操作系統(tǒng)的事件處理機(jī)制的框架,及提供了各具體事件的處理。包括ngx_events_module, ngx_event_core_module和ngx_epoll_module等。nginx具體使用何種事件處理模塊,這依賴于具體的操作系統(tǒng)和編譯選項。
- phase handler: 此類型的模塊也被直接稱為handler模塊。主要負(fù)責(zé)處理客戶端請求并產(chǎn)生待響應(yīng)內(nèi)容,比如ngx_http_static_module模塊,負(fù)責(zé)客戶端的靜態(tài)頁面請求處理并將對應(yīng)的磁盤文件準(zhǔn)備為響應(yīng)內(nèi)容輸出。
- output filter: 也稱為filter模塊,主要是負(fù)責(zé)對輸出的內(nèi)容進(jìn)行處理,可以對輸出進(jìn)行修改。例如,可以實現(xiàn)對輸出的所有html頁面增加預(yù)定義的footbar一類的工作,或者對輸出的圖片的URL進(jìn)行替換之類的工作。
- upstream: upstream模塊實現(xiàn)反向代理的功能,將真正的請求轉(zhuǎn)發(fā)到后端服務(wù)器上,并從后端服務(wù)器上讀取響應(yīng),發(fā)回客戶端。upstream模塊是一種特殊的handler,只不過響應(yīng)內(nèi)容不是真正由自己產(chǎn)生的,而是從后端服務(wù)器上讀取的。
- load-balancer: 負(fù)載均衡模塊,實現(xiàn)特定的算法,在眾多的后端服務(wù)器中,選擇一個服務(wù)器出來作為某個請求的轉(zhuǎn)發(fā)服務(wù)器。
浙公網(wǎng)安備 33010602011771號