优雅的重启服务(热更新/重载)

优雅的重启服务/重载也叫热更新用来实现服务不中断就能进行重启加载新的元素,概念分为应用程序和服务器两方面。

===> 今天本文以go和nginx分别来介绍优雅的重启服务(其实生产业务中有很多服务都需要优雅的重启服务来进行线上更新)。

Go优雅的重启服务

前言

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

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

项目地址:https://github.com/EDDYCJY/go-gin-example

ctrl + c

内核在某些情况下发送信号,比如在进程往一个已经关闭的管道写数据时会产生SIGPIPE信号

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

命令 信号 含义
ctrl + c SIGINT 强制进程结束
ctrl + z SIGTSTP 任务中断,进程挂起
ctrl + \ SIGQUIT 进程结束 和 dump core
ctrl + d   EOF
  SIGHUP 终止收到该信号的进程。若程序中没有捕捉该信号,当收到该信号时,进程就会退出(常用于 重启、重新加载进程)

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

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

信号

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

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

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

所有信号

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

怎样算优雅

目的

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

流程

1、替换可执行文件或修改配置文件

2、发送信号量 SIGHUP

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

4、启动新的子进程

5、新的子进程开始 Accet

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

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

实现优雅重启

endless

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 正正是依靠监听这些信号量,完成管控的一系列动作

安装

go get -u github.com/fvbock/endless

编写

打开 gin-blog 的 main.go文件,修改文件:

package main

import (
    "fmt"
    "log"
    "syscall"

    "github.com/fvbock/endless"

    "gin-blog/routers"
    "gin-blog/pkg/setting"
)

func main() {
    endless.DefaultReadTimeOut = setting.ReadTimeout
    endless.DefaultWriteTimeOut = setting.WriteTimeout
    endless.DefaultMaxHeaderBytes = 1 << 20
    endPoint := fmt.Sprintf(":%d", setting.HTTPPort)

    server := endless.NewServer(endPoint, routers.InitRouter())
    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)
    }
}

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

验证

编译

$ go build main.go 

执行

$ ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
...
Actual pid is 48601

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

[root@localhost go-gin-example]# ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /auth                     --> ...
[GIN-debug] GET    /api/v1/tags              --> ...
...

Actual pid is 48601

...

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

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

48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

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

唤醒

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

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection


$ [GIN] 2018/03/15 - 13:00:16 | 200 |     188.096µs |   192.168.111.1 | GET      /api/v1/tags...

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

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

问题

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

http.Server - Shutdown()

如果你的Golang >= 1.8,也可以考虑使用 http.Server 的 Shutdown 方法

package main

import (
	"fmt"
	"net/http"
    "context"
    "log"
    "os"
    "os/signal"
    "time"


	"gin-blog/routers"
	"gin-blog/pkg/setting"
)

func main() {
	router := routers.InitRouter()

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

    go func() {
        if err := s.ListenAndServe(); err != nil {
            log.Printf("Listen: %s\n", err)
        }
    }()
	
    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 := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }

    log.Println("Server exiting")
}

小结

在日常的服务中,优雅的重启(热更新)是非常重要的一环。而 Golang 在 HTTP 服务方面的热更新也有不少方案了,我们应该根据实际应用场景挑选最合适的(提供毫秒级的切换时间,让服务永不中断)

参考

本系列示例代码

Nginx优雅重启升级

nginx有一个主进程和几个工作进程,主进程的主要作用就是读取、评估配置文件和管理工作进程,工作进程对请求做实际处理。工作进程的数量是在配置文件中配置的,一般设置为cpu的核心数*线程数。

nginx可以通过信号控制,默认情况下,主进程的进程ID记录在/usr/local/nginx/logs/nginx.pid 文件中。信号可以通过nginx可执行文件发送给nginx (nginx -s 信号),也可以通过unix的kill命令发送给nginx,这种情况下是将信号直接发送给指定进程ID的进程,如:kill QUIT 1628。

1、启动nginx,直接运行nginx的可执行文件:

  1. [root@localhost ~]# /usr/local/nginx/sbin/nginx

2、停止nginx

