熱重啟golang服務(wù)器(graceful restart golang http server)

原文:熱重啟golang服務(wù)器(graceful restart golang http server)

服務(wù)端代碼經(jīng)常需要升級(jí)砸泛,對(duì)于線上系統(tǒng)的升級(jí)常用的做法是,通過(guò)前端的負(fù)載均衡(如nginx)來(lái)保證升級(jí)時(shí)至少有一個(gè)服務(wù)可用蛆封,依次(灰度)升級(jí)唇礁。
而另一種更方便的方法是在應(yīng)用上做熱重啟,直接升級(jí)應(yīng)用而不停服務(wù)娶吞。

原理


熱重啟的原理非常簡(jiǎn)單垒迂,但是涉及到一些系統(tǒng)調(diào)用以及父子進(jìn)程之間文件句柄的傳遞等等細(xì)節(jié)比較多。
處理過(guò)程分為以下幾個(gè)步驟:
監(jiān)聽(tīng)信號(hào)(USR2)

  1. 收到信號(hào)時(shí)fork子進(jìn)程(使用相同的啟動(dòng)命令)妒蛇,將服務(wù)監(jiān)聽(tīng)的socket文件描述符傳遞給子進(jìn)程
  2. 子進(jìn)程監(jiān)聽(tīng)父進(jìn)程的socket机断,這個(gè)時(shí)候父進(jìn)程和子進(jìn)程都可以接收請(qǐng)求
  3. 子進(jìn)程啟動(dòng)成功之后,父進(jìn)程停止接收新的連接绣夺,等待舊連接處理完成(或超時(shí))
  4. 父進(jìn)程退出吏奸,升級(jí)完成

細(xì)節(jié)


  • 父進(jìn)程將socket文件描述符傳遞給子進(jìn)程可以通過(guò)命令行,或者環(huán)境變量等
  • 子進(jìn)程啟動(dòng)時(shí)使用和父進(jìn)程一樣的命令行陶耍,對(duì)于golang來(lái)說(shuō)用更新的可執(zhí)行程序覆蓋舊程序
  • server.Shutdown()優(yōu)雅關(guān)閉方法是go1.8的新特性
  • server.Serve(l)方法在Shutdown時(shí)立即返回奋蔚,Shutdown方法則阻塞至context完成,所以Shutdown的方法要寫在主goroutine中

代碼


代碼部分加了些注釋

package main

import (
    "context"
    "errors"
    "flag"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    server   *http.Server
    listener net.Listener
    graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)

func handler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(3 * time.Second)
    w.Write([]byte("hello world233333--!!!!"))
}

func main() {
    flag.Parse()

    http.HandleFunc("/hello", handler)
    server = &http.Server{Addr: ":9999"}

    var err error
    if *graceful {
        log.Print("main: Listening to existing file descriptor 3.")
        // cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
        // when we put socket FD at the first entry, it will always be 3(0+3)
        // 在linux中,值為0泊碑、1坤按、2的fd,分別代表標(biāo)準(zhǔn)輸入馒过、標(biāo)準(zhǔn)輸出臭脓、標(biāo)準(zhǔn)錯(cuò)誤輸出,因?yàn)?0 1 2已經(jīng)被linux使用了
        // 返回具有給定文件描述符和名稱的新文件腹忽。如果fd不是有效的文件描述符来累,則返回值為nil。
        // 3是什么窘奏?3其實(shí)就是從父進(jìn)程繼承過(guò)來(lái)的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開始求冷。
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f) //返回與打開的文件f對(duì)應(yīng)的網(wǎng)絡(luò)偵聽(tīng)器的副本瘤运。

        log.Printf("ln:addr=%v", listener.Addr().String())

        // f.Close() 是干什么的窍霞,Close它會(huì)有什么影響匠题。這個(gè)問(wèn)題直接的答案是,沒(méi)有任何影響但金,只是為了防止資源泄漏韭山。
        f.Close()

        // 在測(cè)試發(fā)現(xiàn),在kill -USR2原有的服務(wù)結(jié)束時(shí)冷溃,才會(huì)響應(yīng)新打開的服務(wù)
    } else {
        log.Print("main: Listening on a new file descriptor.")
        listener, err = net.Listen("tcp", server.Addr)
    }

    if err != nil {
        log.Fatalf("listener error: %v", err)
    }

    go func() {
        // server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
        err = server.Serve(listener)
        log.Printf("server.Serve err: %v\n", err)
    }()
    signalHandler()
    log.Printf("signal end")
}

