更多好文 vx golang技术实验室
优雅启动http服务一般的时候我们对服务进行升级的时候,会直接终止当前服务,ctrl + c
终止,或者发送其他信号量终止。
生产的话,
- 一般部署在k8s ,k8s 做对应的升级摘除,一批次一批次的进行摘除更新挂载
- nginx做反代,一台一台的摘机器,升级完毕在挂在
- 或者DNS做对应的域名解析
这些或多或少都存在一个问题,如果我当前的http请求打在了当前要摘除的服务上,如何保证请求完毕才进行服务升级重启。有的是给定一个时间10s,或者更长,那如果我的请求是超过这个时间呢,强制的操作,导致连接中断,对于一些业务要求很严谨的场景,我们不知道最后的结果,很显然,这很难接受。
我们来看下,如何解决这个问题
首先我们来看下信号量有哪几种
命令 | 信号 | 含义 |
---|---|---|
ctrl + c | SIGINT | 强制进程结束 |
ctrl + z | SIGTSTP | 任务中断,进程挂起 |
ctrl + |SIGQUIT | 进程结束和dump core | |
ctrl + d | EOF |
因此在我们执行ctrl + c关闭gin服务端时,会强制进程结束,导致正在访问的用户等出现问题
常见的 kill -9 pid 会发送 SIGKILL 信号给进程,也是一样的。
怎么样才是优雅
- 首先现有的请求是不能关闭的
- 新的进程启动并替代旧进程
- 新的进程接管新的连接
- 旧服务的连接依然存在,新服务处理新的请求
流程是怎样的
- 发送信号量
- 服务fork新的进程
- 新的进程处理新的请求
- 旧的进程要处理完毕之前到达的请求,然后退出进程
endless
https://github.com/fvbock/endless
endless 的作用:
- 不需要在负载均衡器或其他东西上挂入或挂出——只需要编译、SIGHUP、启动新的请求、完成旧的请求等等。
需要注意
Hammer Time(子进程多久通知父进程关闭)
为了在父进程重新启动后处理挂起的请求,endless将在收到来自分支子进程的关闭信号60秒后打击父进程。当收到通知信号时,仍然在运行的请求会被终止。这种行为可以由另一个导出的变量控制:
DefaultHammerTime time.Duration
默认值是60秒。当设置为-1时,不会自动调用hammerTime()。然后,您可以通过发送SIGUSR2来手动确定父节点。这只会在父程序已经处于关机模式时打击它。因此,除非进程在SIGUSR2之前接收到SIGTERM、SIGSTOP或SIGINT(手动或通过分支),否则将被忽略。
如果你有挂起的请求,服务器被敲打,你会看到这样的日志消息:
2015/04/04 13:04:10 [STOP - Hammer Time] Forcefully shutting down parent
使用案例
func main(){
endless.DefaultReadTimeOut = 5 * time.Second
endless.DefaultWriteTimeOut = 5* time.Second
endless.DefaultMaxHeaderBytes = 1 << 20
endPoint := fmt.Sprintf(":%d",8088)
r := Router{}
Routes(r)
server := endless.NewServer(endPoint,r)
server.BeforeBegin = func(add string){
log.Printf("Actual pid is %d",syscall.Getpid())
}
err := server.ListenAndServe()
if err != nil{
log.Printf("Server err : %v",err)
}
}
func Routes(r Router){
r.Route("GET", "/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Body)
fmt.Println(r.URL)
fmt.Println(r)
fmt.Printf("%#v",r)
w.Write([]byte("Hello,Chongchong!"))
})
r.Route("GET", `/hello/(?P<Message>\w+)`, func(w http.ResponseWriter, r *http.Request) {
message := URLParam(r, "Message")
w.Write([]byte("Hello " + message))
})
}
go run main.go
2021/08/18 11:07:39 Actual pid is 31020
服务启动,pid为 31020
我们在另一个终端输入 kill -1 31020
结果
2021/08/18 11:27:11 31020 Received SIGHUP. forking.
2021/08/18 11:27:11 Actual pid is 31094
2021/08/18 11:27:11 31020 Received SIGTERM.
2021/08/18 11:27:11 31020 Waiting for connections to finish...
2021/08/18 11:27:11 31020 Serve() returning...
2021/08/18 11:27:11 Server err : accept tcp [::]:8088: use of closed network connection
2021/08/18 11:27:11 31020 [::]:8088 Listener closed.
我们看到 31020 已经挂起,并且 fork 了新的子进程 pid 为 31094
ps -ef|grep 31094
501 31094 1 0 11:27AM ttys006 0:00.01 /var/folders/69/m2hx0d9532q3cnkt80_ndtxh0000gn/T/go-build3023669437/b001/exe/main
大致意思为主进程(pid为31020)接受到 SIGTERM 信号量,关闭主进程的监听并且等待正在执行的请求完成。
接下来见证奇迹
http://127.0.0.1:8088/hello
原来的进程活了
只需要给该进程发送SIGTERM信号,而不需要强制结束应用,是多么便捷又安全的事!
问题
endless 热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求
http.Server - Shutdown()
如果你的Golang >= 1.8,也可以考虑使用 http.Server 的 Shutdown 方法
r := &routerApi.Router{}
r.Route("GET", "/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Body)
fmt.Println(r.URL)
fmt.Println(r)
fmt.Printf("%#v",r)
time.Sleep(10 * time.Second)
fmt.Println(12345)
w.Write([]byte("Hello,Chongchong!"))
})
r.Route("GET", `/hello/(?P<Message>\w+)`, func(w http.ResponseWriter, r *http.Request) {
message := routerApi.URLParam(r, "Message")
w.Write([]byte("Hello " + message))
})
server := &http.Server{
Addr: fmt.Sprintf(":%d",8089),
Handler: r,
TLSConfig: nil,
ReadTimeout: 0,
ReadHeaderTimeout: 0,
WriteTimeout: 0,
IdleTimeout: 0,
MaxHeaderBytes: 0,
TLSNextProto: nil,
ConnState: nil,
ErrorLog: nil,
BaseContext: nil,
ConnContext: nil,
}
go func() {
quit := make(chan os.Signal)
signal.Notify(quit,os.Interrupt)
<- quit
log .Println("shutdown Server ...")
ctx ,cancel := context.WithTimeout(context.Background(),5 *time.Second)
defer cancel()
if err := server.Shutdown(ctx);nil != err{
log.Fatal("Server Shutdown:" ,err)
}
log .Println("Server exiting")
}()
if err := server.ListenAndServe();nil != err{
log.Println(err)
}
在实际的工作中,无损的热重启是右很多场景需要的,我们要根据自己的需要来设置
更多好文 vx golang技术实验室