Nginx 採用的是多進程(單線程) & 多路IO復用模型,使用了 I/O 多路復用技術的 Nginx,就成了」並發事件驅動「的伺服器,同時使用sendfile等技術,最終實現了高性能。主要從以下幾個方面講述Nginx高性能機制:
- Nginx master-worker進程機制。
- IO多路復用機制。
- Accept鎖及REUSEPORT機制。
- sendfile零拷貝機制
1、Nginx進程機制
1.1、Nginx進程機制概述
許多web伺服器和應用伺服器使用簡單的線程的(threaded)、或基於流程的(process-based)架構, NGINX則以一種複雜的事件驅動(event-driven)的架構脫穎而出,這種架構能支持現代硬體上成千上萬的並發連接。
NGINX有一個主進程(master process)(執行特定許可權的操作,如讀取配置、綁定埠)和一系列工作進程(worker process)和輔助進程(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核伺服器所示,NGINX主進程創建了4個工作進程和2個緩存輔助進程(cachehelper processes)來管理磁碟內容緩存(on-disk content cache)。如果我們不配置緩存,那麼就只會有master、worker兩個進程,worker進程的數量,通過配置文件worker_process進行配置(一般worker_process數與伺服器CPU核心數一致),如下所示:
$ 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根據可用的硬體資源,使用一個可預見式的(predictable)進程模型:
- Master主進程執行特權操作,如讀取配置和綁定埠,還負責創建少量的子進程(以下三種進程)。
- Cache Loader緩存載入進程在啟動時運行,把基於磁碟的緩存(disk-based cache)載入到內存中,然後退出。緩存載入進程的調度很謹慎,所以其資源需求很低。
- Cache Manager緩存管理進程周期性運行,並削減磁碟緩存(prunes entries from the disk caches)來使緩存保持在配置的大小範圍內。
- Worker工作進程才是執行所有實際任務的進程:處理網路連接、讀取和寫入內容到磁碟,與upstream伺服器通信等。
多數情況下,NGINX建議每1個CPU核心都運行1個工作進程,使硬體資源得到最有效的利用。你可以在配置中設置如下指令:
worker_processes auto;
當NGINX伺服器運行時,只有Worker工作進程在忙碌。每個工作進程都以非阻塞的方式處理多個連接,以減少上下文切換的開銷。 每個工作進程都是單線程且獨立運行的,抓取並處理新的連接。進程間通過共享內存的方式,來共享緩存數據、持久性會話數據(session persistence data)和其他共享資源。
1.2、Master進程
nginx啟動後,系統中會以daemon的方式在後台運行,後台進程包含一個master進程和多個worker進程。當然nginx也是支持多線程的方式的,只是我們主流的方式還是多進程的方式,也是nginx的默認方式。我們可以手動地關掉後台模式,讓nginx在前台運行,並且通過配置讓nginx取消master進程,從而可以使nginx以單進程方式運行。生產環境下肯定不會這麼做,所以關閉後台模式,一般是用來調試用的。
master進程主要用來管理worker進程,包含:接收來自外界的信號,向各worker進程發送信號,監控worker進程的運行狀態,當worker進程退出後(異常情況下),會自動重新啟動新的worker進程。
1.3、Worker進程
每一個Worker工作進程都是使用NGINX配置文件初始化的,並且主節點會為其提供一套監聽套接字(listen sockets)。 worker進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供80埠的http服務時,一個連接請求過來,每個進程都有可能處理這個連接,怎麼做到的呢?首先,每個worker進程都是從master進程fork過來,在master進程裡面,先建立好需要listen的socket(listenfd)之後,然後再fork出多個worker進程。
所有worker進程的listenfd會在新連接到來時變得可讀,為保證只有一個進程處理該連接,所有worker進程在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀事件,在讀事件里調用accept接受該連接。當一個worker進程在accept這個連接之後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開連接,這樣一個完整的請求就是這樣的了。所以Worker工作進程通過等待在監聽套接字上的事件(accept_mutex和kernel socketsharding)開始工作。事件是由新的incoming connections初始化的。這些連接被會分配給狀態機(statemachine)—— HTTP狀態機是最常用的,但NGINX還為流(原生TCP)和大量的郵件協議(SMTP,IMAP和POP3)實現了狀態機。

狀態機本質上是一組告知NGINX如何處理請求的指令。大多數和NGINX具有相同功能的web伺服器也使用類似的狀態機——只是實現不同。如下圖所示,就是一個HTTP請求的生命周期:

關於Nginx的處理流程,也可以如下圖所示:

C/C++Linux伺服器開發高級架構師學習視頻 點擊 正在跳轉 獲取,內容知識點包括Linux,Nginx,ZeroMQ,MySQL,Redis,線程池,MongoDB,ZK,Linux內核,CDN,P2P,epoll,Docker,TCP/IP,協程,DPDK等等。
對標騰訊T9後端開發崗位,linux伺服器開發高級架構師系統學習視頻:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂

1.4、 Nginx信號管理
Nginx可以通過信號的方式進行管理,信號在Master-Worker的關係以及對應的命令行,如下圖所示:

其具體使用方式為:
# 以下SIG有無前綴含義一樣,如SIGHUP和HUP等同
kill -信號 進程號
Nginx主要是通過信號量來控制Nginx,所以我們常用的Nginx命令也都可以通過信號的方式進行執行。具體含義如下所示:
The master process of nginx can handle the following signals:
SIGINT, SIGTERM Shut down quickly. //nginx的進程馬上被關閉,不能完整處理正在使用的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進程不關閉,但是重新載入配置文件。等同於nginx -s reload
SIGQUIT Shut down gracefully. //優雅的關閉nginx進程,在處理完所有正在使用nginx用戶請求後再關閉nginx進程,等同於nginx -s quit
SIGUSR1 Reopen log files. //不用關閉nginx進程就可以重讀日誌,此命令可以用於nginx的日誌定時備份,按月/日等時間間隔分割有用等同於nginx -s reopen
SIGUSR2 Upgrade the nginx executable on the fly. //nginx的版本需要升級的時候,不需要停止nginx,就能對nginx升級
SIGWINCH Shut down worker processes gracefully. //配合USR2對nginx升級,優雅的關閉nginx舊版本的進程。
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的進程體系結構具有少量的Worker工作進程,因此可以非常有效地更新配置,甚至可以更新NGINX二進位文件本身。

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

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

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

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

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

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

I/O復用模型會用到select、poll、epoll函數,在這種模型中,這時候並不是進程直接發起資源請求的系統調用去請求資源,進程不會被「全程阻塞」,進程是調用select或poll函數。進程不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll幫助用戶進程去輪詢那些IO操作是否完成。
- select:基於數組實現,最多支持1024路IO。
- poll:基於鏈表實現IO管理無限制,可以超過1024,沒有IO量的限制。
- epoll:linux 2.6以上才支持,是基於紅黑樹+鏈表,擁有更高的IO管理性能。
- kqueue:unix的內核支持,如Mac。
2.2、epoll
2.3.1、epoll概述
我們重點看看epoll,epoll提供了三個函數,epoll_create, epoll_ctl和epoll_wait,epoll_create是創建一個epoll句柄(也就是一個epoll instance);epoll_ctl是註冊要監聽的事件類型;epoll_wait則是等待事件的產生。
int epoll_create(int size);//創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大
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);
① 執行 epoll_create
內核在epoll文件系統中建了個file結點,(使用完,必須調用close()關閉,否則導致fd被耗盡)
在內核cache里建了紅黑樹存儲epoll_ctl傳來的socket,
在內核cache里建了rdllist雙向鏈表存儲準備就緒的事件。
② 執行 epoll_ctl
如果增加socket句柄,檢查紅黑樹中是否存在,存在立即返回,不存在則添加到樹榦上,然後向內核註冊回調函數,告訴內核如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裡。所有添加到epoll中的事件都會與設備(如網卡)驅動程序建立回調關係,相應的事件發生時,會調用回調方法。
③ 執行 epoll_wait
立刻返回準備就緒表裡的數據即可(將內核cache里雙向列表中存儲的準備就緒的事件 複製到用戶態內存),當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。
在io活躍數比較少的情況下使用epoll更有優勢:因為鏈表時間複雜度o(n)。epoll同select、poll比較:

