Nginx 學(xué)習(xí)筆記3 高并發(fā)與go

最近和春暉、劉丁討論定時(shí)器的問題最筒,又仔細(xì)看了下 go timer 兩個(gè)版本的實(shí)現(xiàn),再結(jié)合 epoll 事件驅(qū)動(dòng)蔚叨,對(duì)比 Nginx, 實(shí)現(xiàn)方式如出一轍床蜘。只不過 go 的是無(wú)阻塞順序編程,Nginx 異步回調(diào)蔑水。

高并發(fā)

老生常談了邢锯,什么是“高并發(fā)編程”呢?核心只有兩個(gè)搀别,epollNonBlock. Nginx 和 go 實(shí)現(xiàn)方式太像了丹擎,go runtime 庫(kù)所提供的接口都是無(wú)阻塞的,用 epoll 來(lái)實(shí)現(xiàn)事件驅(qū)動(dòng)歇父,效率非常高蒂培,后面的定時(shí)器就是典型案例。先舉一個(gè) openresty 的例子榜苫,這本書蠻不錯(cuò)护戳,以后好好研究下:

location /sleep_1 {
    default_type 'text/plain';
    content_by_lua_block {
        ngx.sleep(0.01)
        ngx.say("ok")
    }
}

location /sleep_2 {
    default_type 'text/plain';
    content_by_lua_block {
        function sleep(n)
            os.execute("sleep " .. n)
        end
        sleep(0.01)
        ngx.say("ok")
    }
}

上面的配置,很好理解垂睬,兩個(gè) location 都是 sleep(0.01) 秒操作媳荒,但是區(qū)別在哪呢抗悍?先看壓測(cè)

?  nginx git:(master) ab -c 10 -n 20  http://127.0.0.1/sleep_1
...
Requests per second:    860.33 [#/sec] (mean)
...
?  nginx git:(master) ab -c 10 -n 20  http://127.0.0.1/sleep_2
...
Requests per second:    56.87 [#/sec] (mean)
...

性能差距 10 倍,原因就在于 sleep1 使用 openresty 提供的非阻塞 sleep 操作肺樟,執(zhí)行的時(shí)候會(huì)導(dǎo)致協(xié)程切換檐春,出讓 cpu, 但是 sleep2 調(diào)用了系統(tǒng)函數(shù),這是阻塞的么伯,cpu 空轉(zhuǎn)瓢省,openresty lua 開發(fā)的坑也很多。

阻塞 Block 是高并發(fā)的敵人寺庄,go 同理耀石,很多人認(rèn)為 goroutine 很歷害,但是一遇到 cgo, 或是需要系統(tǒng)調(diào)用就會(huì)出問題硬爆,阻塞操作占用了大量的 m, 系統(tǒng)線程猛增欣舵,這時(shí) gc 又會(huì)出來(lái)?yè)v亂。借用 qyuhen 老師的一句話缀磕,每寫一行代碼缘圈,都得知道背后發(fā)生了什么。為了搞清楚 go 背后的原理袜蚕,最近 春暉 在翻譯 go-internal糟把,有興趣的可以看看,需要有匯編和 go runtime 知識(shí)牲剃。

無(wú)阻塞操作

平時(shí)使用最多的 Read, Write 操作遣疯,go 都做了無(wú)阻塞封裝。Netpoll Accept 連接時(shí)凿傅,先設(shè)置成 SetNonBlock 模式缠犀,再使用 epoll et 邊緣觸發(fā)方式注冊(cè)到 netpoll 中。對(duì)于 Read 操作聪舒,如果當(dāng)前有數(shù)據(jù)辨液,那么讀出后返回。如果沒有箱残,那么調(diào)用 waitRead 將當(dāng)前 goroutine 掛起室梅,讓出 process, 等待網(wǎng)絡(luò)消息或是超時(shí)回調(diào)喚醒

// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
        ......
    for {
        n, err := syscall.Read(fd.Sysfd, p)
        if err != nil {
            n = 0
            if err == syscall.EAGAIN && fd.pd.pollable() {
                if err = fd.pd.waitRead(fd.isFile); err == nil {
                    continue
                }
            }

                ......
        }
        err = fd.eofError(n, err)
        return n, err
    }
}

