Golang的Graceful Restart

從1.8開始搬男,Go標(biāo)準(zhǔn)庫(kù)中的net/http支持了GracefulShutdown腺兴,使得進(jìn)程可以把現(xiàn)有請(qǐng)求都處理完之后再退出悼尾,從而最大限度地減少不一致性給服務(wù)端帶來的負(fù)擔(dān)傻铣。如果不做GracefulShutdown章贞,有哪些不一致性呢?簡(jiǎn)單舉個(gè)例子:

服務(wù)T是類似微博的點(diǎn)贊功能,當(dāng)用戶點(diǎn)贊某條微博的時(shí)候,一方面要給點(diǎn)贊數(shù)+1靴迫,另一方面要通知post的作者“XXX贊了你的微博”友酱,同時(shí)還要有策略通知點(diǎn)贊人的粉絲“你關(guān)注的XXX點(diǎn)贊了這條微博”……當(dāng)然這些功能不是一個(gè)事務(wù)证九,而且也不是同步的,應(yīng)該異步來做。所以,最終的流程可能是:

db.IncrPostNumber()
mqA.Send(messageA)
mqB.Send(messageB)
//...

如果不做gracefulShutdown赡麦,在中途的任何一個(gè)步驟時(shí),進(jìn)程被殺掉帕识,都可能造成一些問題泛粹。當(dāng)然就這個(gè)例子來說,往小了說也不是什么大事肮疗,這些問題都可以忍受晶姊。但往大了說,大V通過點(diǎn)贊讓粉絲看到某條微博伪货,這也是收費(fèi)的们衙。結(jié)果廣告主給了錢卻看不到效果,甚至發(fā)現(xiàn)根本沒有粉絲看到碱呼,這是要讓你退錢的蒙挑!

所以做GracefulShutdown,不論對(duì)什么業(yè)務(wù)系統(tǒng)來說巍举,都是很有必要的脆荷。但是本文我們不討論GracefulShutdown,而是討論一個(gè)更進(jìn)一步的話題懊悯,Graceful Restart。

GracefulShutdown和Graceful Restart是什么區(qū)別呢梦皮?從名字上大概就能看出炭分,一個(gè)是優(yōu)雅退出,一個(gè)是優(yōu)雅重啟剑肯。優(yōu)雅退出上面也說了捧毛,重點(diǎn)是保證進(jìn)程退出前處理完當(dāng)下所有的請(qǐng)求。而優(yōu)雅重啟要求更高,它的目標(biāo)是在進(jìn)程重啟時(shí)整個(gè)過程要平滑呀忧,不要讓用戶感受到任何異樣师痕,不要有任何downtime,也就是停機(jī)時(shí)間而账,保證進(jìn)程持續(xù)可用胰坟。因此,gracefulShutdown只是實(shí)現(xiàn)gracefulRestart的一個(gè)必要部分泞辐,gracefulRestart還要求更多笔横。

一種GracefulRestart的方法是,通過部署系統(tǒng)配合nginx來完成咐吼。由于大部分業(yè)務(wù)系統(tǒng)都是掛在nginx之后通過nginx進(jìn)行反向代理的吹缔,因此在重啟某臺(tái)機(jī)器的進(jìn)程A時(shí),可以把該機(jī)器IP從nginx的upstream中摘除掉锯茄,等一段時(shí)間比如1分鐘厢塘,該進(jìn)程差不多也處理完了所以請(qǐng)求,實(shí)際上已經(jīng)處于空閑狀態(tài)了肌幽。這時(shí)就可以kill掉該進(jìn)程并重啟俗冻,等重啟成功之后,再把該機(jī)器的IP加回到nginx對(duì)應(yīng)的upstream中去牍颈。
這種方式是語(yǔ)言迄薄、平臺(tái)無關(guān)的一種技術(shù)方案,但是缺點(diǎn)也很明顯:

  • 首先就是復(fù)雜煮岁,需要部署系統(tǒng)和網(wǎng)關(guān)(nginx)恰到好處地配合讥蔽。開發(fā)人員點(diǎn)擊部署時(shí),部署系統(tǒng)需要通知nginx摘掉某個(gè)upstream的某個(gè)IP画机;然后等進(jìn)程重啟成功之后冶伞,部署系統(tǒng)需要通知nginx在某個(gè)upstream中加上某個(gè)IP。這一整套系統(tǒng)的開發(fā)測(cè)試還是有一定復(fù)雜性的步氏。
  • 其次是等待時(shí)間的未知性响禽。當(dāng)把機(jī)器A摘掉以后過多久進(jìn)程才能處理完請(qǐng)求?10秒荚醒?1分鐘芋类?誰(shuí)也不知道…間隔短了,會(huì)出問題界阁,因?yàn)椴糠终?qǐng)求被卡斷了侯繁;間隔長(zhǎng)了,上線又慢泡躯,而且你還是不能確定是否請(qǐng)求都處理完了(其實(shí)基本上沒問題贮竟,但是理論上無法保證)丽焊。
  • 另一個(gè)問題是壓力陡增。對(duì)于大公司動(dòng)輒幾百臺(tái)的集群咕别,摘一兩臺(tái)無關(guān)緊要技健。但是對(duì)于小公司,比如某個(gè)服務(wù)只有兩臺(tái)機(jī)器惰拱,并且每臺(tái)機(jī)器壓力都挺大雌贱。這時(shí)如果直接摘一臺(tái),所有流量到另一臺(tái)機(jī)器上弓颈,使得那臺(tái)機(jī)器承受不住帽芽,那么可能會(huì)導(dǎo)致整個(gè)服務(wù)不可用。

