专业编程基础技术教程

网站首页 > 基础教程 正文

golang热重启

ccvgpt 2024-11-26 00:56:29 基础教程 1 ℃

什么是热重启

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

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

Tags:

最近发表
标签列表