waitRead 函數(shù)最終會(huì)調(diào)用 netpollblock 函數(shù),并 gopark 在這里疚宇,runtime 釋放當(dāng)前 goroutine 所使用的 process

func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
    gpp := &pd.rg
    if mode == 'w' {
        gpp = &pd.wg
    }
      ......
    if waitio || netpollcheckerr(pd, mode) == 0 {
        gopark(netpollblockcommit, unsafe.Pointer(gpp), "IO wait", traceEvGoBlockNet, 5)
    }
    // be careful to not lose concurrent READY notification
    old := atomic.Xchguintptr(gpp, 0)
    if old > pdWait {
        throw("runtime: corrupted polldesc")
    }
    return old == pdReady
}

那么 Read 函數(shù)是如何繼續(xù)亡鼠?goroutine 如何喚醒的呢?

  1. SetReadDeadline 超時(shí)到時(shí)敷待,定時(shí)器喚醒
  2. Netpoll 收到了消息间涵,觸發(fā)epollin 消息喚醒

SetDeadline,每次對(duì) conn 讀寫前設(shè)置榜揖,并且只對(duì)下一次讀寫生效勾哩。先來(lái)看一下 go 如何實(shí)現(xiàn)

//go:linkname poll_runtime_pollSetDeadline internal/poll.runtime_pollSetDeadline
func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
      ......
    pd.seq++ // invalidate current timers 用來(lái)檢測(cè)當(dāng)前定時(shí)任務(wù)是否過期
    // Reset current timers. 刪除老的任務(wù)
    if pd.rt.f != nil {
        deltimer(&pd.rt)
        pd.rt.f = nil
    }
    if pd.wt.f != nil {
        deltimer(&pd.wt)
        pd.wt.f = nil
    }
    // Setup new timers.
    if d != 0 && d <= nanotime() {
        d = -1
    }
    if mode == 'r' || mode == 'r'+'w' {
        pd.rd = d
    }
    if mode == 'w' || mode == 'r'+'w' {
        pd.wd = d
    }
    if pd.rd > 0 && pd.rd == pd.wd {
        pd.rt.f = netpollDeadline
        pd.rt.when = pd.rd
        // Copy current seq into the timer arg.
        // Timer func will check the seq against current descriptor seq,
        // if they differ the descriptor was reused or timers were reset.
        pd.rt.arg = pd
        pd.rt.seq = pd.seq
        addtimer(&pd.rt)
    } else {
        if pd.rd > 0 {
            pd.rt.f = netpollReadDeadline
            pd.rt.when = pd.rd
            pd.rt.arg = pd
            pd.rt.seq = pd.seq
            addtimer(&pd.rt)
        }
        if pd.wd > 0 {
            pd.wt.f = netpollWriteDeadline
            pd.wt.when = pd.wd
            pd.wt.arg = pd
            pd.wt.seq = pd.seq
            addtimer(&pd.wt)
        }
    }
    ......
}

上面是 SetDeadline 核心部份代碼抗蠢,比較容易理解

  1. 生成 seq 號(hào),這個(gè)序列號(hào)用來(lái)判斷當(dāng)前定時(shí)器是否過期
  2. 查看是否設(shè)置了 pd.rt.f 定時(shí)器回調(diào)函數(shù)思劳,刪除上一次的任務(wù)
  3. 設(shè)置定時(shí)時(shí)間
  4. 將讀|寫任務(wù)加入定時(shí)器迅矛,任務(wù)到期后回調(diào)函數(shù) netpollDeadline
func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
    lock(&pd.lock)
    // Seq arg is seq when the timer was set.
    // If it's stale, ignore the timer event.
    if seq != pd.seq {
        // The descriptor was reused or timers were reset.
        unlock(&pd.lock)
        return
    }
    var rg *g
    if read {
        if pd.rd <= 0 || pd.rt.f == nil {
            throw("runtime: inconsistent read deadline")
        }
        pd.rd = -1
        atomicstorep(unsafe.Pointer(&pd.rt.f), nil) // full memory barrier between store to rd and load of rg in netpollunblock
        rg = netpollunblock(pd, 'r', false)
    }
    var wg *g
    if write {
        if pd.wd <= 0 || pd.wt.f == nil && !read {
            throw("runtime: inconsistent write deadline")
        }
        pd.wd = -1
        atomicstorep(unsafe.Pointer(&pd.wt.f), nil) // full memory barrier between store to wd and load of wg in netpollunblock
        wg = netpollunblock(pd, 'w', false)
    }
    unlock(&pd.lock)
    if rg != nil {
        netpollgoready(rg, 0)
    }
    if wg != nil {
        netpollgoready(wg, 0)
    }
}

