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