平滑升級的一般思路
- 發佈新的bin文件去覆蓋老的bin文件(如只需優雅重啓,可以跳過這一步)
- 發送一個(USR2)信號量,告訴正在運行的進程,進行重啓
- 正在運行的進程收到信號後,會以子進程的方式啓動新的bin文件
- 子進程啓動成功之後,老進程停止接收新的連接,等待舊連接處理完成(或超時)
- 父進程退出,升級完成
golang庫
tcp應該有很多成熟的方案,可參照facebook庫
注意點
平滑重啓有個特點:兩個相互獨立的進程存在同時 bind、listen 相同的 IP + 端口,對於各個操作系統及版本驗證如下:
- linux 內核 4.13.0
設置參數SO_REUSEADDR、SO_REUSEPORT,可以bind、listen成功,新的連接由系統分配到某一個進程; - linux 內核 2.6.32
不支持SO_REUSEPORT,僅設置參數SO_REUSEADDR時,新進程 bind 失敗。經查資料linux 3.9及以後開始支持參數SO_REUSEPORT;
參考資料
udp
這裏主要總結下udp碰到的坑
udp繼承套接字
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)
f := os.NewFile(3, "")
listener, err = net.FilePacketConn(f)
f.Close()
} else {
log.Print("main: Listening on a new file descriptor.")
// listener, err = net.Listen("tcp", addr)
listener, err = net.ListenPacket("udp", addr)
}
udp啓動子進程繼承套接字
tl, ok := listener.(*net.UDPConn)
if !ok {
return errors.New("listener is not tcp listener")
}
f, err := tl.File()
if err != nil {
return err
}
args := []string{"-graceful", "-seq", "1"}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// put socket FD at the first entry
cmd.ExtraFiles = []*os.File{f}
cmd.Start()
由於udp無連接,作爲服務端,在讀取客戶端舊連接發送的數據包時,新舊進程會搶佔式讀取。對單個服務進程來說,有丟包的假象。
如果你在其上定製了一層維持連接的協議層,可能由於丟包假象,無法維持連接。