Contents

Go服务如何实现优雅关停

什么是优雅关停

在我们需要关停如http服务等不断循环工作的业务进程时,一般采取以下两种方式:

  • 在Linux命令行ctrl + c,其实背后就是系统发送SIGINT信号
  • 通过kill或者systemctl stop等方式发送SIGTERM信号

那么假如进程在当前处理工作还没完成时收到这些信号,则当前工作就会处于完成一半然后放弃的情况,这就会对业务产生可大可小的影响,所以我们需要“优雅”地处理这些关停信号,在系统发出后拦截它们,并在进程完全退出前处理关停前的一些必要工作。下面就是一个用go channel来实现关停的简单例子。

Go channel的妙用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
    "time"
    "fmt"
    "os"
    "os/signal"
)

func main() {
    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt, os.Kill)
    go loop()
    var s := <-quit
    fmt.Printf("sig received: %v\n", s)
    // do things before exit
    gracefulShutdown()
}

func loop() {
    for {
        time.Sleep(time.Second)
        // main working goroutine
    }
}

在上面代码中,我们:

  1. 创建一个监听系统中断信号的quit channel,并对这些信号进行监听注册
  2. 把业务函数放在另一个协程中运行
  3. 通过等待quit channel的信号来阻塞main函数的退出,让业务函数得以一直运行
  4. 假如收到关停信号,则main函数继续往下执行gracefulShutdown(),最后整个进程退出

Gin优雅关停实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context){
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "hello")
    })
    srv := &http.Server{
        Addr: ":8080",
        Handler: router,
    }
    go func() {
        if err := srv.ListenAndServe(); err!= nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()
    
    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt, os.Kill)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown", err)
    }
    log.Println("Bye")
}