最終的定時(shí)任務(wù)到期,執(zhí)行回調(diào) netpollDeadline潜叛,執(zhí)功能也很簡(jiǎn)單:

  1. 判斷 seq 是否是最新的秽褒,對(duì)于長(zhǎng)連接存在多次讀寫交互,正常情況網(wǎng)絡(luò) socket 不會(huì)超時(shí)威兜,那么定時(shí)器觸發(fā)后什么也不做
  2. 根據(jù)讀|寫事件销斟,獲取要喚醒的 goroutine
  3. netpollgoready 喚醒 goroutine, 所謂的喚醒只是標(biāo)記成 可運(yùn)行 狀態(tài),具體執(zhí)行時(shí)間由 go runtime 決定

這是超時(shí)的情況椒舵,對(duì)于正常收到數(shù)據(jù)也很簡(jiǎn)單蚂踊,findrunnable 調(diào)用 netpoll 獲取收到事件的 goroutine, 標(biāo)記成 runnable 可運(yùn)行狀態(tài),具體執(zhí)行時(shí)間由 go runtime 決定笔宿。代碼參考 proc.go findrunnable 函數(shù)犁钟。

定時(shí)器

市面上流行的高效定時(shí)器有三種,go 使用的堆結(jié)構(gòu)泼橘、linux kernel 使用的時(shí)間輪涝动、nginx 紅黑樹。
go 在1.10前使用一個(gè)全局的四叉小頂堆結(jié)構(gòu)侥加,在面對(duì)大量連接時(shí),定時(shí)器性能非常差粪躬,所以很多人實(shí)現(xiàn)了用戶層的定時(shí)器庫(kù)担败,很多公司還做過分享。但是 1.10 引入了 runtime 層的 64 個(gè)定時(shí)器镰官,也就是 64 個(gè)四叉小頂堆定時(shí)器提前,性能提升不少。


golang timer

相比二叉泳唠,更遍平一些狈网,增加刪除都是 O(log4N) 級(jí)別,查詢是O(1)笨腥,但是為了維護(hù)堆結(jié)構(gòu)也要額外操作 O(log4N)

時(shí)間輪有很多變種拓哺,內(nèi)核使用了多級(jí) time wheel, 沒看過內(nèi)核代碼,舉一個(gè)單輪的例子吧脖母,圖片來(lái)自csdn這篇文章

時(shí)間輪

一個(gè)輪有 N 個(gè)刻度士鸥,每個(gè)刻度是一個(gè) t 嘀嗒時(shí)間,假如 N = 60, t = 1s, 那么就是生活中的秒針谆级。時(shí)間輪初始化 N 個(gè)槽烤礁,每個(gè)槽是一個(gè)鏈表讼积,在某一時(shí)刻加入一個(gè)時(shí)間為 T 的超時(shí)事件,cycle = T / t, n = T % t, 其中 cycle 是輪數(shù)脚仔,n 代表當(dāng)前事件插入 current 時(shí)刻后的第 n 個(gè)槽勤众。當(dāng)時(shí)間流逝,指針指向下一個(gè)時(shí)刻鲤脏,遍歷槽內(nèi)鏈表们颜,cycle - 1, 如果為 0 那么回調(diào)當(dāng)前超時(shí)任務(wù)函數(shù),否則繼續(xù)檢查下一個(gè)任務(wù)凑兰。插入刪除超時(shí)任務(wù)時(shí)間復(fù)雜度 O(1)掌桩,查詢是 O(n),但由于己經(jīng)分成多個(gè)槽姑食,所以效率肯定好于 O(n)波岛,多級(jí)時(shí)間輪計(jì)算更復(fù)雜。

