使用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服务会立刻结束运行。