Go长连接后台平滑重启
目标:tcp长连接的后台服务,重启时,会断开所有客户端的连接。影响范围很大,现想实现重启时,已建立的连接不断开,继续提供服务,直到客户端主动断开连接。而新建立的连接使用更新后的服务。
原理:
1. 更新可执行文件后
2. 向进程发送SIGUSR1信号
3. 进程收到SIGUSR1信号后,停止Accept新连接
4. 启动子进程,在子进程中通过文件索引创建listener,在子进程继续Accept连接
5. 启动子进程后,父进程不立即退出,而且等待所有连接都断开后才退出
示例代码:
package main import ( "flag" "fmt" "log" "net" "os" "os/signal" "sync" "syscall" "time" ) func main() { var listener net.Listener var err error var ok bool isRestart := flag.Bool("restart", false, "restart flag") if *isRestart { fd := 3 // 文件索引 file := os.NewFile(uintptr(fd), "") listener, err = net.FileListener(file) if err != nil { panic(err) } listener, ok = listener.(*net.TCPListener) if !ok { panic(fmt.Sprintf("fd[%d] not tcp listener", fd)) } } else { listener, err = net.Listen("tcp", "0.0.0.0:8080") } // 启动信号处理协程 sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGUSR1) go func() { <-sigChan fmt.Println("Received SIGUSR1, starting smooth restart") _, err = smoothRestart(listener) if err != nil { log.Fatal(err) } }() wg := sync.WaitGroup{} for { conn, err := listener.Accept() if err != nil { fmt.Println(err) break } wg.Add(1) go handleConn(&wg, conn) } wg.Wait() } func handleConn(wg *sync.WaitGroup, conn net.Conn) { defer conn.Close() defer wg.Done() for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { log.Println("Error reading:", err) return } fmt.Println("Received message:", string(buf[:n])) _, err = conn.Write([]byte("Received message")) if err != nil { log.Println("Error writing:", err) return } } } func smoothRestart(listener net.Listener) (net.Listener, error) { // 获取当前程序的PID pid := syscall.Getpid() fmt.Println("current program pid:", pid) tl, ok := listener.(*net.TCPListener) if !ok { panic("not tcp listener") } tl.SetDeadline(time.Now()) os.Args = append(os.Args, "-restart") // 派生子进程 childPid, err := syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{ Env: os.Environ(), Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3}, }) if err != nil { return nil, err } // 输出新程序的PID fmt.Println("New program started, PID:", childPid) return nil, nil }