go 平滑重启endless方案
目录
信号处理
Go 信号通知通过在 Channel 上发送 os.Signal 值来工作。如我们如果使用 Ctrl+C,那么会触发 SIGINT 信号,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
// 监听信号
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
// 接收到信号返回
sig := <-sigs
fmt.Println()
fmt.Println(sig)
done <- true
}()
fmt.Println("awaiting signal")
// 等待信号的接收
<-done
fmt.Println("exiting")
}
我们就可以监听 SIGINT 和 SIGTERM 信号。当 Go 接收到操作系统发送过来的信号,那么会将信号值放入到 sigs 管道中进行处理。
fork子进程
file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
"-graceful"}
// 产生 Cmd 实例
cmd := exec.Command(path, args...)
// 标准输出
cmd.Stdout = os.Stdout
// 标准错误输出
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}
// 启动命令
err := cmd.Start()
if err != nil {
log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}
通过调用 exec 包的 Command 命令传入 path(将要执行的命令路径)、args (命令的参数)即可返回 Cmd 实例,通过 ExtraFiles 字段指定额外被新进程继承的已打开文件,最后调用 Start 方法创建子进程。
这里的 netListener.File会通过系统调用 dup 复制一份 file descriptor 文件描述符。
func Dup(oldfd int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_DUP, uintptr(oldfd), 0, 0)
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
endless 不停机重启示例
func initRouter2() {
engine := gin.Default()
engine.GET("/bbb/aa", func(context *gin.Context) {
time.Sleep(time.Second * 40)
context.JSON(http.StatusOK, "11")
})
err := endless.ListenAndServe("localhost:8080", engine)
if err != nil {
log.Println(err)
}
log.Println("Server on 8080 stopped")
}
go build -o hello
./hello
修改文件,context.JSON(http.StatusOK, "22")
go build -o hello
curl http://localhost:8080/bbb/aa
kill -1 hello_pid
curl http://localhost:8080/bbb/aa
...
请求返回11
请求返回22
我们要做的不停机重启,监听 SIGHUP 信号;
- 收到信号时 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程;
- 子进程监听父进程的 socket,这个时候父进程和子进程都可以接收请求;
- 子进程启动成功之后发送 SIGTERM 信号给父进程,父进程停止接收新的连接,等待旧连接处理完成(或超时);
- 父进程退出,升级完成;
endless实现
func (srv *endlessServer) ListenAndServe() (err error) {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
// 异步处理信号量
go srv.handleSignals()
// 获取端口监听
l, err := srv.getListener(addr)
if err != nil {
log.Println(err)
return
}
// 将监听转为 endlessListener
srv.EndlessListener = newEndlessListener(l, srv)
// 如果是子进程,那么发送 SIGTERM 信号给父进程
if srv.isChild {
syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
}
srv.BeforeBegin(srv.Addr)
// 响应Listener监听,执行对应请求逻辑
return srv.Serve()
}
信号处理主要是信号的一个监听,然后根据不同的信号循环处理。
func (srv *endlessServer) handleSignals() {
var sig os.Signal
// 注册信号监听
signal.Notify(
srv.sigChan,
hookableSignals...,
)
// 获取pid
pid := syscall.Getpid()
for {
sig = <-srv.sigChan
// 在处理信号之前触发hook
srv.signalHooks(PRE_SIGNAL, sig)
switch sig {
// 接收到平滑重启信号
case syscall.SIGHUP:
log.Println(pid, "Received SIGHUP. forking.")
err := srv.fork()
if err != nil {
log.Println("Fork err:", err)
}
// 停机信号
case syscall.SIGINT:
log.Println(pid, "Received SIGINT.")
srv.shutdown()
// 停机信号
case syscall.SIGTERM:
log.Println(pid, "Received SIGTERM.")
srv.shutdown()
...
// 在处理信号之后触发hook
srv.signalHooks(POST_SIGNAL, sig)
}
}
当我们用kill -1 $pid 的时候这里 srv.sigChan 就会接收到相应的信号,并进入到 case syscall.SIGHUP 这块逻辑代码中。
参考链接:https://www.luozhiyun.com/archives/584