一文淺析Nginx線程池!
Nginx通過使用多路復(fù)用IO(如Linux的epoll、FreeBSD的kqueue等)技術(shù)很好的解決了c10k問題,但前提是Nginx的請求不能有阻塞操作,否則將會導(dǎo)致整個Nginx進(jìn)程停止服務(wù)。
但很多時候阻塞操作是不可避免的,例如客戶端請求靜態(tài)文件時,由于磁盤IO可能會導(dǎo)致進(jìn)程阻塞,所以將會導(dǎo)致Nginx的性能下降。為了解決這個問題,Nginx在1.7.11版本中實現(xiàn)了線程池機(jī)制。
下面我們將會分析Nginx是怎么通過線程池來解決阻塞操作問題。
啟用線程池功能
要使用線程池功能,首先需要在配置文件中添加如下配置項:
上面定義了一個名為“default”,包含32個線程,任務(wù)隊列最多支持65536個請求的線程池。如果任務(wù)隊列過載,Nginx將輸出如下錯誤日志并拒絕請求:
如果出現(xiàn)上面的錯誤,說明線程池的負(fù)載很高,這是可以通過添加線程數(shù)來解決這個問題。當(dāng)達(dá)到機(jī)器的最高處理能力之后,增加線程數(shù)并不能改善這個問題。
一切從“源”開始
下面主要通過剖析Nginx的源碼來了解線程池機(jī)制實現(xiàn)原理?,F(xiàn)在先來了解Nginx線程池的兩個重要數(shù)據(jù)結(jié)構(gòu)ngx_thread_pool_t和ngx_thread_task_t。
ngx_thread_pool_t結(jié)構(gòu)體
下面解釋下每個字段的用途:
mtx: 互斥鎖,用于鎖定任務(wù)隊列,避免競爭狀態(tài)。
queue: 任務(wù)隊列。
waiting: 有多少個任務(wù)正在等待處理。
cond: 用于通知線程池有任務(wù)需要處理。
name: 線程池名稱。
threads: 線程池由多少個線程組成(線程數(shù))。
max_queue: 線程池最大能處理的任務(wù)數(shù)。
ngx_thread_task_t結(jié)構(gòu)體
下面解釋下每個字段的用途:
mtx: 互斥鎖,用于鎖定任務(wù)隊列,避免競爭狀態(tài)。
queue: 任務(wù)隊列。
waiting: 有多少個任務(wù)正在等待處理。
cond: 用于通知線程池有任務(wù)需要處理。
name: 線程池名稱。
threads: 線程池由多少個線程組成(線程數(shù))。
max_queue: 線程池最大能處理的任務(wù)數(shù)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)??


ngx_thread_task_t結(jié)構(gòu)體
下面解釋下每個字段的用途:
next: 指向下一個任務(wù)。
id: 任務(wù)ID。
ctx: 任務(wù)的上下文。
handler: 處理任務(wù)的函數(shù)句柄。
event: 跟任務(wù)關(guān)聯(lián)的事件對象(當(dāng)線程池處理成任務(wù)之后將會由主線程調(diào)用event對象的handler回調(diào)函數(shù))。
線程池初始化
下面介紹下線程池的初始化過程。
在Nginx啟動的時候,首先會調(diào)用ngx_thread_pool_init_worker()函數(shù)來初始化線程池。ngx_thread_pool_init_worker()函數(shù)最終會調(diào)用ngx_thread_pool_init(),源碼如下:
ngx_thread_pool_init()最終調(diào)用pthread_create()函數(shù)創(chuàng)建線程池中的工作線程,工作線程會從ngx_thread_pool_cycle()函數(shù)開始執(zhí)行。
ngx_thread_pool_cycle()函數(shù)源碼如下:
ngx_thread_pool_cycle()函數(shù)的主要工作是從待處理的任務(wù)隊列中獲取一個任務(wù),然后調(diào)用任務(wù)對象的handler()函數(shù)處理任務(wù),完成后把任務(wù)放置到完成隊列中,并通過ngx_notify()通知主線程。
添加任務(wù)到任務(wù)隊列
通過上面的分析,我們知道了線程池是怎么從任務(wù)隊列獲取任務(wù)并處理。但任務(wù)隊列的任務(wù)從哪里來的呢?因為Nginx的使命是處理客戶端請求,所以可以知道任務(wù)是通過客戶端請求產(chǎn)生的。也就是說,任務(wù)是主線程創(chuàng)建的(主線程負(fù)責(zé)處理客戶端請求)。
主線程通過ngx_thread_task_post()函數(shù)向任務(wù)隊列中添加一個任務(wù),代碼如下:
ngx_thread_task_post()函數(shù)首先調(diào)用ngx_thread_cond_signal()通知線程池的線程有任務(wù)需要處理,然后把任務(wù)添加到任務(wù)隊列中??赡苡腥藭枺韧ㄖ€程池在添加任務(wù)到任務(wù)隊列中會不會有順序問題。其實這樣做是沒問題的,這是因為只要主線程不調(diào)用ngx_thread_mutex_unlock()把互斥鎖解開,線程池中的工作線程是不會從ngx_thread_cond_wait()返回的。
收尾工作
當(dāng)線程池把任務(wù)處理完后會把其放置到完成隊列中(ngx_thread_pool_done),然后調(diào)用ngx_notify()通知主線程有任務(wù)完成了。主線程收到通知后,會在事件模塊中進(jìn)行收尾工作:調(diào)用task.event.handler()。task.event.handler由任務(wù)創(chuàng)建者設(shè)置,例如在ngx_http_copy_filter模塊的ngx_http_copy_thread_handler()函數(shù):
task.event.handler被設(shè)置為ngx_http_copy_thread_event_handler,就是說當(dāng)任務(wù)處理完成后,主線程將會調(diào)用ngx_http_copy_thread_event_handler來進(jìn)行收尾工作。
哪些操作會使用線程池
那么哪些操作會使用線程池去處理。一般來說,磁盤IO會使用線程池來處理。在ngx_http_copy_filter模塊中,會調(diào)用ngx_thread_read()讀取文件的內(nèi)容(當(dāng)啟用了線程池時),而ngx_thread_read()會把讀取文件內(nèi)容的操作讓線程池去處理。ngx_thread_read()代碼如下:
從上面的代碼看到,task的handler被設(shè)置為ngx_thread_read_handler,也就是說在線程池中將會調(diào)用ngx_thread_read_handler()去讀取文件內(nèi)容。而file->thread_handler()將會調(diào)用ngx_thread_task_post(),前面已經(jīng)分析過,ngx_thread_task_post()會把任務(wù)添加到任務(wù)隊列中。
圖解
最后用一張圖來解釋Nginx線程池機(jī)制的原理吧。

原文作者:Linux內(nèi)核那些事