因此這里引出第二種實(shí)現(xiàn)方式——fd繼承

FD繼承

fd(file descriptor)也就是文件描述符翔冀,是Unix*系統(tǒng)上最常見的概念导街,everything is file。我們基于一個(gè)非诚俗樱基礎(chǔ)的知識(shí)點(diǎn):

進(jìn)程T fork 出子進(jìn)程時(shí)搬瑰,子進(jìn)程會(huì)繼承父進(jìn)程T打開的fd。

進(jìn)程T大概的處理流程類似于:

int sock_fd = createSocketBindTo(":80");
int ok = listen(sock_fd, backlog);
do {
  int connect_sock = accept(sock_fd, &SockStruct, &Addr);
  process(connect_sock);
}

也就是:

  • 構(gòu)建監(jiān)聽某個(gè)端口的socket
  • 不斷從該socket中讀取連接控硼,并處理

這里你可以發(fā)現(xiàn)泽论,如果想要accept到連接,我們只需要socket就夠了卡乾,bind listen這些都是準(zhǔn)備工作翼悴。如果父進(jìn)程把這些工作都做了,子進(jìn)程似乎可以直接從繼承過來的socket上讀取數(shù)據(jù)幔妨。
這里先不說具體實(shí)現(xiàn)細(xì)節(jié)鹦赎,但是大體思路其實(shí)就是上面說的,非常簡(jiǎn)單误堡。進(jìn)程通過環(huán)境變量或者args來判斷是應(yīng)該先Listen再accpet古话,還是直接用繼承來的socket進(jìn)行accept。
這里有個(gè)問題锁施,子進(jìn)程如果在該socket上accept陪踩,主進(jìn)程也accept,那么對(duì)同一個(gè)socket進(jìn)行accept操作并發(fā)安全嗎悉抵?答案是——安全肩狂,這是glibc為我們保證的,正如malloc這類函數(shù)調(diào)用一樣基跑。

下面是一個(gè)簡(jiǎn)單的代碼示例:

package main

import (
    "context"
    "flag"
    "fmt"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
)

var (
    upgrade bool
    ln net.Listener
    server *http.Server
)

func init() {
    flag.BoolVar(&upgrade, "upgrade", false, "user can't use this")
}

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello world from pid:%d, ppid: %d\n", os.Getpid(), os.Getppid())
}

func main() {
    flag.Parse()
    http.HandleFunc("/", hello)
    server = &http.Server{Addr:":8999",}
    var err error
    if upgrade {
        fd := os.NewFile(3, "")
        ln,err = net.FileListener(fd)
        if err != nil {
            fmt.Printf("fileListener fail, error: %s\n", err)
            os.Exit(1)
        }
        fd.Close()
    } else {
        ln, err = net.Listen("tcp", server.Addr)
        if err != nil {
            fmt.Printf("listen %s fail, error: %s\n", server.Addr, err)
            os.Exit(1)
        }
    }
    go func() {
        err := server.Serve(ln)
        if err != nil && err != http.ErrServerClosed{
            fmt.Printf("serve error: %s\n", err)
        }
    }()
    setupSignal()
    fmt.Println("over")
}

func setupSignal() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGTERM)
    sig := <-ch
    switch sig {
    case syscall.SIGUSR2:
        err := forkProcess()
        if err != nil {
            fmt.Printf("fork process error: %s\n", err)
        }
        err = server.Shutdown(context.Background())
        if err != nil {
            fmt.Printf("shutdown after forking process error: %s\n", err)
        }
    case syscall.SIGINT,syscall.SIGTERM:
        signal.Stop(ch)
        close(ch)
        err := server.Shutdown(context.Background())
        if err != nil {
            fmt.Printf("shutdown error: %s\n", err)
        }
    }
}

