搜索
简帛阁>技术文章>如何用 Go 实现热重启

如何用 Go 实现热重启

热重启

热重启(Zero Downtime),指新老进程无缝切换,在替换过程中可保持对 client 的服务。

原理

  • 父进程监听重启信号
  • 在收到重启信号后,父进程调用 fork ,同时传递 socket 描述符给子进程
  • 子进程接收并监听父进程传递的 socket 描述符
  • 在子进程启动成功之后,父进程停止接收新连接,同时等待旧连接处理完成(或超时)
  • 父进程退出,热重启完成

实现

package main

import (
    "context"
    "errors"
    "flag"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    server   *http.Server
    listener net.Listener = nil

    graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
    message  = flag.String("message", "Hello World", "message to send")
)

func handler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(5 * time.Second)
    w.Write([]byte(*message))
}

func main() {
    var err error

    // 解析参数
    flag.Parse()

    http.HandleFunc("/test", handler)
    server = &http.Server{Addr: ":3000"}

    // 设置监听器的监听对象(新建的或已存在的 socket 描述符)
    if *graceful {
        // 子进程监听父进程传递的 socket 描述符
        log.Println("listening on the existing file descriptor 3")
        // 子进程的 0, 1, 2 是预留给标准输入、标准输出、错误输出,故传递的 socket 描述符
        // 应放在子进程的 3
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f)
    } else {
        // 父进程监听新建的 socket 描述符
        log.Println("listening on a new file descriptor")
        listener, err = net.Listen("tcp", server.Addr)
    }
    if err != nil {
        log.Fatalf("listener error: %v", err)
    }

    go func() {
        err = server.Serve(listener)
        log.Printf("server.Serve err: %v\n", err)
    }()
    // 监听信号
    handleSignal()
    log.Println("signal end")
}

func handleSignal() {
    ch := make(chan os.Signal, 1)
    // 监听信号
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        log.Printf("signal receive: %v\n", sig)
        ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM: // 终止进程执行
            log.Println("shutdown")
            signal.Stop(ch)
            server.Shutdown(ctx)
            log.Println("graceful shutdown")
            return
        case syscall.SIGUSR2: // 进程热重启
            log.Println("reload")
            err := reload() // 执行热重启函数
            if err != nil {
                log.Fatalf("graceful reload error: %v", err)
            }
            server.Shutdown(ctx)
            log.Println("graceful reload")
            return
        }
    }
}

func reload() error {
    tl, ok := listener.(*net.TCPListener)
    if !ok {
        return errors.New("listener is not tcp listener")
    }
    // 获取 socket 描述符
    f, err := tl.File()
    if err != nil {
        return err
    }
    // 设置传递给子进程的参数(包含 socket 描述符)
    args := []string{"-graceful"}
    cmd := exec.Command(os.Args[0], args...)
    cmd.Stdout = os.Stdout         // 标准输出
    cmd.Stderr = os.Stderr         // 错误输出
    cmd.ExtraFiles = []*os.File{f} // 文件描述符
    // 新建并执行子进程
    return cmd.Start()
}

我们在父进程执行 cmd.ExtraFiles = []*os.File{f} 来传递 socket 描述符给子进程,子进程通过执行 f := os.NewFile(3, "") 来获取该描述符。值得注意的是,子进程的 0 、1 和 2 分别预留给标准输入、标准输出和错误输出,所以父进程传递的 socket 描述符在子进程的顺序是从 3 开始。

测试

编译上述程序为 main ,执行 ./main -message "Graceful Reload" ,访问 http://localhost:3000/test ,等待 5 秒后,我们可以看到 Graceful Reload 的响应。

通过执行 kill -USR2 [PID] ,我们即可进行 Graceful Reload 的测试。

通过执行 kill -INT [PID] ,我们即可进行 Graceful Shutdown 的测试。

参考资料

  • gracehttp: 优雅重启 Go 程序
  • Golang服务器热重启、热升级、热更新详解
  • 阅读原文
 
热重启热重启(ZeroDowntime),指新老进程无缝切换,在替换过程中可保持对client的服务。原理父进程监听重启信号在收到重启信号后,父进程调用fork,同时传递socket描述符给子进程子进
packagemainimport(fmt)funcprint(nint,xrune,yrune)(){fmtPrintf(movingdisk%dfrompole%ctopole%c\n,n,x,y
最近发现golang社区里出了一个新星的微服务框架,来自好未来,光看这个名字,就很有奔头,之前,也只是玩过gomicro,其实真正的还没有在项目中运用过,只是觉得微服务,grpc这些很高大尚,还没有在
publicclassBaseDistributedLock{privatefinalZkClientExtclient;privatefinalStringpath;privatefinalStri
大家可能会想,程序和第三方提供了很多压缩方式,何必自己写压缩代码呢?不错,GZIP这样的压缩工具很多,可是在某些情况下(文本内容小且字符不重复),GZIP压缩后会比原始文本还要大。所以在某些特殊
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据。算法描述:⒈从第一个元素开始,该元素可以认为已经被排序⒉取出下一个元素,在已经排序的元素序列中从后向
多线程线程首先说下线程:线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条
主要实现目标:为多个指定的程序实现统一的老板键,一键隐藏多个指定的应用程序的窗口及任务栏。1获取所有顶层窗口importwin32guihwnd_titledict()defget_all_hwnd
何用组件实现自动发送电子邮件?JMailUploadAutoFormasp<html><body><fontface"verdana,arial"size"2">
需求我们的数据表有多个维度,任意多个维度组合后进行groupby可能会产生一些”奇妙”的反应,由于不确定怎么组合,就需要将所有的组合都列出来进行尝试。抽象一下就是从一个集合中取出任意元素,形成唯一的组