如何用 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 的测试。
参考资料
如何用 Go 实现热重启的更多相关文章
- Golang服务器热重启、热升级、热更新(safe and graceful hot-restart/reload http server)详解
		服务端代码经常需要升级,对于线上系统的升级常用的做法是,通过前端的负载均衡(如nginx)来保证升级时至少有一个服务可用,依次(灰度)升级. 而另一种更方便的方法是在应用上做热重启,直接更新源码.配置 ... 
- prometheus热重启
		prometheus启动命令添加参数 --web.enable-lifecycle 然后热重启:curl -XPOST http://localhost:9090/-/reload 
- [4G]4G模块的热重启
		最近在调试4G模块,发现在开机启动时执行的AT指令会概率性的出现返回杂乱字符串的问题.想尽了各种办法还是行不通,在系统中使用minicom敲AT指令就不会有问题,开始怀疑是串口初始化的问题,修改了很多 ... 
- Go 如何实现热重启
		https://mp.weixin.qq.com/s/UVZKFmv8p4ghm8ICdz85wQ Go 如何实现热重启 原创 zhijiezhang 腾讯技术工程 2020-09-09 
- 说一下我认识的*nix下的服务器热重启
		步骤: 第一: 收到SIGTERM以后现在的服务器监听socket停止accept 但是并没有停止listen,这个很关键.(所以客户端发起的tcp连接的syn得不到synack,只是继续等待,而不会 ... 
- Windows7添加SSD硬盘后重启卡住正在启动
		楼主办公电脑,原来只配置了一块机械硬盘,用着总很不顺心,于是说服领导给加了块SSD固态硬盘. 操作如下: 1.在PE下分区格式化新固态硬盘,将原来机械硬盘的C盘GHOST备份后还原到新固态硬盘: 2. ... 
- Golang学习--平滑重启
		在上一篇博客介绍TOML配置的时候,讲到了通过信号通知重载配置.我们在这一篇中介绍下如何的平滑重启server. 与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只 ... 
- Windows 7 添加SSD硬盘后重启卡住正在启动
		楼主办公电脑,原来只配置了一块机械硬盘,用着总很不顺心,于是说服领导给加了块SSD固态硬盘. 操作如下: 1.在PE下分区格式化新固态硬盘,将原来机械硬盘的C盘GHOST备份后还原到新固态硬盘: 2. ... 
- SpringBoot 在IDEA中实现热部署(实用版)(引入)
		SpringBoot 在IDEA中实现热部署(实用版) 引用:https://www.jianshu.com/p/f658fed35786 好的热部署让开发调试事半功倍,这样的“神技能”怎么能错过呢, ... 
随机推荐
- prometheus学习系列五: Prometheus配置文件
			在prometheus监控系统,prometheus的职责是采集,查询和存储和推送报警到alertmanager.本文主要介绍下prometheus的配置文件. 全局配置文件简介 默认配置文件 [ro ... 
- Windows Server 2008 R2 忘记密码的处理方法
			这篇文章主要介绍了Windows Server 2008 R2 忘记密码的处理方法,一般两种方法,一种是软件方法一种是通过系统安装盘实现的,这里久违大家分享一下需要的朋友可以参考下 遗忘Windows ... 
- oracle 字符集安装错了,修改字符集  及创建用户 表空间 ,删除用户及所有的表
			1.首先以sysdba的身份登录上去 conn /as sysdba 2.关闭数据库shutdown immediate; 3.以mount打来数据库,startup mount 4.设置sessio ... 
- 项目Beta冲刺(团队)——05.25(3/7)
			项目Beta冲刺(团队)--05.25(3/7) 格式描述 课程名称:软件工程1916|W(福州大学) 作业要求:项目Beta冲刺(团队) 团队名称:为了交项目干杯 作业目标:记录Beta敏捷冲刺第3 ... 
- 74HC595 8位移位寄存器的使用小结
			请查看我的博客园文章,比较详细. https://www.cnblogs.com/CodeWorkerLiMing/p/11964258.html 
- Codechef July Challenge 2019 Snake and Apple Tree
			费用流.把每个方格拆成 $T$ 个点,$t$ 时刻一个方格向周围四个方格的 $t + 1$ 的点连一条容量为 $1$ 费用为 $0$ 的边,向自身的 $t + 1$ 连一条容量为 $1$ 费用为该方格 ... 
- day007-python函数之课后作业讲解
			作业一: 需求:写函数,计算传入字符串中的[数字].[字母].[空格]以及[其他]的个数 #!/usr/bin/env python # -*- coding:utf-8 -*- #先定义一个函数 d ... 
- OpenCV 学习笔记(11)像素级别指针操作
			//优化两图的连接处,使得拼接自然 void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst) { int start = MIN(c ... 
- 第八次 Java 作业 重写正方形周长方法
			# 题目 编写一个应用程序,创建一个矩形类,类中具有长.宽两个成员变量和求周长的方法. 再创建一个矩形类的子类——正方形类,类中定义求面积方法.重写求周长的方法. 在主类中,输入一个正方形边长,创建正 ... 
- paython基础-格式化输出
			目录 %s %r %d 及其他%... formate f"{变量}" 详细查找:https://docs.python.org/3/library/string.html#for ... 