快速关闭:nginx -s stop 或者kill -TERM 主进程号 或者kill -INT 主进程号

  1. [root@localhost sbin]# pwd
  2. /usr/local/nginx/sbin
  3. [root@localhost sbin]# ./nginx -s stop

或者

  1. [root@localhost sbin]# ps -ef |grep nginx|grep master
  2. root 9879 1 0 13:15 ? 00:00:00 nginx: master process ./nginx
  3. [root@localhost sbin]# kill -TERM 9879

或者

  1. [root@localhost sbin]# kill -INT 9879

优雅关闭(不接受新的连接请求,等待旧的连接请求处理完毕再关闭):nginx -s quit 或者 kill -QUIT 主进程号

  1. [root@songguoliang sbin]# pwd
  2. /usr/local/nginx/sbin
  3. [root@songguoliang sbin]# ./nginx -s quit

注意:执行该命令的用户应该是启动nginx的用户

  1. [root@localhost sbin]# ps -ef |grep nginx
  2. root 9889 1 0 13:17 ? 00:00:00 nginx: master process ./nginx
  3. nobody 9890 9889 0 13:17 ? 00:00:00 nginx: worker process
  4. root 9893 2008 0 13:18 pts/1 00:00:00 grep nginx
  5. [root@localhost sbin]# kill -QUIT 9889

3、重新加载配置文件:nginx -s reload 或者 kill -HUP 主进程号

nginx配置平滑更新

为了让主进程重新读取配置文件,应该向主进程发送一个HUP信号,主进程一旦接收到重新加载配置的的信号,它就检查配置文件语法的有效性,然后试图应用新的配置,即打开新的日志文件和新的socket 监听,如果失败,它将回滚配置更改并继续使用旧的配置,如果成功了,它开启新的工作进程,并给旧的工作进程发消息让它们优雅的关闭,旧的工作进程接收到关闭信号后,不再接收新的请求,如果已有请求正在处理,等当前请求处理完毕后关闭,如果没有请求正在处理,则直接关闭。

  1. [root@localhost sbin]# ./nginx -s reload

或者

  1. [root@localhost sbin]# ps -ef|grep nginx
  2. root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
  3. nobody 9949 9944 0 13:23 ? 00:00:00 nginx: worker process
  4. root 9960 9917 0 13:28 pts/1 00:00:00 grep nginx
  5. [root@songguoliang sbin]# kill -HUP 9944

4、测试配置文件,检查配置文件语法是否正确,然后试图打开文件涉及的配置:nginx -t

  1. [root@localhost sbin]# ./nginx -t
  2. nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
  3. nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

5、查看nginx版本信息: nginx -v

  1. [root@songguoliang sbin]# ./nginx -v
  2. nginx version: nginx/1.10.1

6、查看nginx版本信息,编译版本,和配置参数:nginx -V

  1. [root@songguoliang sbin]# ./nginx -V
  2. nginx version: nginx/1.10.1
  3. built by gcc 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC)
  4. configure arguments: --prefix=/usr/local/nginx

7、重启日志文件,备份日志文件时常用:nginx -s reopen 或者 kill -USR1 主进程号

  1. [root@localhost sbin]# ./nginx -s reopen

或者

  1. [root@localhost sbin]# kill -USR1 2030

由于nginx是通过inode指向日志文件的,inode和文件名无关,所以即使把日志文件重命名,nginx还是将日志文件写入原文件,只有用上面的命令重新开启日志文件才能将日志写入新的日志文件。

8、nginx的平滑升级

如果你需要升级或者添加、删除服务器模块时,可以通过nginx的平滑升级,在不停止服务的情况下升级nginx。

(1)用新的nginx可执行程序替换旧的可执行程序,即下载新的nginx,重新编译到旧版本的安装路径中(重新编译之前可以备份旧的可执行文件)

(2)给nginx主进程号发送USR2信号

  1. [root@localhost sbin]# kill -USR2 9944

执行命令前的进程:

  1. [root@localhost sbin]# ps -ef |grep nginx
  2. root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
  3. nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
  4. root 10010 9917 0 13:42 pts/1 00:00:00 grep nginx

