让我们给http服务写一个版本更新接口,让它自动更新版本并重启服务吧。
初步例子
注:为了精简,文中代码都去除了err处理
main.go
var Version = "1.0"
/* 打印版本 */
func version(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("version %v\n", Version)
w.Write([]byte(msg))
}
/* 版本升级 */
func upgrade(w http.ResponseWriter, r *http.Request) {
// 1. 把新版本文件放置到服务主目录(简化)
os.Remove("test_restart")
os.Rename("new_test_restart", "test_restart")
// 2. go的可执行文件加权限(省略)
// 3. 重启服务
cmd := exec.Command("/bin/bash", "-c", "./restart.sh")
cmd.Output()
w.Write([]byte("restart ok\n"))
}
func main() {
// 记录pid
f,_ := os.Create("s.pid")
pid := os.Getpid()
f.WriteString(fmt.Sprintf("%v", pid))
fmt.Printf("System running:%v\n", pid)
// 监听连接
mux := http.NewServeMux()
mux.HandleFunc("/version", version)
mux.HandleFunc("/upgrade", upgrade)
http.ListenAndServe("127.0.0.1:9527", mux)
}
restart.sh(重启脚本)
kill -9 $(cat s.pid)
nohup ./test_restart > nohup.log 2>&1 &
测试
1. 编译后开始运行
2. 请求一下版本信息接口
3. 将代码中Version改为“1.1”, 生成一个新文件“new_test_restart”
4. 请求版本更新接口
发现问题
重启脚本是stop后start的,stop时直接杀死了进程,程序直接中断了当前所有的连接,此时接口函数还未return,导致调用方接收不到响应。
使用Endless
看来得不中断已有连接的情况下进行重启才行,不能简单的stop后start,得平滑重启。大致就是让父进程启动一个子进程去监听新的连接,自己不再监听新的连接,而是在处理完已有连接后终止,之后子进程独挑大梁。
随后发现github上的endless挺满足需求,它是一个不停机重启的服务器实现,实现流程为:
- 监听 SIGHUP 信号
- 收到信号后 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程
- 子进程启动成功后开始监听新的连接,并发送 SIGTERM 信号给父进程
- 父进程收到 SIGTERM 信号后停止接收新的连接,等待旧连接处理完成后终止
- 父进程终止,重启完成
关于 SIGHUP 信号,我们可以用“kill -1”命令发送给endless。
使用endless改动很小,在main函数中只需要把 http.ListenAndServe 修改为 endless.ListenAndServe即可:
main.go
func main() {
// 记录pid(省略)
// 监听连接
mux := http.NewServeMux()
mux.HandleFunc("/version", version)
mux.HandleFunc("/upgrade", upgrade)
// http.ListenAndServe("127.0.0.1:9527", mux)
endless.ListenAndServe("127.0.0.1:9527", mux)
}
restart.sh
kill -1 $(cat s.pid)
再测试
1. 编译后开始运行(带时间前缀的是endless打印的日志)
2. 请求一下版本信息接口
3. 将代码中Version改为“1.1”, 生成一个新文件“new_test_restart”
4. 请求版本更新接口
请求没有被中断,成功接收了响应。
endless的日志比较清晰,105300的父进程接收 SIGHUP 后,fork了子进程105335,接收到子进程传递的 SIGHUP 后,等待已有连接处理完成后终止,完全符合上述介绍的流程。
5. 验证版本
至此,不停机版本更新成功 ٩(◕‿◕。)۶