什么是优雅退出?什么才叫优雅?
这两个问题是本文的核心,因此需要首先分析这两个问题。我们的进程存在各种退出场景,比如收到terminate信号进而退出,也可能被ctrl-c终止了程序,也有可能是执行kill -9 pid指令直接杀死进程。对于这些异常且突发的进程退出,会导致正在执行任务的进程终止自己手头上的工作,比如正在计算到一半的任务就终止了;也可能是计算完的结果来不及存入数据库就终止了;也可能是我们的log信息也没来得及调用write就进程退出了。这种的进程退出只能叫暴力退出,因为此时的退出没有做任何保护措施,因此可能会导致一些数据的丢失。
那什么样的退出才叫优雅?那对比暴力退出其实就可以得出,优雅退出的本质是留给进程清理剩余请求的时间,等到自己的子协程/线程完成本轮任务时,再发起进程退出,此时的退出不会丢失任何数据。对比生活场景就是,电脑突然关机就是非常不优雅的,因为此时我们可能正在编写文档,此时文档还没来得及保存系统就关闭了,我就丢失了我的已输入但未保存的文档数据。如果是系统提前告知我系统将在60秒后自动关闭,那我就是有时间保存好我的所有数据,再等待关机时间到达,这就不会造成数据丢失。
日常的服务管理中,进程热重启是非常常见的操作,比如我们需要重启我们的web server进程,我们不要简单粗暴地使用kill -9 pid来杀死进程,然后再启动进程。正确的做法是先在进程内编写好优雅重启的代码,如果线上需要重启该进程时,请使用SIGTERM等可捕捉的信号来终止进程,进程内捕捉到这些信号后就开始停止接收新的请求,剩余请求都完成后,再退出进程。若发现一定时间内进程仍未优雅退出,此时的进程已陷入卡死状态,此时可以使用SIGKILL来强制终止进程。
因此,简单梳理优雅退出的步骤如下:
- 捕捉进程终止信号,如syscall.SIGINT, syscall.SIGTERM, os.Kill
- 捕捉到信号后,开启进程退出倒计时,不管如何,必须在n秒后退出进程,这是底线
- 停止接收新的请求
- 等待进程内剩余请求的完成
- 终止时间已到,进程正式退出
特别注意:kill -9 pid 触发的SIGKILL信号无法被处理,因为进程已经被系统强制关闭了。因此尽可能不要用这个指令终止程序,更好的方式是使用kill pid,触发的是SIGTERM。
在go中,我们可以使用signal.Notify 和 context.WithTimeout 来完成我们上面提到的的优雅退出。
我们以http server为例,介绍go是如何做到server进程的优雅退出。
首先我们启动我们的web服务器
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
HelloWeb(c)
})
s := &http.Server{
Addr: ":8091",
Handler: r,
}
go func() {
if err := s.ListenAndServe(); err != nil {
log.Printf("Listen err: %s\n", err)
}
}()
然后我们使用signal.Notify 去捕捉syscall.SIGINT, syscall.SIGTERM, os.Kill。当channel signalChan有数据时,就是捕捉到信号了,此时调用context.WithTimeout 开启4秒后的强制退出。继续调用srv.Shutdown(ctx) 关闭 http server,而srv.Shutdown(ctx)的本质操作是,关闭监听端口,停止接收新的请求,通过context通知自己的子协程退出,即当自己的子协程处理完本轮请求后,因为子协程内case <- ctx.Done() 触发,因此协程退出。最后close(exitCh) 让主进程顺利退出。
func gracefulExit(srv *http.Server) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, os.Kill)
sig := <-signalChan
log.Printf("catch signal, %+v", sig)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) // 4秒后退出
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Printf("server exiting")
close(exitCh)
}
完整优雅退出代码如下:
package main
// main.go
import (
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/signal"
"syscall"
"context"
"log"
"time"
)
var exitCh = make(chan int)
func HelloWeb(c *gin.Context) {
c.String(http.StatusOK, "Hello, Go\n")
}
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
HelloWeb(c)
})
s := &http.Server{
Addr: ":8091",
Handler: r,
}
go func() {
if err := s.ListenAndServe(); err != nil {
log.Printf("Listen err: %s\n", err)
}
}()
go gracefulExit(s)
<-exitCh
log.Printf("Server exit")
}
func gracefulExit(srv *http.Server) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, os.Kill)
sig := <-signalChan
log.Printf("catch signal, %+v", sig)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) // 4秒后退出
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Printf("server exiting")
close(exitCh)
}
ctrl-c触发了进程的优雅退出。
^C2021/06/27 01:04:59 catch signal, interrupt
2021/06/27 01:04:59 server exiting
2021/06/27 01:04:59 Server exit
junshideMacBook-Pro:gogo junshili$
附上POSIX中定义的信号,供查阅