2.3.2、epoll水平觸發與邊緣觸發
- Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩衝區太小),那麼下次調用
epoll_wait()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率!!!
- Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩衝區太小),那麼下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符!!
select(),poll()模型都是水平觸發模式,信號驅動IO是邊緣觸發模式,epoll()模型即支持水平觸發,也支持邊緣觸發,默認是水平觸發。
3、Accept鎖及REUSEPORT機制
3.1、Accept鎖
Nginx這種多進程的伺服器,在fork後同時監聽同一個埠時,如果有一個外部連接進來,會導致所有休眠的子進程被喚醒,而最終只有一個子進程能夠成功處理accept事件,其他進程都會重新進入休眠中。這就導致出現了很多不必要的schedule和上下文切換,而這些開銷是完全不必要的。
在Linux內核的較新版本中,accept調用本身所引起的驚群問題已經得到了解決,但是在Nginx中,accept是交給epoll機制來處理的,epoll的accept帶來的驚群問題並沒有得到解決(應該是epoll_wait本身並沒有區別讀事件是否來自於一個Listen套接字的能力,所以所有監聽這個事件的進程會被這個epoll_wait喚醒。),所以Nginx的accept驚群問題仍然需要定製一個自己的解決方案。
accept鎖就是nginx的解決方案,本質上這是一個跨進程的互斥鎖,以這個互斥鎖來保證只有一個進程具備監聽accept事件的能力。
C/C++Linux伺服器開發高級架構師學習視頻 點擊 「鏈接」 獲取,內容知識點包括Linux,Nginx,ZeroMQ,MySQL,Redis,線程池,MongoDB,ZK,Linux內核,CDN,P2P,epoll,Docker,TCP/IP,協程,DPDK等等。
對標騰訊T9後端開發崗位,linux伺服器開發高級架構師系統學習視頻:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂

