Openresty協(xié)程調(diào)度對(duì)比Go協(xié)程調(diào)度

在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)度模型

總覽:

image

其中:

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)行)

2.runtime包初始化

3.啟動(dòng)gc

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)致性能下降醇疼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市法焰,隨后出現(xiàn)的幾起案子秧荆,更是在濱河造成了極大的恐慌,老刑警劉巖埃仪,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乙濒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)颁股,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門么库,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人甘有,你說我怎么就攤上這事诉儒。” “怎么了亏掀?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵忱反,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我滤愕,道長(zhǎng)温算,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任间影,我火速辦了婚禮注竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘魂贬。我一直安慰自己巩割,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布随橘。 她就那樣靜靜地躺著喂分,像睡著了一般。 火紅的嫁衣襯著肌膚如雪机蔗。 梳的紋絲不亂的頭發(fā)上蒲祈,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音萝嘁,去河邊找鬼梆掸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牙言,可吹牛的內(nèi)容都是我干的酸钦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼咱枉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卑硫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蚕断,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤欢伏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后亿乳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硝拧,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡径筏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了障陶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滋恬。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抱究,靈堂內(nèi)的尸體忽然破棺而出恢氯,到底是詐尸還是另有隱情,我是刑警寧澤鼓寺,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布酿雪,位于F島的核電站,受9級(jí)特大地震影響侄刽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朋凉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一州丹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杂彭,春花似錦墓毒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至团秽,卻和暖如春主胧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背习勤。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工踪栋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人图毕。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓夷都,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親予颤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子囤官,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容