func reload() error {
    tl, ok := listener.(*net.TCPListener)
    if !ok {
        return errors.New("listener is not tcp listener")
    }

    f, err := tl.File()
    if err != nil {
        return err
    }

    args := []string{"-graceful"}
    cmd := exec.Command(os.Args[0], args...)
    log.Printf("os.args[0]:%s\n", os.Args[0]) //可執(zhí)行文件名
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    // put socket FD at the first entry
    cmd.ExtraFiles = []*os.File{f}
    return cmd.Start()
}

func signalHandler() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        log.Printf("signal: %v", sig)

        // timeout context for shutdown
        ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM:
            // stop
            log.Printf("stop")
            signal.Stop(ch)
            server.Shutdown(ctx)
            log.Printf("graceful shutdown")
            return
        case syscall.SIGUSR2:
            // reload
            log.Printf("reload")
            err := reload()
            if err != nil {
                log.Fatalf("graceful restart error: %v", err)
            }
            server.Shutdown(ctx)
            log.Printf("graceful reload")
            return
        }
    }
}

% go build main.go
% ./main
% kill -USR2 77458  #熱重啟

os.NewFile(3, "") 為什么是3钱磅,可以參考這篇文章Golang的Graceful Restart

systemd & supervisor

父進(jìn)程退出之后,子進(jìn)程會(huì)掛到1號(hào)進(jìn)程上面似枕。這種情況下使用systemd和supervisord等管理程序會(huì)顯示進(jìn)程處于failed的狀態(tài)盖淡。解決這個(gè)問(wèn)題有兩個(gè)方法:

  • 使用pidfile,每次進(jìn)程重啟更新一下pidfile凿歼,讓進(jìn)程管理者通過(guò)這個(gè)文件感知到mainpid的變更褪迟。
  • 起一個(gè)master來(lái)管理服務(wù)進(jìn)程,每次熱重啟master拉起一個(gè)新的進(jìn)程答憔,把舊的kill掉味赃。這時(shí)master的pid沒(méi)有變化,對(duì)于進(jìn)程管理者來(lái)說(shuō)進(jìn)程處于正常的狀態(tài)虐拓。一個(gè)簡(jiǎn)潔的實(shí)現(xiàn)

References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末心俗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌城榛,老刑警劉巖揪利,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異狠持,居然都是意外死亡土童,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門工坊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)献汗,“玉大人,你說(shuō)我怎么就攤上這事王污∷倌牵” “怎么了底燎?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我讼呢,道長(zhǎng),這世上最難降的妖魔是什么拘泞? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任舷胜,我火速辦了婚禮,結(jié)果婚禮上里覆,老公的妹妹穿的比我還像新娘丧荐。我一直安慰自己,他們只是感情好喧枷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布虹统。 她就那樣靜靜地躺著,像睡著了一般隧甚。 火紅的嫁衣襯著肌膚如雪车荔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天戚扳,我揣著相機(jī)與錄音忧便,去河邊找鬼。 笑死帽借,一個(gè)胖子當(dāng)著我的面吹牛珠增,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宜雀,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼切平,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了辐董?” 一聲冷哼從身側(cè)響起悴品,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后苔严,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體定枷,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年届氢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了欠窒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡退子,死狀恐怖岖妄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寂祥,我是刑警寧澤荐虐,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站丸凭,受9級(jí)特大地震影響福扬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惜犀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一铛碑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虽界,春花似錦汽烦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至颈将,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間言疗,已是汗流浹背晴圾。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留噪奄,地道東北人死姚。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像勤篮,于是被迫代替她去往敵國(guó)和親都毒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355