执行命令后的进程:

  1. [root@localhost sbin]# ps -ef |grep nginx
  2. root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
  3. nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
  4. root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
  5. nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
  6. root 10015 9917 0 13:43 pts/1 00:00:00 grep nginx

给nginx发送USR2信号后,nginx会将logs/nginx.pid文件重命名为nginx.pid.oldbin,然后用新的可执行文件启动一个新的nginx主进程和对应的工作进程,并新建一个新的nginx.pid保存新的主进程号

  1. [root@songguoliang logs]# pwd
  2. /usr/local/nginx/logs
  3. [root@songguoliang logs]# ll
  4. 总用量 16
  5. -rw-r--r--. 1 nobody root 596 10月 8 13:02 access.log
  6. -rw-r--r--. 1 nobody root 881 10月 8 13:43 error.log
  7. -rw-r--r--. 1 root root 6 10月 8 13:43 nginx.pid
  8. -rw-r--r--. 1 root root 5 10月 8 13:22 nginx.pid.oldbin
  9. [root@songguoliang logs]# cat nginx.pid
  10. 10012
  11. [root@songguoliang logs]# cat nginx.pid.oldbin
  12. 9944

这时,nginx的新的实例和旧的实例同时工作,共同处理请求连接。接下来要关闭旧的实例进程。

(3)给旧的主进程发送WINCH信号,kill -WINCH 旧的主进程号

  1. [root@localhost sbin]# ps -ef |grep nginx
  2. root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
  3. nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
  4. root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
  5. nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
  6. root 10092 9917 0 14:05 pts/1 00:00:00 grep nginx
  7. [root@localhost sbin]# kill -WINCH 9944
  8. [root@localhost sbin]#
  9. [root@localhost sbin]#
  10. [root@localhost sbin]#
  11. [root@localhost sbin]# ps -ef |grep nginx
  12. root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
  13. root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
  14. nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
  15. root 10094 9917 0 14:06 pts/1 00:00:00 grep nginx

旧的主进程号收到WINCH信号后,将旧进程号管理的旧的工作进程优雅的关闭。即一段时间后旧的工作进程全部关闭,只有新的工作进程在处理请求连接。这时,依然可以恢复到旧的进程服务,因为旧的进程的监听socket还未停止。

(4)给旧的主进程发送QUIT信号,使其关闭。

  1. [root@localhost sbin]# kill -QUIT 9944
  2. [root@localhost sbin]# ps -ef |grep nginx
  3. root 10012 1 0 13:43 ? 00:00:00 nginx: master process ./nginx
  4. nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
  5. root 10118 9917 0 14:16 pts/1 00:00:00 grep nginx

给旧的主进程发送QUIT信号后,旧的主进程退出,并移除logs/nginx.pid.oldbin文件,nginx的升级完成。

中途停止升级,回滚到旧的nginx

在步骤(3)时,如果想回到旧的nginx不再升级

(1)给旧的主进程号发送HUP命令,此时nginx不重新读取配置文件的情况下重新启动旧主进程的工作进程。

  1. [root@localhost sbin]# kill -HUP 9944

(2)优雅的关闭新的主进程

  1. [root@localhost sbin]# kill -QUIT 10012

注:如果由于某种原因,无法关闭新主进程的工作进程,直接给进程号发送KILL信号

nginx的进程分为主进程和工作进程关于nginx的信号,主进程支持以下信号:

TERM,INT 快速关闭

QUIT 优雅的关闭

KILL 关闭一个顽固进程

HUP 改变配置,使用新配置开启新的工作进程,优雅的关闭旧的进程

USR1 重新开启日志文件

USR2 平滑升级nginx

WINCH 优雅的关闭工作进程

单个工作进程也可以通过信号控制,但它不是必须的。单个工作进程支持的信号有:

TERM,INT 快速关闭

QUIT 优雅的关闭

USR1 重新开启日志文件

WINCH 异常终止调试(需要启动debug_points)

拓展阅读(其他服务优雅重启参考)



 

猜你喜欢

转载自blog.csdn.net/english0523/article/details/108010829