在web編程領(lǐng)域,Openresty與Go均有十分優(yōu)秀的處理能力接谨,在面對(duì)高并發(fā)的web編程摆碉,兩者一般都是首選的技術(shù)方案。這兩者我也一直使用脓豪,而且兩者均有協(xié)程巷帝,現(xiàn)總結(jié)下,留個(gè)備忘跑揉。
Openresty及其工作流程
基于Openresty 1.18版本
將Lua集成到Nginx中锅睛,而Nginx埠巨,更是高性能HTTP服務(wù)器的代表。
Nginx是多進(jìn)程單線程:一個(gè)master進(jìn)程和多個(gè)worker進(jìn)程现拒,處理請(qǐng)求的是worker進(jìn)程辣垒。
啟動(dòng)流程
Openresty是在master進(jìn)程創(chuàng)建時(shí)通過ngx_http_lua_init_vm函數(shù)初始化lua vm,在fork出work進(jìn)程時(shí)印蔬,lua vm便集成到work進(jìn)程勋桶,每個(gè)work進(jìn)程均有一個(gè)lua vm。
worker啟動(dòng)起來(lái)后侥猬,worker進(jìn)程便開始循環(huán)處理請(qǐng)求例驹,當(dāng)有新的請(qǐng)求到來(lái)時(shí),只有申請(qǐng)到ngx_accept_mutex的worker才會(huì)處理它(注冊(cè)listen fd到自己的epoll中),避免驚群退唠。
Nginx高性能的原因是其異步非阻塞的事件處理機(jī)制鹃锈,即select/poll/epoll/kqueue這樣的系統(tǒng)調(diào)用。
協(xié)程調(diào)度
假如有這個(gè)配置:
配置項(xiàng)為:
location ~ ^/api {
content_by_lua_file test.lua;
}
而對(duì)于每個(gè)請(qǐng)求瞧预,如請(qǐng)求為:request=/api?age=20
Openresty都會(huì)創(chuàng)建一個(gè)協(xié)程來(lái)處理屎债。
而這個(gè)創(chuàng)建的協(xié)程是系統(tǒng)協(xié)程,是主協(xié)程垢油,用戶無(wú)法控制它盆驹。
而用戶通過ngx.thread.spawn創(chuàng)建的協(xié)程是通過ngx_http_lua_coroutine_create_helper創(chuàng)建出來(lái)的,用戶創(chuàng)建的協(xié)程是主協(xié)程的子協(xié)程滩愁。并通過ngx_http_lua_co_ctx_s保存協(xié)程的相關(guān)信息躯喇。
協(xié)程通過ngx_http_lua_run_thread函數(shù)來(lái)運(yùn)行與調(diào)度。當(dāng)前待執(zhí)行的協(xié)程為ngx_http_lua_ctx_t->cur_co_ctx硝枉。
對(duì)于每個(gè)worker進(jìn)程來(lái)說廉丽,每個(gè)用戶請(qǐng)求都會(huì)創(chuàng)建一個(gè)協(xié)程,每個(gè)協(xié)程都是互相隔離的妻味,而且用戶還會(huì)創(chuàng)建用戶協(xié)程雅倒,這些協(xié)程最終交與當(dāng)前worker進(jìn)程中的lua vm進(jìn)行執(zhí)行。在同一個(gè)時(shí)間點(diǎn)弧可,只能有一個(gè)協(xié)程來(lái)執(zhí)行,這些協(xié)程該怎么調(diào)度呢劣欢?
其實(shí)這些協(xié)程是基于事件的(利用nginx的事件機(jī)制)協(xié)作式的調(diào)度:
1.對(duì)于系統(tǒng)創(chuàng)建的協(xié)程來(lái)說棕诵,當(dāng)系統(tǒng)事件未觸發(fā),對(duì)應(yīng)的就是IO事件未準(zhǔn)備好(ET模式凿将,epoll_wait返回的活躍fd一直讀或?qū)懼钡椒祷谽AGIAN)時(shí)校套,當(dāng)前執(zhí)行的協(xié)程就會(huì)讓出cpu,讓別的協(xié)程進(jìn)行執(zhí)行牧抵;
2.對(duì)于用戶創(chuàng)建的協(xié)程來(lái)說笛匙,除了上面提到的1外侨把,如果用戶代碼執(zhí)行了讓出,也會(huì)進(jìn)行讓出操作妹孙。
GO及其工作流程
基于go 1.15版本
Go是單進(jìn)程秋柄,多線程,多協(xié)程蠢正。
啟動(dòng)流程
對(duì)于下面這個(gè)簡(jiǎn)單的go程序:
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
我們可以通過gdb跟蹤到啟動(dòng)流程骇笔。
Go程序啟動(dòng)后,在執(zhí)行用戶的main函數(shù)前嚣崭,啟動(dòng)順序?yàn)椋?br> runtime.args->runtime.osinit->runtime.schedinit->runtime.newproc->runtime.mstart
其中:
runtime.args:初始化argc,argv笨触;遍歷auxv,即輔助向量(auxiliary vector)來(lái)初始化一些系統(tǒng)運(yùn)行變量:如內(nèi)存頁(yè)大小(physPageSize)雹舀,startupRandomData(初始化隨機(jī)數(shù)種子時(shí)會(huì)用到),cpuid信息等
runtime.osinit:設(shè)置cpu核數(shù)(ncpu)和huge page大頁(yè)內(nèi)存大新印(physHugePageSize)
runtime.schedinit:
初始化棧、內(nèi)存分配器说榆、隨機(jī)數(shù)種子;
初始化m0并放入allm中虚吟;gc初始化;
對(duì)所有p初始化,對(duì)allp[0]和m0進(jìn)行綁定
runtime.newproc:
這個(gè)函數(shù)就是我們?cè)趃o語(yǔ)言中使用go func()來(lái)創(chuàng)建協(xié)程時(shí)娱俺,go會(huì)調(diào)用它來(lái)實(shí)際創(chuàng)建goroutine.
會(huì)優(yōu)先在本地p中獲取空閑g(Gdead狀態(tài))稍味,
如果本地p中沒有,會(huì)獲取全局空閑的g(schedt.gFree),
仍沒有就會(huì)在堆上創(chuàng)建一個(gè)初始棧為2k大小的g,
初始化時(shí)是后者荠卷,直接在堆上創(chuàng)建一個(gè)g用來(lái)執(zhí)行runtime.main.
runtime.mstart:
啟動(dòng)m,并進(jìn)行g(shù)的調(diào)度(循環(huán)調(diào)度)
上面介紹了每步函數(shù)的解釋模庐,細(xì)節(jié)要復(fù)雜的多,上面函數(shù)中介紹了m0和所有p的初始化油宜,至于g0掂碱,其實(shí)會(huì)在入口用匯編在棧上初始化的。啟動(dòng)時(shí)其棧大小約為64k(65432字節(jié))慎冤。m0和g0的相互引用也用是在這時(shí)確立的疼燥,至此,m0,g0,allp[0]的關(guān)系確立蚁堤。
調(diào)度模型
總覽:
其中:
G:g結(jié)構(gòu)體對(duì)象醉者,代表Goroutine。每個(gè)G代表一個(gè)待執(zhí)行任務(wù)披诗。
M:m結(jié)構(gòu)體對(duì)象撬即,代表工作線程(每個(gè)工作線程都有一個(gè)m與之對(duì)應(yīng))
P:p結(jié)構(gòu)體對(duì)象,代表處理器(Processor)呈队。
程序啟動(dòng)后會(huì)創(chuàng)建跟cpu核數(shù)相等的P(也可以自己更改剥槐,一般不修改)。每個(gè)P都持有一個(gè)待運(yùn)行G的環(huán)形隊(duì)列(即本地運(yùn)行隊(duì)列)宪摧。
M-P-G調(diào)度是在用戶態(tài)完成粒竖。其中M和G的關(guān)系是多對(duì)多(M:N)颅崩。即M個(gè)線程負(fù)責(zé)對(duì)N個(gè)G進(jìn)行調(diào)度,內(nèi)核對(duì)M個(gè)線程進(jìn)行調(diào)度蕊苗。
goroutine調(diào)度
循環(huán)調(diào)度沿后,搶占調(diào)度(go 1.14開始實(shí)現(xiàn)了基于信號(hào)的搶占式調(diào)度)。
schedule()->execute()->gogo()->g.sched.pc()->goexit()->goexit1->goexit0()->schedule()
其中:
1.schedule()函數(shù)主要為了找尋一個(gè)可執(zhí)行的g:
1.每經(jīng)過61輪調(diào)度則從全局運(yùn)行隊(duì)列中獲取g進(jìn)行執(zhí)行
2.在本地運(yùn)行隊(duì)列中獲取g進(jìn)行執(zhí)行
3.如果上面兩步都沒有找到則會(huì)一直找(阻塞)岁歉,直到找到一個(gè)可執(zhí)行的g.
這個(gè)階段會(huì)在嘗試本地運(yùn)行隊(duì)列得运、全局運(yùn)行隊(duì)列、netpoll锅移、竊取其他p的運(yùn)行隊(duì)列找到一個(gè)可執(zhí)行的g
2.execute()函數(shù)主要設(shè)置當(dāng)前線程的curg熔掺,關(guān)聯(lián)當(dāng)前待執(zhí)行的g的m為當(dāng)前線程,更改g的狀態(tài)從_Grunnable為_Grunning
3.gogo()函數(shù)是匯編語(yǔ)言編寫:
切換到當(dāng)前的g(切換棧g0->g非剃,恢復(fù)g.sched結(jié)構(gòu)體中保存的寄存器的值到cpu寄存器中)
讓cpu真正執(zhí)行當(dāng)前g(執(zhí)行入口函數(shù)為g.sched.pc,即pc寄存器置逻,下一條指令待執(zhí)行的入口地址)。
4.g.sched.pc()备绽,對(duì)于我們這個(gè)程序券坞,就是main goroutine,入口函數(shù)為runtime.main:
1.啟動(dòng)一個(gè)線程執(zhí)行sysmon函數(shù),負(fù)責(zé)整個(gè)程序的netpoll監(jiān)控肺素,gc恨锚,搶占調(diào)度(對(duì)陷入阻塞系統(tǒng)調(diào)用的g釋放p,對(duì)長(zhǎng)時(shí)間運(yùn)行(>10ms)的g進(jìn)行搶占)等倍靡。該線程獨(dú)立運(yùn)行(無(wú)需p猴伶,循環(huán)運(yùn)行)
4.main包初始化
import的包也會(huì)在這個(gè)階段初始化
5.執(zhí)行main.main函數(shù)(我們定義的main函數(shù))
6.從main.main函數(shù)返回后,執(zhí)行系統(tǒng)調(diào)用退出進(jìn)程
主goroutine結(jié)束后塌西,我們的程序就結(jié)束了他挎,這也就是為啥我們?cè)趍ain函數(shù)中啟動(dòng)了一個(gè)goroutine,如果沒有做chan對(duì)協(xié)程進(jìn)行數(shù)據(jù)接收捡需,沒看到協(xié)程執(zhí)行結(jié)果的原因办桨。
對(duì)于非main goroutine,執(zhí)行完fn(即g.sched.pc)后:
goexit函數(shù):執(zhí)行runtime.goexit1函數(shù)
goexit1函數(shù):mcall切換到g0棧執(zhí)行runtime.goexit0函數(shù)
goexit0函數(shù):g放入gFree隊(duì)列重用站辉,進(jìn)行下一輪循環(huán)調(diào)度呢撞。
網(wǎng)絡(luò)IO
同樣實(shí)現(xiàn)了epoll/kqueue等系統(tǒng)調(diào)用,底層使用了匯編實(shí)現(xiàn)饰剥。如epoll相關(guān)函數(shù):
我們用netpoller來(lái)稱呼它,它把goroutine和io多路復(fù)用結(jié)合起來(lái)狸相。
通過netpoll()就可以獲取fd活躍的goroutine列表。
一些go的冷知識(shí):
1.m0是做什么的捐川?m0和別的m有什么區(qū)別?
1.如字面所見逸尖,m0是第一個(gè)被創(chuàng)建的線程古沥。
2.m0的作用跟別的m一樣瘸右,都是系統(tǒng)線程,cpu分配時(shí)間片執(zhí)行任務(wù)的線程岩齿。
3.m上限是10000個(gè)太颤,g只有和m綁定后才能真正執(zhí)行。
2.g0到底是做什么的盹沈,g0和別的g有什么區(qū)別龄章,g0是否也會(huì)被調(diào)度?
1.如字面所見乞封,g0是第一個(gè)被創(chuàng)建的g做裙,但它不是普通的g,并不會(huì)被調(diào)度.
2.g0的作用就是提供一個(gè)棧供runtime代碼執(zhí)行肃晚,典型的就是mcall()和systemstack()這兩個(gè)函數(shù)锚贱,都是切換到g0棧執(zhí)行函數(shù),不同的是:前者只能由非g0發(fā)起切換到g0棧執(zhí)行函數(shù)关串,并且不會(huì)跳轉(zhuǎn)回來(lái)拧廊;而后者可以在g或g0發(fā)起切換。如果當(dāng)前已經(jīng)在g0棧則直接執(zhí)行晋修,否則會(huì)切換到g0棧執(zhí)行函數(shù)吧碾,在函數(shù)執(zhí)行完后切回到現(xiàn)在正在執(zhí)行的代碼繼續(xù)執(zhí)行后續(xù)代碼。
3.每個(gè)m都有一個(gè)g0墓卦。
4.g0跟別的g不一樣倦春。首先初始化的棧大小不一樣,普通g初始化棧2k大小趴拧,g0初始化棧大小有兩種情況:將近64k大小和8k大小溅漾。在新的m建立的時(shí)候,非cgo情況下對(duì)應(yīng)的g0會(huì)分配8k大小的棧著榴。
5.棧的位置不同添履。普通的g會(huì)在堆上分配棧空間脑又,而g0會(huì)在系統(tǒng)棧上分配暮胧。
4.對(duì)于go程序,啟動(dòng)后會(huì)創(chuàng)建多少個(gè)線程问麸?
各個(gè)平臺(tái)不一樣往衷;對(duì)于windows平臺(tái),會(huì)在osinit階段就提前創(chuàng)建好一些線程严卖,對(duì)于linux平臺(tái)席舍,在執(zhí)行到runtime.main前,只有一個(gè)線程哮笆,后面創(chuàng)建線程場(chǎng)景:
1.在創(chuàng)建goroutine時(shí)會(huì)根據(jù)需要?jiǎng)?chuàng)建線程来颤。
2.runtime階段創(chuàng)建線程汰扭,如啟動(dòng)sysmon系統(tǒng)監(jiān)控線程,cgo調(diào)用啟動(dòng)線程startTemplateThread等福铅。
3.cgo執(zhí)行時(shí)萝毛,多個(gè)cgo同時(shí)執(zhí)行,每個(gè)都會(huì)需要一個(gè)線程滑黔。
4.在調(diào)度go協(xié)程時(shí)笆包,p找不到需要空閑的m進(jìn)行執(zhí)行時(shí)。典型場(chǎng)景如web開發(fā)中略荡,goroutine執(zhí)行了阻塞的syscall調(diào)用庵佣,還有新到的go協(xié)程需要處理時(shí)。
最多創(chuàng)建10000個(gè)
5.p,m,g在程序運(yùn)行過程中會(huì)改變嗎撞芍?
p確定后就不會(huì)改變了秧了,為cpu核心數(shù)量(除非人為調(diào)整),存入allp中序无。
g和m會(huì)增加验毡,但不會(huì)減少。g無(wú)上限帝嗡,跟內(nèi)存有關(guān)晶通。空閑的g會(huì)放入gFree里(p的gFree為本地空閑隊(duì)列哟玷;schedt的gFree為全局空閑隊(duì)列)狮辽;
m最多10000個(gè),存入allm中巢寡。
6.在做高性能web開發(fā)時(shí)喉脖,需要協(xié)程池嗎?
在面對(duì)高并發(fā)時(shí)抑月,如果不限制g的數(shù)量树叽,每個(gè)請(qǐng)求一個(gè)g的話,則本地p滿了后(每個(gè)p中存256個(gè))谦絮,就會(huì)放入全局隊(duì)列中题诵,大量的g會(huì)增加gc掃描壓力,同時(shí)會(huì)占用大量?jī)?nèi)存层皱,大量全局p會(huì)有鎖訪問性锭。
所以有必要限制g的數(shù)量。而我們其實(shí)無(wú)法對(duì)goroutine進(jìn)行控制的叫胖,而go調(diào)度器會(huì)自己復(fù)用gFree里的goroutine草冈。
所以協(xié)程池更準(zhǔn)確的叫法應(yīng)該是消費(fèi)池(請(qǐng)求如同生產(chǎn),我們處理請(qǐng)求如同消費(fèi))。
所以我們要做的就是1.盡可能的減少堆內(nèi)存分配及對(duì)內(nèi)存復(fù)用(pool)怎棱,
2.避免阻塞系統(tǒng)調(diào)用方淤,
3.優(yōu)化下游及算法響應(yīng)時(shí)間,
4.做好限流,限制g的數(shù)量蹄殃,
如果做了上面這些后,仍然都是有效訪問你踩,且壓力很大诅岩,那就增加機(jī)器吧。
對(duì)比
1.Openresty啟動(dòng)后带膜,每個(gè)cpu核心綁定一個(gè)進(jìn)程吩谦,而對(duì)于Go來(lái)說,每個(gè)工作線程對(duì)應(yīng)一個(gè)cpu核心膝藕,有異曲同工之妙式廷。
2.可以看出,go的調(diào)度模型要復(fù)雜的多。
Openresty是基于nginx事件的協(xié)作式調(diào)度(應(yīng)避免長(zhǎng)時(shí)間的cpu密集型計(jì)算導(dǎo)致的“熱循環(huán)”)
Go實(shí)現(xiàn)了一套高效的P-M-G調(diào)度(基于信號(hào)的搶占式調(diào)度(1.14開始))
3.對(duì)于網(wǎng)絡(luò)成面芭挽,底層都使用多路IO復(fù)用提升web性能滑废。
4.在作為高性能web服務(wù)器時(shí),都應(yīng)避免阻塞的系統(tǒng)調(diào)用:
如果涉及到耗時(shí)很長(zhǎng)的阻塞系統(tǒng)調(diào)用袜爪,
對(duì)于Openresty來(lái)說蠕趁,當(dāng)前協(xié)程一直占用cpu,導(dǎo)致進(jìn)程直接被阻塞辛馆,導(dǎo)致處理性能大幅下降俺陋;
對(duì)于Go來(lái)說,當(dāng)前goroutine陷入阻塞系統(tǒng)調(diào)用后昙篙,雖然p會(huì)被釋腊状,但是工作線程同樣會(huì)陷入,對(duì)于別的要處理的goroutine苔可,發(fā)現(xiàn)沒有空閑工作線程缴挖,就會(huì)持續(xù)創(chuàng)建工作線程,大量的線程會(huì)大幅增加上下文切換硕蛹,導(dǎo)致性能下降醇疼。