七叶笔记 » golang编程 » golang热重启

golang热重启

什么是热重启

所谓热重启, 就是当关闭一个正在运行的进程时,该进程并不会立即停止,而是会等待所有当前逻辑继续执行完毕,才会中断。这就要求我们的服务需要支持一条重启命令,通过该命令可以重启服务,并同时保证重启过程中正在执行的逻辑不会中断,且重启后可以继续正常服务。

热重启的原理

热重启的原理会涉及到一些linux下系统调用以及进程之间socket句柄传递等细节,处理过程可以分为以下几个步骤:

1、监听重启信号 (SIGHUP);

2、收到重启信号时fork子进程,同时需要将服务监听的socket文件描述符传递给子进程;

3、子进程接收并监听父进程传递的socket (这时候父进程和子进程都可以接收请求);

4、等待子进程启动成功之后,停止父进程对新连接的接收 (父进程会等待旧连接逻辑处理完成);

5、父进程退出,重启完成

实现代码如下:

 package main

import (
"flag"
"fmt"
"golang.org/x/net/context"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
)

var (
server   *http.Server
listener net.Listener
// 子进程的标志
child = flag.Bool("child", false, "")
)

// init
func init() {
updatePidFile()
}

// 更新pid文件
func updatePidFile() {
oldPid := fmt.Sprint(os.Getpid())
// 创建临时目录
tmpDir := os.TempDir()
// 判断进程是否启动
if err := procExist(tmpDir); err != nil {
fmt.Printf("pid file exists, update\n")
} else {
fmt.Printf("pid file NOT exists, create\n")
}

// 创建pid文件
pidFile, _ := os.Create(tmpDir + "/gracefulRestart.pid")
defer pidFile.Close()

// 写入文件内容
pidFile.WriteString(oldPid)
}

// 检查进程是否存在
func procExist(tmpDir string) (err error) {
// 打开文件
pidFile, err := os.Open(tmpDir + "/gracefulRestart.pid")
defer pidFile.Close()
if err != nil {
return
}

// 读取文件内容
filePid, err := ioutil.ReadAll(pidFile)
if err != nil {
return
}

pid, _ := strconv.Atoi(fmt.Sprintf("%s", filePid))
// 查找pid进程
if _, err = os.FindProcess(pid); err != nil {
fmt.Printf("Failed to find process: %v\n", err)
return
}

return
}

func main() {
flag.Parse()

// 启动监听
http.HandleFunc("/hello", HelloHandler)
server = &http.Server{Addr: ":8081"}

var err error
if *child { // 子进程
fmt.Println("In Child, Listening...")

f := os.NewFile(3, "")
listener, err = net.FileListener(f)
} else {
fmt.Println("In Father, Listening...")

listener, err = net.Listen("tcp", server.Addr)
}
if err != nil {
fmt.Printf("Listening failed: %v\n", err)
return
}

// go协程启动server
go func() {
err = server.Serve(listener)
if err != nil {
fmt.Printf("server.Serve failed: %v\n", err)
}
}()

// 监听系统信号
signalHandler()

fmt.Printf("singalHandler end\n")
}

func HelloHandler(w http.ResponseWriter, r *http.Request) {
//time.Sleep(30 * time.Second)
for i := 0; i < 20; i++ {
log.Printf("working %v\n", i)
time.Sleep(1 * time.Second)
}
w.Write([]byte("hello world..."))
}

// 信号处理
func signalHandler() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

for {
sig := <-ch
fmt.Printf("signal: %v\n", sig)

ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
switch sig {
case syscall.SIGINT, syscall.SIGTERM: // 终止信号
log.Printf("stop")
signal.Stop(ch)
server.Shutdown(ctx)
fmt.Printf("graceful shutdown\n")
return
case syscall.SIGHUP: // 重启信号
// reload
log.Printf("restart")
err := restart()
if err != nil {
fmt.Printf("graceful restart failed: %v\n", err)
}

// 更新当前pid文件
updatePidFile()

// golang >1.8,可以通过调用Golang中的Server.Shutdown()方法直接实现graceful stop
// 老版本golang自己实现一个shutdown功能步骤:
// 1、关闭listenr,停止接收新请求;
// 2、通过sync.WaitGroup.wait()阻塞服务退出,从而实现等待其他逻辑的全部退出
server.Shutdown(ctx)
fmt.Printf("graceful reload\n")
return
}
}
}

// 重启
func restart() error {
// 提取文件描述符
tl, ok := listener.(*net.TCPListener)
if !ok {
return fmt.Errorf("listener is not tcp listener")
}
f, err := tl.File()
if err != nil {
return err
}

// 创建子进程,同时传递了child参数到子进程中,从而可以执行在进程监听时走子进程创建socket的流程。
args := []string{"-child"}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// cmd的ExtraFiles参数会将额外的文件描述符传递给继承的新进程(不包括标准输入、标准输出和标准错误)
// 父进程传递listener的fd给子进程,而子进程里0、1、2是预留给标准输入、输出和错误的,所以父进程给的第一个fd在子进程里顺序排就是从3开始,
// 即上文中的os.NewFile(3,"")中3的由来
cmd.ExtraFiles = []*os.File{f}
return cmd.Start()
}  

测试:

 $ go run main.go
pid file NOT exists, create
In Father, Listening... // 父进程启动
2022/01/21 17:42:21 working 0
2022/01/21 17:42:22 working 1
2022/01/21 17:42:23 working 2
2022/01/21 17:42:24 working 3
2022/01/21 17:42:25 working 4
2022/01/21 17:42:26 working 5
2022/01/21 17:42:27 working 6
2022/01/21 17:42:28 working 7
2022/01/21 17:42:29 working 8
2022/01/21 17:42:30 working 9
2022/01/21 17:42:31 working 10
2022/01/21 17:42:32 working 11
2022/01/21 17:42:33 working 12
2022/01/21 17:42:34 working 13
2022/01/21 17:42:35 working 14
2022/01/21 17:42:36 working 15 // 执行到这里时进行重启

# 通过端口号8081查看对应的pid
$ lsof -i :8081
COMMAND   PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
main    79963 admin    3u  IPv6 0xeacf6c71139ace71      0t0  TCP *:sunproxyadmin (LISTEN)

# 重启(发送 SIGHUP 信号)
$ kill -HUP 79963
signal: hangup
2022/01/21 17:42:36 restart
pid file NOT exists, create
server.Serve failed: http: Server closed
pid file NOT exists, create
In Child, Listening... // 子进程启动
2022/01/21 17:42:37 working 16 // 重启成功后,继续执行,不中断
2022/01/21 17:42:38 working 17
2022/01/21 17:42:39 working 18
2022/01/21 17:42:40 working 19
graceful reload
singalHandler end
  

相关文章