exec go 重启_如何用 Go 实现热重启

本文介绍了如何使用Go语言实现HTTP服务的热重启(ZeroDowntime)和优雅关闭。通过监听信号,父进程在接收到特定信号后创建子进程,将socket描述符传递给子进程,确保在更新或重启过程中能保持服务不间断。同时,文章提供了详细的代码示例,演示了如何处理SIGINT和SIGUSR2信号以实现GracefulShutdown和GracefulReload。

热重启

热重启(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 :=

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 的测试。

参考资料

有疑问加站长微信联系(非本文作者)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值