如何在Golang中实现热重启功能?

发表时间: 2023-07-23 11:15

热重启

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

原理

父进程监听重启信号

在收到重启信号后, 父进程调用 fork, 同时传递 socket 描述符给子进程

子进程接收并监听父进程传递的 socket 描述符

在子进程启动成功之后, 父进程停止接收新连接, 同时等待旧连接处理完成(或超时)

父进程退出, 热重启完成

实现

> # mkdir hotRestart && cd hotRestart

初始化项目

> # go mod init server

注意: 项目的名称不一定与模块名称(module) 一致

> # vim main.go

package mainimport ("context""errors""flag""log""net""net/http""os""os/exec""os/signal""syscall""time")var (server *http.Serverlistener net.Listener = nilgraceful = 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 描述符// 应放在子进程的 3f := 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 := <-chlog.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")returncase 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...)// 进程转入daemon的方法cmd.Stdout = os.Stdout // 标准输出cmd.Stderr = os.Stderr // 错误输出cmd.ExtraFiles = []*os.File{f} // 文件描述符// 新建并执行子进程return cmd.Start()}

编译

> # go build

> # tree

.

├── go.mod

├── main.go

└── server

注意: 此时编译的二进制执行文件的名称 由 [module] 决定

go mod init [module]

当然, 我们编译时也可以指定名称

go build [-o output] [build flags] [packages]

> # go version

go version go1.16.6 linux/amd64

测试

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

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

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

具体步骤:

> # ./server -message "Graceful Reload"

2021/09/13 18:06:40 listening on a new file descriptor

> # curl http://localhost:3000/test

Graceful Reload

> # ps -ef | grep server

root 6991 12565 0 18:06 pts/14 00:00:00 ./server -message Graceful Reload

> # kill -USR2 6991

> # ps -ef | grep server

root 30783 1 0 18:10 pts/14 00:00:00 ./server -graceful

再次访问

> # curl http://localhost:3000/test

Graceful Reload

我们此时发现 server 进程服务已经实现平滑重启

kill掉进程

> # kill -INT 30783