最近對(duì)Nginx源碼比較感興趣鸣个,借助于強(qiáng)大的VS Code帅矗,我一步一步,似魔鬼的步伐科吭,開始了Nginx的探索之旅昏滴。關(guān)于 VS Code 如何調(diào)試 Nginx 可參考上篇文章《VS CODE 輕松調(diào)試 Nginx》。
一. 引言
Nginx 其實(shí)無(wú)需做太多介紹砌溺,作為業(yè)界知名的高性能服務(wù)器影涉,被廣大互聯(lián)網(wǎng)公司應(yīng)用,阿里的 Tegine 就是基于 Nginx 開發(fā)的规伐。
Nginx 基本上都是用來(lái)做負(fù)載均衡蟹倾、反向代理和動(dòng)靜分離。目前大部分公司都采用 Nginx 作為負(fù)載均衡器猖闪。作為 LBS鲜棠,最基本的要求就是要支持高并發(fā),畢竟所有的請(qǐng)求都要經(jīng)過(guò)它來(lái)進(jìn)行轉(zhuǎn)發(fā)培慌。
那么為什么 Nginx 擁有如此強(qiáng)大的并發(fā)能力呢豁陆?這便是我感興趣的事情,也是這篇文章所要講的事情吵护。但是標(biāo)題是《動(dòng)手打造Nginx多進(jìn)程架構(gòu)》盒音,難道這篇文章卻只是簡(jiǎn)單的源碼分析?
這幾天研究 Nginx 過(guò)程中馅而,我常常陷于Nginx 復(fù)雜的源碼之中祥诽,不得其解,雖然也翻了一些資料和書籍瓮恭,但是總覺(jué)得沒(méi)有 get 到精髓雄坪,就是好像已經(jīng)理解了,但是對(duì)于具體流程和細(xì)節(jié)屯蹦,總是模模糊糊维哈。于是趁著周末绳姨,花了小半天,再次梳理了下Nginx 多進(jìn)程事件的源碼阔挠,仿照著寫了一個(gè)普通的 Server飘庄,雖然代碼和功能都非常簡(jiǎn)單,不過(guò)剛好適合于讀者了解Nginx谒亦,而不至于陷于叢林之中竭宰,不知方向。
二. 傳統(tǒng) Web Server 架構(gòu)
讓我們來(lái)思考下份招,如果讓你動(dòng)手打造一個(gè) web 服務(wù)器切揭,你會(huì)怎么做?
第一步锁摔,監(jiān)聽(tīng)端口
第二步廓旬,處理請(qǐng)求
監(jiān)聽(tīng)端口倒是很簡(jiǎn)單,處理請(qǐng)求該怎么做呢谐腰?不知道大家上大學(xué)剛開始學(xué)c語(yǔ)言的時(shí)候孕豹,老師有沒(méi)有布置過(guò)聊天室之類的作業(yè)?那時(shí)候我其實(shí)完全靠百度來(lái)完成的:開啟端口監(jiān)聽(tīng)十气,死循環(huán)接收請(qǐng)求励背,每接收一個(gè)請(qǐng)求就直接開個(gè)新線程去處理。
這樣做當(dāng)然可以砸西,也很簡(jiǎn)單叶眉,完全滿足了我當(dāng)時(shí)的作業(yè)要求,其實(shí)目前很多web服務(wù)器芹枷,諸如tomcat之類衅疙,也都是這樣做的,為每個(gè)請(qǐng)求單獨(dú)分配一個(gè)線程鸳慈。那么這樣做饱溢,有什么弊端呢?
最直接的弊端就是線程數(shù)量開的太多走芋,會(huì)導(dǎo)致 CPU 在不同線程之間不斷的進(jìn)行上下文切換绩郎。CPU 的每次任務(wù)切換,都需要為上一次任務(wù)保存一些上下文信息(如寄存器的值)翁逞,再裝載新任務(wù)的上下文信息嗽上,這些都是不小的開銷。
第二個(gè)弊端就是CPU利用率的下降熄攘,考慮當(dāng)前只有一個(gè)線程的情況,當(dāng)線程在等待網(wǎng)絡(luò) IO 的時(shí)候其實(shí)是處于阻塞狀態(tài)彼念,這個(gè)時(shí)候 CPU 便處于空閑狀態(tài)挪圾,這直接導(dǎo)致了 CPU 沒(méi)有被充分利用浅萧,簡(jiǎn)直是暴殄天物!
這種架構(gòu)哲思,使 Web 服務(wù)器從骨子里洼畅,就對(duì)高并發(fā)沒(méi)有很好的承載能力!
三. Nginx 多進(jìn)程架構(gòu)
Nginx 之所以可以支持高并發(fā)棚赔,正是因?yàn)樗饤壛藗鹘y(tǒng) Web 服務(wù)器的多線程架構(gòu)帝簇,并充分利用了 CPU。
Nginx采用的是 單Master靠益、多Worker 架構(gòu)丧肴,顧名思義,Master 是老板胧后,而 Worker 才是真正干活的工人階層芋浮。
我們先來(lái)看下 Nginx 接收請(qǐng)求的大概架構(gòu)。
乍一看壳快,好像和傳統(tǒng)的 Web Server 也沒(méi)啥區(qū)別啊纸巷,不過(guò)是右邊的 Thread 變成了 Worker 罷了扫腺。這其實(shí)正是 Nginx 的精妙之處缴川。
Master 進(jìn)程啟動(dòng)后,會(huì) fork 出 N 個(gè) Worker 進(jìn)程谴垫,N 是 可配置的竖伯,一般來(lái)說(shuō)存哲,可以設(shè)置為服務(wù)器核心數(shù),設(shè)置更大值也沒(méi)有太多意義黔夭,無(wú)非是會(huì)增加 CPU 進(jìn)程切換的開銷宏胯。
每個(gè)Worker 進(jìn)程都會(huì)監(jiān)聽(tīng)來(lái)自客戶端的請(qǐng)求,并進(jìn)行處理本姥,與傳統(tǒng) Web Server 不同的是肩袍,Worker 進(jìn)程不會(huì)對(duì)于每個(gè)請(qǐng)求都分配一個(gè)單獨(dú)線程去處理,而是充分利用了IO多路復(fù)用 的特性婚惫。
如果讀者之前沒(méi)有了解或者使用過(guò)IO多路復(fù)用氛赐,那確實(shí)該好好充充電了。Android 中的 Looper先舷、Java 著名的開源庫(kù) Netty艰管,都是基于多路復(fù)用,所謂多路復(fù)用蒋川,與同步阻塞IO最大的區(qū)別就是牲芋,一個(gè)進(jìn)程可以同時(shí)處理多個(gè)IO操作,當(dāng) 某個(gè)IO 操作 Ready 時(shí),操作系統(tǒng)會(huì)主動(dòng)通知進(jìn)程缸浦。
Nginx 正是使用了這樣的思想夕冲,雖然同時(shí)有很多請(qǐng)求需要處理,但是沒(méi)必要為每個(gè)請(qǐng)求都分配一個(gè)線程啊裂逐。哪個(gè)請(qǐng)求的網(wǎng)絡(luò) IO Ready 了歹鱼,我就去處理哪個(gè),這樣不就可以了嗎卜高?何必創(chuàng)建一個(gè)線程在那傻傻的等著弥姻。
舉個(gè)不恰當(dāng)?shù)睦樱?wù)器就好比是學(xué)校掺涛,客戶端好比是學(xué)生庭敦,學(xué)生有不會(huì)的問(wèn)題就會(huì)問(wèn)老師。
- 對(duì)于傳統(tǒng)的 Web 服務(wù)器鸽照,每個(gè)學(xué)生螺捐,學(xué)校都會(huì)派一個(gè)老師去服務(wù),一個(gè)學(xué)邪牵可能有幾千個(gè)學(xué)生定血,那豈不是要雇幾千個(gè)老師,校領(lǐng)導(dǎo)怕是連工資都發(fā)不出來(lái)了吧诞外。仔細(xì)想想澜沟,每個(gè)學(xué)生不可能隨時(shí)都在提問(wèn)吧,總得休息下吧峡谊!那學(xué)生休息時(shí)茫虽,老師干嘛呢?白拿工資還不干活既们。
- 對(duì)于Nginx濒析,它就不給老師閑的機(jī)會(huì)啦,學(xué)校有幾間辦公室啥纸,就雇幾個(gè)老師号杏,有學(xué)生提問(wèn)時(shí),就派一個(gè)老師解答斯棒,所以一個(gè)老師會(huì)負(fù)責(zé)很多學(xué)生盾致,哪個(gè)學(xué)生舉手了,他就去幫助哪個(gè)學(xué)生解決問(wèn)題荣暮。
這里有讀者怕是會(huì)疑惑庭惜,如果哪個(gè)學(xué)生一直霸占著老師不放怎么辦?這樣老師不就沒(méi)有機(jī)會(huì)去解答其他同學(xué)的問(wèn)題了嗎穗酥?如果作為一個(gè)負(fù)責(zé)業(yè)務(wù)處理的 Web 服務(wù)器护赊,Nginx這種架構(gòu)確實(shí)可能出現(xiàn)這樣的問(wèn)題惠遏,但是要記住,Nginx主要是用來(lái)做負(fù)載均衡的骏啰,他的主要任務(wù)是接收請(qǐng)求爽哎、轉(zhuǎn)發(fā)請(qǐng)求,所以它的業(yè)務(wù)處理其實(shí)就是將請(qǐng)求再轉(zhuǎn)發(fā)給其他的服務(wù)器器一,那么接收用IO多路復(fù)用,轉(zhuǎn)發(fā)也用 IO 多路復(fù)用不就行了厨内。
四. 源碼分析
基于最新 1.15.5 版本
4.1 整體運(yùn)行機(jī)制
一切都從 main()開始祈秕。
nginx 的 main()方法中有不少邏輯,不過(guò)對(duì)于今天我要講的事情來(lái)說(shuō)雏胃,最重要的就是兩件事:
- 創(chuàng)建套接字请毛,監(jiān)聽(tīng)端口;
- Fork 出 N 個(gè) Worker 進(jìn)程瞭亮。
監(jiān)聽(tīng)端口沒(méi)什么太多邏輯方仿,我們先來(lái)看看 Worker 進(jìn)程的誕生:
static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
ngx_int_t i;
ngx_channel_t ch;
....
for (i = 0; i < n; i++) {
ngx_spawn_process(cycle, ngx_worker_process_cycle,
(void *) (intptr_t) i, "worker process", type);
......
}
}
這里主要是根據(jù)配置的 Worker 數(shù)量,創(chuàng)建出對(duì)應(yīng)數(shù)量的 Worker 進(jìn)程统翩,創(chuàng)建 Woker 進(jìn)程調(diào)用的是 ngx_spawn_process()仙蚜,第二個(gè)參數(shù) ngx_worker_process_cycle 就是子進(jìn)程的新起點(diǎn)。
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
......
for ( ;; ) {
......
ngx_process_events_and_timers(cycle);
......
}
}
上面的代碼省略了一些邏輯厂汗,只保留了最核心的部分委粉。ngx_worker_process_cycle ,正如其名娶桦,在其內(nèi)部開啟了一個(gè)死循環(huán)贾节,不斷調(diào)用 ngx_process_events_and_timers()。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
......
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
......
}
}
......
(void) ngx_process_events(cycle, timer, flags);
......
}
這里最后調(diào)用了ngx_process_events()來(lái)接收并處理事件衷畦。
ngx_process_events()在不同平臺(tái)指向不同的 IO 處理模塊栗涂,比如Linux上為epoll,而在Mac OS上指向的其實(shí)是kqueue模塊中的ngx_kqueue_process_events()祈争。
static ngx_int_t
ngx_kqueue_process_events(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags)
{
int events, n;
ngx_int_t i, instance;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *ev;
ngx_queue_t *queue;
struct timespec ts, *tp;
n = (int) nchanges;
nchanges = 0;
......
events = kevent(ngx_kqueue, change_list, n, event_list, (int) nevents, tp);
......
for (i = 0; i < events; i++) {
......
ev = (ngx_event_t *) event_list[i].udata;
switch (event_list[i].filter) {
case EVFILT_READ:
case EVFILT_WRITE:
......
break;
case EVFILT_VNODE:
ev->kq_vnode = 1;
break;
case EVFILT_AIO:
ev->complete = 1;
ev->ready = 1;
break;
......
}
......
ev->handler(ev);
}
return NGX_OK;
}
上面其實(shí)就是一個(gè)比較基本的 kqueue 使用方式了斤程。說(shuō)到這里,我們就不得不說(shuō)下 kqueue 的使用方式了铛嘱。
kqueue 主要依托于兩個(gè) API:
// 創(chuàng)建一個(gè)內(nèi)核消息隊(duì)列暖释,返回隊(duì)列描述符
int kqueue(void);
// 用途:注冊(cè)\反注冊(cè) 監(jiān)聽(tīng)事件,等待事件通知
// kq墨吓,上面創(chuàng)建的消息隊(duì)列描述符
// changelist球匕,需要注冊(cè)的事件
// changelist,changelist數(shù)組大小
// eventlist帖烘,內(nèi)核會(huì)把返回的事件放在該數(shù)組中
// nevents亮曹,eventlist數(shù)組大小
// timeout,等待內(nèi)核返回事件的超時(shí)事件,NULL 即為無(wú)限等待
int kevent(int kq,
const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout);
我們回過(guò)頭再來(lái)看看上面 ngx_kqueue_process_events()中代碼照卦,其實(shí)也就是在調(diào)用kevent()等待內(nèi)核返回消息式矫,收到消息后再進(jìn)行處理。這里消息處理主要是進(jìn)行ACCEPT役耕、READ采转、WRITE等。
所以從整體來(lái)看瞬痘,Nginx事件模塊的運(yùn)行就是 Worker 進(jìn)程在死循環(huán)中故慈,不斷等待內(nèi)核消息隊(duì)列返回事件消息,并加以處理的一個(gè)過(guò)程框全。
4.2 驚群?jiǎn)栴}
到這里我們一直在討論一個(gè)單獨(dú)的 Worker 進(jìn)程運(yùn)行機(jī)制察绷,那么每個(gè) Worker 進(jìn)程之間有沒(méi)有什么交互呢?
回到上面的 ngx_process_events_and_timers()中津辩,在每次調(diào)用 ngx_process_events()等待消息之前拆撼,Worker 進(jìn)程都會(huì)進(jìn)行一個(gè) ngx_trylock_accept_mutex()操作,這其實(shí)就是多個(gè) Worker 進(jìn)程之間在爭(zhēng)奪監(jiān)聽(tīng)資格的過(guò)程喘沿,是 Nginx 為了解決驚群?jiǎn)栴}而設(shè)計(jì)出的方案闸度。
所謂驚群,其實(shí)就是如果有多個(gè)Worker進(jìn)程同時(shí)在監(jiān)聽(tīng)內(nèi)核消息事件摹恨,當(dāng)有請(qǐng)求到來(lái)時(shí)筋岛,每個(gè)Worker進(jìn)程都會(huì)被喚醒,去accept同一個(gè)請(qǐng)求晒哄,但是只能有一個(gè)進(jìn)程會(huì)accept成功睁宰,其他進(jìn)程會(huì)accept失敗,被白白的喚醒了寝凌,就像你再睡覺(jué)時(shí)被突然叫醒柒傻,卻發(fā)現(xiàn)壓根沒(méi)你啥事,你說(shuō)氣不氣人较木。
為了解決這個(gè)問(wèn)題红符,Nginx 讓每個(gè)Worker 進(jìn)程在監(jiān)聽(tīng)內(nèi)核消息事件前去競(jìng)爭(zhēng)一把鎖,只有成功獲得鎖的進(jìn)程才能去監(jiān)聽(tīng)內(nèi)核事件伐债,其他進(jìn)程就乖乖的睡眠在鎖的等待隊(duì)列上预侯。當(dāng)獲得鎖的進(jìn)程處理完accept事件,就會(huì)回來(lái)釋放掉這把鎖峰锁,這時(shí)所有進(jìn)程又會(huì)同時(shí)去競(jìng)爭(zhēng)鎖了萎馅。
為了不讓每次都是同一個(gè)進(jìn)程搶到鎖,Nginx 設(shè)計(jì)了一個(gè)小算法虹蒋,用一個(gè)因子ngx_accept_disabled 去 平均每個(gè)進(jìn)程獲得鎖的概率糜芳,感興趣的同學(xué)可以自己看下這塊源碼飒货。
五. 動(dòng)手打造 Nginx 多進(jìn)程架構(gòu)
終于到DIY的環(huán)節(jié)了,這里我基于 MacOS 平臺(tái)來(lái)開發(fā)峭竣,IO多路復(fù)用也是選用上面所講的 kqueue塘辅。
5.1 創(chuàng)建進(jìn)程鎖,用于搶到監(jiān)聽(tīng)事件資格
mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
memset(mm,0x00,sizeof(*mm));
pthread_mutexattr_init(&mm->mutexattr);
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mm->mutex,&mm->mutexattr);
5.2 創(chuàng)建套接字皆撩,監(jiān)聽(tīng)端口
// 創(chuàng)建套接字
int serverSock =socket(AF_INET, SOCK_STREAM, 0);
if (serverSock == -1)
{
printf("socket failed\n");
exit(0);
}
//綁定ip和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(::bind(serverSock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
{
printf("bind failed\n");
exit(0);
}
//啟動(dòng)監(jiān)聽(tīng)
if(listen(serverSock, 20) == -1)
{
printf("listen failed\n");
exit(0);
}
5.3 創(chuàng)建多個(gè) Worker 進(jìn)程
// fork 出 3 個(gè) Worker 進(jìn)程
int result;
for(int i = 1; i< 3; i++){
result = fork();
if(result == 0){
startWorker(i,serverSock);
printf("start worker %d\n",i);
break;
}
}
5.4 啟動(dòng)Worker 進(jìn)程扣墩,監(jiān)聽(tīng) IO 事件
void startWorker(int workerId,int serverSock)
{
// 創(chuàng)建內(nèi)核事件隊(duì)列
int kqueuefd=kqueue();
struct kevent change_list[1]; //想要監(jiān)控的事件的數(shù)組
struct kevent event_list[1]; //用來(lái)接受事件的數(shù)組
//初始化所需注冊(cè)事件
EV_SET(&change_list[0], serverSock, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
// 循環(huán)接受事件
while (true) {
// 競(jìng)爭(zhēng)鎖,獲取監(jiān)聽(tīng)資格
pthread_mutex_lock(&mm->mutex);
printf("Worker %d get the lock\n",workerId);
// 注冊(cè)事件扛吞,等待通知
int nevents = kevent(kqueuefd, change_list, 1, event_list, 1, NULL);
// 釋放鎖
pthread_mutex_unlock(&mm->mutex);
//遍歷返回的所有就緒事件
for(int i = 0; i< nevents;i++){
struct kevent event =event_list[i];
if(event.ident == serverSock){
// ACCEPT 事件
handleNewConnection(kqueuefd,serverSock);
}else if(event.filter == EVFILT_READ){
//讀取客戶端傳來(lái)的數(shù)據(jù)
char * msg = handleReadFromClient(workerId,event);
handleWriteToClient(workerId,event,msg);
}
}
}
}
5.5 開啟多個(gè) Client 進(jìn)程測(cè)試
運(yùn)行結(jié)果:
哈哈沮榜,基本實(shí)現(xiàn)了我的要求。
Demo 源碼見(jiàn):https://github.com/HalfStackDeveloper/LearnNginx
六. 總結(jié)
Nginx 之所以有強(qiáng)大的高并發(fā)能力喻粹,得益于它與眾不同的架構(gòu)設(shè)計(jì),無(wú)論是多進(jìn)程還是 IO 多路復(fù)用草巡,都是 Nginx 不可或缺的一部分守呜。研究 Nginx 源碼十分有趣,但是看源碼和動(dòng)手寫又是兩回事山憨,看源碼只能大概了解脈絡(luò)查乒,只有自己操刀,才能真正理解和運(yùn)用郁竟!