3.2、Accept鎖實現(accept_mutex)
實現上accept鎖是一個跨進程鎖,其在Nginx中是一個全局變數,聲明如下:
ngx_shmtx_t ngx_accept_mutex;
nginx是一個 1(master)+N(worker) 多進程模型:master在啟動過程中負責讀取nginx.conf中配置的監聽埠,然後加入到一個cycle->listening數組中。init_cycle函數中會調用init_module函數,init_module函數會調用所有註冊模塊的module_init函數完成相關模塊所需資源的申請以及其他一些工作;其中event模塊的module_init函數申請一塊共享內存用於存儲accept_mutex鎖信息以及連接數信息。
因此這是一個在event模塊初始化時就分配好的鎖,放在一塊進程間共享的內存中,以保證所有進程都能訪問這一個實例,其加鎖解鎖是藉由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)
可以看出,調用ngx_shmtx_trylock失敗後會立刻返回而不會阻塞。那麼accept鎖如何保證只有一個進程能夠處理新連接呢?要解決epoll帶來的accept鎖的問題也很簡單,只需要保證同一時間只有一個進程註冊了accept的epoll事件即可。Nginx採用的處理模式也沒什麼特別的,大概就是如下的邏輯:
嘗試獲取accept鎖
if 獲取成功:
在epoll中註冊accept事件
else:
在epoll中註銷accept事件
處理所有事件
釋放accept鎖
我們知道,所有的worker進程均是由master進程通過fork() 函數啟動的,所以所有的worker進程也就繼承了master進程所有打開的文件描述符(包括之前創建的共享內存的fd)以及變數數據(這其中就包括之前創建的accept_mutex鎖)。worker啟動的過程中會調用各個模塊的process_init函數,其中event模塊的process_init函數中就會將master配置好的listening數組加入到epoll監聽的events中,這樣初始階段所有的worker的epoll監聽列表中都包含listening數組中的fd。
當各個worker實際運行時,對於accept鎖的處理和epoll中註冊註銷accept事件的的處理都是在ngx_trylock_accept_mutex中進行的。而這一系列過程則是在nginx主體循環中反覆調用的void
ngx_process_events_and_timers(ngx_cycle_t *cycle)中進行。
也就是說,每輪事件的處理都會首先競爭accept鎖,競爭成功則在epoll中註冊accept事件,失敗則註銷accept事件,然後處理完事件之後,釋放accept鎖。由此只有一個進程監聽一個listen套接字,從而避免了驚群問題。
那麼如果某個獲取accept_mutex鎖的worker非常忙,有非常多事件要處理,一直沒輪到釋放鎖,那麼某一個進程長時間佔用accept鎖,而又無暇處理新連接;其他進程又沒有佔用accept鎖,同樣無法處理新連接,這是怎麼處理的呢?為了解決這個問題,Nginx採用了將事件處理延後的方式。即在ngx_process_events的處理中,僅僅將事件放入兩個隊列中:
ngx_thread_volatile ngx_event_t *ngx_posted_accept_events;
ngx_thread_volatile ngx_event_t *ngx_posted_events;
處理的網路事件主要牽扯到2個隊列,一個是ngx_posted_accept_events,另一個是ngx_posted_events。其中,一個隊列用於放accept的事件,另一個則是普通的讀寫事件;
ngx_event_process_posted會處理事件隊列,其實就是調用每個事件的回調函數,然後再讓這個事件出隊。
那麼具體是怎麼實現的呢?其實就是在static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)的flags參數中傳入一個NGX_POST_EVENTS的標誌位,處理事件時檢查這個標誌位即可。
這裡只是避免了事件的消費對於accept鎖的長期佔用,那麼萬一epoll_wait本身佔用的時間很長呢?這方面的處理也很簡單,epoll_wait本身是有超時時間的,限制住它的值就可以了,這個參數保存在ngx_accept_mutex_delay這個全局變數中。
核心代碼如下:
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
/* 省略一些處理時間事件的代碼 */
// 這裡是處理負載均衡鎖和accept鎖的時機
if (ngx_use_accept_mutex) {
// 如果負載均衡token的值大於0, 則說明負載已滿,此時不再處理accept, 同時把這個值減一
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
// 嘗試拿到accept鎖
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
// 拿到鎖之後把flag加上post標誌,讓所有事件的處理都延後
// 以免太長時間佔用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;
// 調用事件處理模塊的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都會監聽listening數組中的所有fd,所以一旦有新連接過來,就會出現worker「搶奪資源「的情況。對於分散式的大量短鏈接來講,打開accept_mutex選項較好,避免了worker爭奪資源造成的上下文切換以及try_lock的鎖開銷。但是對於傳輸大量數據的tcp長鏈接來講,打開accept_mutex就會導致壓力集中在某幾個worker上,特別是將worker_connection值設置過大的時候,影響更加明顯。因此對於accept_mutex開關的使用,根據實際情況考慮,不可一概而論。
一般來說,如果採用的是長tcp連接的方式,而且worker_connection也比較大,這樣就出現了accept_mutex打開worker負載不均造成QPS下降的問題。
目前新版的Linux內核中增加了EPOLLEXCLUSIVE選項,nginx從1.11.3版本之後也增加了對NGX_EXCLUSIVE_EVENT選項的支持,這樣就可以避免多worker的epoll出現的驚群效應,從此之後accept_mutex從默認的on變成了默認off。
3.4、reuseport機制
3.4.1、reuseport機制描述
NGINX發布的1.9.1版本引入了一個新的特性:允許使用SO_REUSEPORT套接字選項,該選項在許多操作系統的新版本中是可用的,包括Bsd和Linux(內核版本3.9及以後)。該套接字選項允許多個套接字監聽同一IP和埠的組合。內核能夠在這些套接字中對傳入的連接進行負載均衡。對於NGINX而言,啟用該選項可以減少在某些場景下的鎖競爭而改善性能。
如下圖描述,當SO_REUSEPORT未開啟時,一個單獨的監聽socket通知工作進程接入的連接,並且每個工作線程都試圖獲得連接。

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

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

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

在這些性能測試中,連接請求的速度是很高的,但是請求不需要大量的處理。其他的基本的測試應該指出——當應用流量符合這種場景時 reuseport 也能大幅提高性能。(reuseport 參數在 mail 上下文環境下不能用在 listen 指令下,例如email,因為email流量一定不會匹配這種場景。)我們鼓勵你先測試而不是直接大規模應用。
4、Sendfile機制
在Nginx作為WEB伺服器使用的時候,會訪問大量的本地磁碟文件,在以往,訪問磁碟文件會經歷多次內核態、用戶態切換,造成大量的資源浪費(如下圖右邊部分),而Nginx支持sendfile(也就是零拷貝),實現文件fd到網卡fd的直接映射(如下圖左側部分),跳過了大量的用戶態、內核態切換。如下圖所示:

註:用戶態、內核態切換一次大約耗費是5ms,而一次CPU時間片大約才10ms-100ms,因此在大量並發的情況下不斷進行內核切換相當浪費CPU資源,建議配置打開sendfile。
配置文件如截圖所示:

附錄、模塊化體系結構
如下圖所示,就是Nginx的模塊體系化結構:

nginx的模塊根據其功能基本上可以分為以下幾種類型:
- event
module: 搭建了獨立於操作系統的事件處理機制的框架,及提供了各具體事件的處理。包括ngx_events_module,
ngx_event_core_module和ngx_epoll_module等。nginx具體使用何種事件處理模塊,這依賴於具體的操作系統和編譯選項。 - phase handler: 此類型的模塊也被直接稱為handler模塊。主要負責處理客戶端請求併產生待響應內容,比如ngx_http_static_module模塊,負責客戶端的靜態頁面請求處理並將對應的磁碟文件準備為響應內容輸出。
- output filter: 也稱為filter模塊,主要是負責對輸出的內容進行處理,可以對輸出進行修改。例如,可以實現對輸出的所有html頁面增加預定義的footbar一類的工作,或者對輸出的圖片的URL進行替換之類的工作。
- upstream: upstream模塊實現反向代理的功能,將真正的請求轉發到後端伺服器上,並從後端伺服器上讀取響應,發回客戶端。upstream模塊是一種特殊的handler,只不過響應內容不是真正由自己產生的,而是從後端伺服器上讀取的。
- load-balancer: 負載均衡模塊,實現特定的演算法,在眾多的後端伺服器中,選擇一個伺服器出來作為某個請求的轉發伺服器。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/284438.html