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()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你?。。∪绻到y中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率!?。?/li>
- 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: 負載均衡模塊,實現特定的算法,在眾多的后端服務器中,選擇一個服務器出來作為某個請求的轉發服務器。
版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。