如何用 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 好的热部署让开发调试事半功倍,这样的“神技能”怎么能错过呢, ...
随机推荐
- Spring框架的AOP编程,最通俗的语言解释,易懂易学
第七章:AOP技术 前言: AOP技术是根据动态代理设计模式进行的技术.动态代理技术分jdk动态代理和cglib动态代理 jdk动态代理特点: (1)继承java.lang.reflect.proxy ...
- Kubernetes学习之路(27)之k8s 1.15.2 部署
目录 一.环境准备 二.软件安装 三.部署master节点 四.部署node节点 五.集群状态检测 一.环境准备 IP地址 节点角色 CPU Memory Hostname Docker versio ...
- 【Nginx】将http升级到https并且同时支持http和https两种请求
一.如何将http升级到https 需要满足下面三个: 1.域名 2.nginx 3.SSL证书 一般第三方证书颁发机构下发的证书是收费的,一年好几千. 1) 从腾讯云申请免费的SSL证 ...
- 使用kubeadm安装Kubernetes 1.15.3 并开启 ipvs
一.安装前准备 机器列表 主机名 IP node-1(master) 1.1.1.101 node-2(node) 1.1.1.102 node-3(node) 1.1.1.103 设置时区 cp / ...
- JAVA之Socket通讯
Server.java: Client.java Server console:(先启动服务器,再启动客户端) 服务器读取了客户端发来的hello server: Client console:客户 ...
- 201671030106 何启芝 实验十四 团队项目评审&课程学习总结
项目 内容 这个作业属于哪个课程 >>2016级计算机科学与工程学院软件工程(西北师范大学) 这个作业的要求在哪里 >>实验十四 团队项目评审&课程学习总结 课程学习目 ...
- php静态方法
静态方法 (1)静态方法不能访问这个类中的普通属性,因为那些属性属于一个对象,但可以访问静态属性: (2)从当前类(不是子类)中访问静态方法或属性,可以使用 self 关键字,self 指向当前类,就 ...
- There is no type initializer in Swift----One answer is to use static, it is the same as class final.
“Unlike stored instance properties, you must always give stored type properties a default value. Thi ...
- OpenCV 学习笔记(1-2)cuda8.0+cudnn安装
https://blog.csdn.net/qq_34199125/article/details/78352486 安装过程1.点击安装包一步一步安装即可.如果原来有cuda7.5,因此8.0安装后 ...
- 验证码破解 | Selenium模拟登陆微博
模拟登陆微博相对来说,并不难.验证码是常规的5个随机数字字母的组合,识别起来也比较容易.主要是用到许多Selenium中的知识,如定位标签.输入信息.点击等.如对Selenium的使用并不熟悉,请先移 ...