func forkProcess() error {
    flags := []string{"-upgrade"}
    cmd := exec.Command(os.Args[0], flags...)
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout
    l,_ := ln.(*net.TCPListener)
    lfd,err := l.File()
    if err != nil {
        return err
    }
    cmd.ExtraFiles = []*os.File{lfd,}
    return cmd.Start()
}

代碼中很關(guān)鍵的兩行:

fd := os.NewFile(3, "")
ln,err = net.FileListener(fd)

fd.Close()

3是什么婚温?3其實(shí)就是從父進(jìn)程繼承過來的socket fd。雖然子進(jìn)程可以默認(rèn)繼承父進(jìn)程絕大多數(shù)的文件描述符(除了文件鎖之類的)媳否,但是golang的標(biāo)準(zhǔn)庫(kù)os/exec只默認(rèn)繼承stdin stdout stderr這三個(gè)栅螟。需要讓子進(jìn)程繼承的fd需要在fork之前手動(dòng)放到ExtraFiles中。由于有了stdin 0 stdout 1 stderr 2篱竭,因此其它fd的序號(hào)從3開始力图。

還有一個(gè)可能比較讓人困惑的問題是,fd.Close()是干什么的掺逼,Close它會(huì)有什么影響吃媒。這個(gè)問題直接的答案是,沒有任何影響吕喘,只是為了防止資源泄漏赘那。具體可以看看net.FileListerner的文檔,相關(guān)的知識(shí)點(diǎn)有點(diǎn)多氯质,可以google fcntl和dup2關(guān)鍵字募舟。

當(dāng)子進(jìn)程運(yùn)行起來后,就可以調(diào)用server實(shí)現(xiàn)好的Shutdown方法闻察,來關(guān)停主進(jìn)程了拱礁。

這種方法代來的一個(gè)問題是,當(dāng)主進(jìn)程fork出子進(jìn)程辕漂,然后主進(jìn)程退出后呢灶,子進(jìn)程的父進(jìn)程就變成了1(孤兒進(jìn)程)。如果使用supervisor等工具來監(jiān)聽服務(wù)的話钉嘹,就會(huì)遇到問題(主進(jìn)程退出了立刻又被supervisor拉起來鸯乃,然后端口沖突了)。這時(shí)候就需要使用linux pidfile跋涣。

RE_USEPORT

還有第三種可以做到不停機(jī)重啟的辦法缨睡,那便是使用Linux內(nèi)核的新特性reuseport。以前仆潮,如果多個(gè)進(jìn)程或者線程同時(shí)監(jiān)聽一個(gè)端口宏蛉,只有一個(gè)可以成功,其它都會(huì)返回端口被占用的錯(cuò)誤性置。
新內(nèi)核支持通過setsockopt對(duì)socket進(jìn)行設(shè)置拾并,使得多個(gè)進(jìn)程或者線程可以同時(shí)監(jiān)聽一個(gè)端口,內(nèi)核來進(jìn)行負(fù)載均衡鹏浅。

利用多進(jìn)程模型加上reuseport庫(kù)的支持嗅义,很容易就可以實(shí)現(xiàn)不停機(jī)重啟。
但是隐砸,reuseport也不是萬(wàn)能的靈丹妙藥之碗,它也有自己的問題,在連接建立非常頻繁的場(chǎng)景下季希,由于內(nèi)核使用的算法的局限性褪那,它的性能會(huì)下降很多幽纷。當(dāng)然,這和不停機(jī)重啟沒有任何關(guān)系博敬,只是順便一提友浸,如果僅僅使用reuseport特性實(shí)現(xiàn)gracefulRestart,應(yīng)該不會(huì)遇到這樣的問題偏窝。
nginx高版本也使用了reuseport收恢,關(guān)于它的性能問題,可以參見這篇文章
到底是通過繼承fd還是reuseport來實(shí)現(xiàn)graceful restart祭往,相關(guān)的比較可以參見https://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/伦意,不過結(jié)論基本上認(rèn)為繼承fd更靠譜(當(dāng)然這篇文章得出的結(jié)論也受限于當(dāng)時(shí)golang本身標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)的局限性,使得沒辦法對(duì)Conn進(jìn)行setsockopt硼补,因?yàn)镃onn不是一個(gè)socket對(duì)象而是一個(gè)runtime.NetPoller)

More

現(xiàn)在開源社區(qū)有不少相關(guān)的實(shí)現(xiàn)驮肉,比如:

?著作權(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)店門宛裕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瑟啃,“玉大人,你說我怎么就攤上這事揩尸∮加欤” “怎么了?”我有些...
    開封第一講書人閱讀 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)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼最仑!你這毒婦竟也來了藐俺?” 一聲冷哼從身側(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
  • 我被黑心中介騙來泰國(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)容