优雅的重启服务

    每次更新完代码,更新完配置文件后
    就直接这么 真的没问题吗,ctrl+c到底做了些什么事情呢?

    在这一节中我们简单讲述 ctrl+c 背后的信号以及如何在Gin优雅的重启服务,也就是对 HTTP 服务进行热更新

    项目地址:

    在终端执行特定的组合键可以使系统发送特定的信号给此进程,完成一系列的动作

    因此在我们执行ctrl + c关闭gin服务端时,会强制进程结束,导致正在访问的用户等出现问题

    常见的 kill -9 pid 会发送 SIGKILL 信号给进程,也是类似的结果

    本段中反复出现信号是什么呢?

    信号是 Unix 、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式

    它是一种异步的通知机制,用来提醒进程一个事件(硬件异常、程序执行异常、外部发出信号)已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程。此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数

    所有信号

    • 不关闭现有连接(正在运行中的程序)
    • 新的进程启动并替代旧进程
    • 新的进程接管新的连接
    • 连接要随时响应用户的请求,当用户仍在请求旧进程时要保持连接,新用户应请求新进程,不可以出现拒绝请求的情况

    流程

    2、发送信号量 SIGHUP

    3、拒绝新连接请求旧进程,但要保证已有连接正常

    4、启动新的子进程

    5、新的子进程开始 Accet

    6、系统将新的请求转交新的子进程

    7、旧进程处理完所有旧连接后正常结束

    Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+)

    我们借助 fvbock/endless 来实现 Golang HTTP/HTTPS 服务重新启动的零停机

    endless server 监听以下几种信号量:

    • syscall.SIGHUP:触发 fork 子进程和重新启动
    • syscall.SIGUSR1/syscall.SIGTSTP:被监听,但不会触发任何动作
    • syscall.SIGUSR2:触发 hammerTime
    • syscall.SIGINT/syscall.SIGTERM:触发服务器关闭(会完成正在运行的请求)

    endless 正正是依靠监听这些信号量,完成管控的一系列动作

    安装

    1. go get -u github.com/fvbock/endless

    编写

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "syscall"
    6. "github.com/fvbock/endless"
    7. "gin-blog/routers"
    8. "gin-blog/pkg/setting"
    9. )
    10. func main() {
    11. endless.DefaultReadTimeOut = setting.ReadTimeout
    12. endless.DefaultWriteTimeOut = setting.WriteTimeout
    13. endless.DefaultMaxHeaderBytes = 1 << 20
    14. server := endless.NewServer(endPoint, routers.InitRouter())
    15. server.BeforeBegin = func(add string) {
    16. log.Printf("Actual pid is %d", syscall.Getpid())
    17. }
    18. err := server.ListenAndServe()
    19. if err != nil {
    20. }
    21. }

    endless.NewServer 返回一个初始化的 endlessServer 对象,在 BeforeBegin 时输出当前进程的 pid,调用 ListenAndServe 将实际“启动”服务

    验证

    编译
    执行
    1. $ ./main
    2. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
    3. ...
    4. Actual pid is 48601

    启动成功后,输出了pid为 48601;在另外一个终端执行 kill -1 48601 ,检验先前服务的终端效果

    1. [root@localhost go-gin-example]# ./main
    2. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
    3. - using env: export GIN_MODE=release
    4. - using code: gin.SetMode(gin.ReleaseMode)
    5. [GIN-debug] GET /auth --> ...
    6. [GIN-debug] GET /api/v1/tags --> ...
    7. ...
    8. Actual pid is 48601
    9. ...
    10. Actual pid is 48755
    11. 48601 Received SIGTERM.
    12. 48601 [::]:8000 Listener closed.
    13. 48601 Waiting for connections to finish...
    14. 48601 Serve() returning...
    15. Server err: accept tcp [::]:8000: use of closed network connection

    可以看到该命令已经挂起,并且 fork 了新的子进程 pid48755

    大致意思为主进程(pid为48601)接受到 SIGTERM 信号量,关闭主进程的监听并且等待正在执行的请求完成;这与我们先前的描述一致

    唤醒

    这时候在 postman 上再次访问我们的接口,你可以惊喜的发现,他“复活”了!

    1. Actual pid is 48755
    2. 48601 Received SIGTERM.
    3. 48601 [::]:8000 Listener closed.
    4. 48601 Waiting for connections to finish...
    5. 48601 Serve() returning...
    6. Server err: accept tcp [::]:8000: use of closed network connection
    7. $ [GIN] 2018/03/15 - 13:00:16 | 200 | 188.096µs | 192.168.111.1 | GET /api/v1/tags...

    这就完成了一次正向的流转了

    你想想,每次更新发布、或者修改配置文件等,只需要给该进程发送SIGTERM信号,而不需要强制结束应用,是多么便捷又安全的事!

    问题

    endless 热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求

    http.Server - Shutdown()

    如果你的,也可以考虑使用 http.Server 的 方法

    1. package main
    2. import (
    3. "net/http"
    4. "context"
    5. "log"
    6. "os"
    7. "os/signal"
    8. "time"
    9. "gin-blog/routers"
    10. "gin-blog/pkg/setting"
    11. )
    12. func main() {
    13. router := routers.InitRouter()
    14. s := &http.Server{
    15. Addr: fmt.Sprintf(":%d", setting.HTTPPort),
    16. Handler: router,
    17. ReadTimeout: setting.ReadTimeout,
    18. WriteTimeout: setting.WriteTimeout,
    19. MaxHeaderBytes: 1 << 20,
    20. }
    21. go func() {
    22. if err := s.ListenAndServe(); err != nil {
    23. log.Printf("Listen: %s\n", err)
    24. }
    25. }()
    26. quit := make(chan os.Signal)
    27. signal.Notify(quit, os.Interrupt)
    28. <- quit
    29. log.Println("Shutdown Server ...")
    30. ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    31. defer cancel()
    32. if err := s.Shutdown(ctx); err != nil {
    33. log.Fatal("Server Shutdown:", err)
    34. }
    35. log.Println("Server exiting")
    36. }

    在日常的服务中,优雅的重启(热更新)是非常重要的一环。而 在 HTTP 服务方面的热更新也有不少方案了,我们应该根据实际应用场景挑选最合适的

    拓展阅读