优雅的重启服务/重载也叫热更新用来实现服务不中断就能进行重启加载新的元素,概念分为应用程序和服务器两方面。
===> 今天本文以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的可执行文件:
- [root@localhost ~]# /usr/local/nginx/sbin/nginx
2、停止nginx
快速关闭:nginx -s stop 或者kill -TERM 主进程号 或者kill -INT 主进程号
- [root@localhost sbin]# pwd
- /usr/local/nginx/sbin
- [root@localhost sbin]# ./nginx -s stop
或者
- [root@localhost sbin]# ps -ef |grep nginx|grep master
- root 9879 1 0 13:15 ? 00:00:00 nginx: master process ./nginx
- [root@localhost sbin]# kill -TERM 9879
或者
- [root@localhost sbin]# kill -INT 9879
优雅关闭(不接受新的连接请求,等待旧的连接请求处理完毕再关闭):nginx -s quit 或者 kill -QUIT 主进程号
- [root@songguoliang sbin]# pwd
- /usr/local/nginx/sbin
- [root@songguoliang sbin]# ./nginx -s quit
注意:执行该命令的用户应该是启动nginx的用户
- [root@localhost sbin]# ps -ef |grep nginx
- root 9889 1 0 13:17 ? 00:00:00 nginx: master process ./nginx
- nobody 9890 9889 0 13:17 ? 00:00:00 nginx: worker process
- root 9893 2008 0 13:18 pts/1 00:00:00 grep nginx
- [root@localhost sbin]# kill -QUIT 9889
3、重新加载配置文件:nginx -s reload 或者 kill -HUP 主进程号
nginx配置平滑更新
为了让主进程重新读取配置文件,应该向主进程发送一个HUP信号,主进程一旦接收到重新加载配置的的信号,它就检查配置文件语法的有效性,然后试图应用新的配置,即打开新的日志文件和新的socket 监听,如果失败,它将回滚配置更改并继续使用旧的配置,如果成功了,它开启新的工作进程,并给旧的工作进程发消息让它们优雅的关闭,旧的工作进程接收到关闭信号后,不再接收新的请求,如果已有请求正在处理,等当前请求处理完毕后关闭,如果没有请求正在处理,则直接关闭。
- [root@localhost sbin]# ./nginx -s reload
或者
- [root@localhost sbin]# ps -ef|grep nginx
- root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
- nobody 9949 9944 0 13:23 ? 00:00:00 nginx: worker process
- root 9960 9917 0 13:28 pts/1 00:00:00 grep nginx
- [root@songguoliang sbin]# kill -HUP 9944
4、测试配置文件,检查配置文件语法是否正确,然后试图打开文件涉及的配置:nginx -t
- [root@localhost sbin]# ./nginx -t
- nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
- nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
5、查看nginx版本信息: nginx -v
- [root@songguoliang sbin]# ./nginx -v
- nginx version: nginx/1.10.1
6、查看nginx版本信息,编译版本,和配置参数:nginx -V
- [root@songguoliang sbin]# ./nginx -V
- nginx version: nginx/1.10.1
- built by gcc 4.4.7 20120313 (Red Hat 4.4.7-17) (GCC)
- configure arguments: --prefix=/usr/local/nginx
7、重启日志文件,备份日志文件时常用:nginx -s reopen 或者 kill -USR1 主进程号
- [root@localhost sbin]# ./nginx -s reopen
或者
- [root@localhost sbin]# kill -USR1 2030
由于nginx是通过inode指向日志文件的,inode和文件名无关,所以即使把日志文件重命名,nginx还是将日志文件写入原文件,只有用上面的命令重新开启日志文件才能将日志写入新的日志文件。
8、nginx的平滑升级
如果你需要升级或者添加、删除服务器模块时,可以通过nginx的平滑升级,在不停止服务的情况下升级nginx。
(1)用新的nginx可执行程序替换旧的可执行程序,即下载新的nginx,重新编译到旧版本的安装路径中(重新编译之前可以备份旧的可执行文件)
(2)给nginx主进程号发送USR2信号
- [root@localhost sbin]# kill -USR2 9944
执行命令前的进程:
- [root@localhost sbin]# ps -ef |grep nginx
- root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
- nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
- root 10010 9917 0 13:42 pts/1 00:00:00 grep nginx
执行命令后的进程:
- [root@localhost sbin]# ps -ef |grep nginx
- root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
- nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
- root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
- nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
- 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保存新的主进程号
- [root@songguoliang logs]# pwd
- /usr/local/nginx/logs
- [root@songguoliang logs]# ll
- 总用量 16
- -rw-r--r--. 1 nobody root 596 10月 8 13:02 access.log
- -rw-r--r--. 1 nobody root 881 10月 8 13:43 error.log
- -rw-r--r--. 1 root root 6 10月 8 13:43 nginx.pid
- -rw-r--r--. 1 root root 5 10月 8 13:22 nginx.pid.oldbin
- [root@songguoliang logs]# cat nginx.pid
- 10012
- [root@songguoliang logs]# cat nginx.pid.oldbin
- 9944
这时,nginx的新的实例和旧的实例同时工作,共同处理请求连接。接下来要关闭旧的实例进程。
(3)给旧的主进程发送WINCH信号,kill -WINCH 旧的主进程号
- [root@localhost sbin]# ps -ef |grep nginx
- root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
- nobody 9965 9944 0 13:29 ? 00:00:00 nginx: worker process
- root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
- nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
- root 10092 9917 0 14:05 pts/1 00:00:00 grep nginx
- [root@localhost sbin]# kill -WINCH 9944
- [root@localhost sbin]#
- [root@localhost sbin]#
- [root@localhost sbin]#
- [root@localhost sbin]# ps -ef |grep nginx
- root 9944 1 0 13:22 ? 00:00:00 nginx: master process ./nginx
- root 10012 9944 0 13:43 ? 00:00:00 nginx: master process ./nginx
- nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
- root 10094 9917 0 14:06 pts/1 00:00:00 grep nginx
旧的主进程号收到WINCH信号后,将旧进程号管理的旧的工作进程优雅的关闭。即一段时间后旧的工作进程全部关闭,只有新的工作进程在处理请求连接。这时,依然可以恢复到旧的进程服务,因为旧的进程的监听socket还未停止。
(4)给旧的主进程发送QUIT信号,使其关闭。
- [root@localhost sbin]# kill -QUIT 9944
- [root@localhost sbin]# ps -ef |grep nginx
- root 10012 1 0 13:43 ? 00:00:00 nginx: master process ./nginx
- nobody 10013 10012 0 13:43 ? 00:00:00 nginx: worker process
- 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不重新读取配置文件的情况下重新启动旧主进程的工作进程。
- [root@localhost sbin]# kill -HUP 9944
(2)优雅的关闭新的主进程
- [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)
拓展阅读(其他服务优雅重启参考)
- manners
- graceful
- grace
- plugin: new package for loading plugins · golang/go@0cbb12f · GitHub
- SpringBoot 优雅重启 - https://www.cnblogs.com/hpxiaokang/p/9300933.html
- 优雅的重启 mongoDB -https://www.jianshu.com/p/8ce4cfc1e845
- 优雅重启或停止 · Gin中文文档 https://www.kancloud.cn/shuangdeyu/gin_book/949445
- 优雅的重启/重载 · pm2(node/vue必会)从入门到精通 https://www.kancloud.cn/daiji/pm2/395305