使用Golang和systemd实现零停机重新部署/重启的解决方案

发表时间: 2022-12-18 14:54

Go程序实现0停机重新部署、重启服务

在k8s等容器化部署方案中,可以依赖k8s自身的调度实现启动新版本,再优雅退出旧版本实现新旧程序交替。但是传统服务器的go程序部署中实现需要自行处理FD继承机制。

本文依赖systemd的socket机制,由systemd负责监听,并把FD通过环境变量传递给应用程序,由应用程序进行FD处理。


新建 socket 监听

systemctl edit --full -f test.socket
[Socket]# 监听8000端口ListenStream=8000[Install]WantedBy=default.target

新建service

# service名称要和socket名称保持一致systemctl edit --full -f test.service
[Unit]Description=test systemd fds[Service]Type=simple# 程序意外退出后自动重新启动Restart=on-failure# 重要# 执行文件路径,要有x权限ExecStart=/usr/local/bin/test[Install]WantedBy=default.target

程序部分

// 接收并处理systemd传递的LISTEN_*环境变量func getLn() ([]net.Listener, error) {	listenFdStart := 3	defer os.Unsetenv("LISTEN_PID")	defer os.Unsetenv("LISTEN_FDS")	defer os.Unsetenv("LISTEN_FDNAMES")  // PID与当前进程PID不一致,不处理  // LISTEN_PID=int	pid, err := strconv.Atoi(os.Getenv("LISTEN_PID"))	if err != nil || pid != os.Getpid() {		return nil, fmt.Errorf("ERR: PIDERROR %s", err)	}  // FD非法不处理  // LISTEN_FDS=int	nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))	if err != nil || nfds == 0 {		return nil, fmt.Errorf("ERR: fds err, %s", err)	}  // 接收socket名称  // LISTEN_FDNAMES=test.socket	names := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":")	files := make([]*os.File, 0, nfds)	for fd := listenFdStart; fd < listenFdStart+nfds; fd++ {		syscall.CloseOnExec(fd)		name := "LISTEN_FD_" + strconv.Itoa(fd)		offset := fd - listenFdStart		if offset < len(names) && len(names[offset]) > 0 {			name = names[offset]		}		files = append(files, os.NewFile(uintptr(fd), name))	}  // 将FD转换成net.Listener对象	listeners := make([]net.Listener, len(files))	for i, file := range files {		if pc, err := net.FileListener(file); err == nil {			listeners[i] = pc			file.Close()		}	}	return listeners, nil}

处理http请求

// 新建handlermux := http.NewServeMux()mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {   // 5秒后输出 hello   time.Sleep(5 * time.Second)   writer.Write([]byte("hello"))})// 新建server对象server := http.Server{   Handler: mux,}// 使用getLn获取listener并使用listener提供服务listeners, _ := getLn()go server.Serve(listeners[0])// 监听退出信号,实现优雅退出signalChan := make(chan os.Signal)signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)<-signalChanerr = server.Shutdown(context.Background())

编译后执行 systemctl start test.socket test.service

curl localhost:8000 发现等待5秒后正常输出hello。

在中间任意时间执行systemctl restart test.service 都能看到在请求正常处理结束后,程序自动退出并启动新进程。目的实现。

TIPS:

server.Shutdown() 会阻塞请求,一旦执行shutdown
server.Serve/ListenAndServe等方法将会立刻退出。因此,shutdown要放在主goroutine执行,或新建一个全局context、chan进行主goroutine阻塞否则http服务会立刻结束运行。