平滑升级的一般思路
- 发布新的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无连接,作为服务端,在读取客户端旧连接发送的数据包时,新旧进程会抢占式读取。对单个服务进程来说,有丢包的假象。
如果你在其上定制了一层维持连接的协议层,可能由于丢包假象,无法维持连接。