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
}
View Code

 

posted @ 2023-04-20 11:26  xiaxiaosheng  阅读(171)  评论(0编辑  收藏  举报