紅黑樹有兩篇文章不錯(cuò)音半,nginx紅黑樹詳解, nginx學(xué)習(xí)9-ngx_rbtree_t则拷,整體感覺效率和小頂堆差不多。Nginx 使用紅黑樹做定時(shí)器曹鸠,舉一個(gè)最熟悉的場(chǎng)景煌茬,接收到連接后,如果長(zhǎng)時(shí)間沒收到 http header, 那么 Nginx 會(huì)關(guān)閉這個(gè)連接彻桃。

epoll 驅(qū)動(dòng)

Nginx 啟動(dòng) N 個(gè) worker, 并將 worker 和 cpu 進(jìn)行綁定坛善,每個(gè) worker 有自己的 epoll 和 定時(shí)器,由于沒有進(jìn)程邻眷、線程切換開銷眠屎,性能非常好。最近在看 Nginx 也引入了線程池肆饶,用于處理文件并發(fā)處理的情況改衩,不過線上并沒有用。

Epoll 開發(fā)多注意觸發(fā)模式驯镊,默認(rèn)是 LT 即水平觸發(fā)葫督,只要有數(shù)據(jù)可讀|寫,epoll_wait 返回時(shí)就一直攜帶 FD板惑。而大部分服務(wù)都使用 ET 邊緣觸發(fā)模式橄镜,即從無(wú)數(shù)據(jù)到有數(shù)據(jù),從不可寫到可寫冯乘,狀態(tài)變化才會(huì)觸發(fā) epoll, 如果一次沒有讀完蛉鹿,內(nèi)核里還有待讀數(shù)據(jù),那么 epoll 是不會(huì)觸發(fā)的往湿。所以 ET 使用的正確姿勢(shì)妖异,抱住 FD惋戏,一直讀|寫,直到遇到 EAGAIN他膳、EWOULDBLOCK 錯(cuò)誤响逢。

Nginx 在這里也做了優(yōu)化,如果有大段數(shù)據(jù)要讀取或是發(fā)送棕孙,他會(huì)分多次調(diào)用的舔亭,防止當(dāng)前 worker 其它任務(wù)餓死。上周公司同事將 1.9G 文件寫到一個(gè) git, 恰巧這個(gè)工程是服務(wù)配置庫(kù)蟀俊,每次上線都要拉取配置庫(kù)的壓縮包钦铺,直接將 Nginx 壓跨了。

小結(jié)

高并發(fā)的要點(diǎn)就是無(wú)阻塞 NonBlock, 看 openresty 文檔肢预,官方 lua 實(shí)現(xiàn)的都是無(wú)阻塞的矛洞,有時(shí)間讀讀。

前幾天某個(gè)大牛又提起所謂的 tcp 粘包烫映、拆包問題沼本,明明就是用戶協(xié)義解析問題,非要發(fā)明新名詞锭沟。thrift protobuf 反序列化就是個(gè)好例子抽兆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市族淮,隨后出現(xiàn)的幾起案子辫红,更是在濱河造成了極大的恐慌,老刑警劉巖祝辣,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贴妻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡较幌,警方通過查閱死者的電腦和手機(jī)揍瑟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門白翻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)乍炉,“玉大人,你說我怎么就攤上這事滤馍〉呵恚” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵巢株,是天一觀的道長(zhǎng)槐瑞。 經(jīng)常有香客問我,道長(zhǎng)阁苞,這世上最難降的妖魔是什么困檩? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任祠挫,我火速辦了婚禮,結(jié)果婚禮上悼沿,老公的妹妹穿的比我還像新娘等舔。我一直安慰自己,他們只是感情好糟趾,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布慌植。 她就那樣靜靜地躺著,像睡著了一般义郑。 火紅的嫁衣襯著肌膚如雪蝶柿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天非驮,我揣著相機(jī)與錄音交汤,去河邊找鬼。 笑死院尔,一個(gè)胖子當(dāng)著我的面吹牛蜻展,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邀摆,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼纵顾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了栋盹?” 一聲冷哼從身側(cè)響起施逾,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎例获,沒想到半個(gè)月后汉额,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榨汤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蠕搜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片收壕。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡妓灌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜜宪,到底是詐尸還是另有隱情虫埂,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布圃验,位于F島的核電站掉伏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斧散,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一供常、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鸡捐,春花似錦话侧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鹿寨,卻和暖如春新博,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背脚草。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工赫悄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人馏慨。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓埂淮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親写隶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子倔撞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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