Featured image of post Nginx平滑升级

Nginx平滑升级

我们都知道nginx的命令执行格式是nginx 参数,参数主要有如下的:

-?, -h 打印帮助。
-v 打印版本。
-V 打印 NGINX 版本、编译器版本并配置参数。
-t 不要运行,只测试配置文件。NGINX 检查配置的语法是否正确,然后尝试打开配置中引用的文件。
-q 在配置测试期间抑制非错误消息。
-s signal 向主进程发送信号:stop(暴力停止)、quit(优雅退出)、reopen(重新打开日志)、reload(重新加载)。(版本 >= 0.7.53)
-p prefix 设置前缀路径(默认:/usr/local/nginx/)。(版本 >= 0.7.53)
-c filename 指定 NGINX 应该使用哪个配置文件而不是默认配置文件。
-g directives 设置全局指令。(版本 >= 0.7.4)

其他的没啥好讲的,我们只讲一个东西nginx -s 参数,如果你以yum方式安装过部分非官方的nginx你就会发现他们自带了日志切割的功能,nginx本身当然是没有这个功能的,我们经过分析发现一般是使用系统自带的logrotate实现的。我们需要了解的是,所谓的日志切割本质上就是把现有的日志重命名为备份日志,让nginx写入到一个新的文件里面去(但是如果你自己直接重命名你就会发现一个问题,日志文件虽然被重命名了,但是日志还是往老的文件里面写,当然原因也很简单:程序是通过文件描述符去操作的,你重命名不会改变这种关系,这个时候你需要让nginx重新打开即nginx -s reopen)。

这个时候重点来了,我们查看logrotate的配置文件,有如下一行:kill -USR1 'cat /var/run/nginx.pid',是不是跟你想象中不同,kill不是杀死程序吗?我们平常大部分也是这么做的,但是其实不然,我们也可以通过kill命令向程序发送某些信号。比如根据nginx程序的定义kill -USR1 pid就跟nginx -s reopen是等效的。其他的如下表:

TERM, INT Quick shutdown
QUIT Graceful shutdown
KILL Halts a stubborn process
HUP Configuration reloadStart the new worker processes with a new configurationGracefully shutdown the old worker processes
USR1 Reopen the log files
USR2 Upgrade Executable on the fly
WINCH Gracefully shutdown the worker processes

ok,有了前面的知识储备,我们就可以进行后面的工作了。如何优雅地不停机的升级nginx?

假设nginx正在提供服务,一切正常,现在我们想要对nginx进行热升级,大致步骤如下:

  1. 最重要的一步,备份。
  2. 下载新版本的nginx,根据老版本的编译选项,对新版本完成编译的步骤,只对新版本进行编译操作,不执行安装操作,换句话说就是,只执行make命令,不执行make install命令,完成编译操作后,即可获取到我们需要的新版本的二进制文件,之后,我们需要根据实际情况判断哪些文件需要被替换,此处描述的”根据情况判断”在后文中会有解释,先不用纠结,此处假设,根据情况判断后,只需要替换nginx二进制文件。
  3. 确定已经备份老版本的nginx二进制文件,以防万一,用编译好的新版本的nginx二进制文件替换老版本的nginx二进制文件,此时老版本的nginx仍然在内存中正常运行,所以不用担心,我们替换的只是硬盘中的二进制文件,做好备份即可。
  4. 对nginx的master进程(正在运行的老版本的master进程)发送USR2信号,老版本的master进程收到信号后,会通过新版本的二进制文件启动新版本的master进程,新版本的master进程会启动新版本的worker进程,此时新老版本的nginx进程同时存在。
  5. 向老版本的master进程发送WINCH信号,以便先优雅的停止老版本的worker进程,新的请求会被新版本的worker进程处理,此时老的master进程仍然存在,留下老的master进程是为了以防万一,以便随时回滚,此时老版本的master进程、新版本的master进程和新版本的worker进程同时存在,升级过程暂且完毕。
  6. 如果升级后万一出现问题,则可以随时进行回滚,由于老版本的master进程并未停止,所以我们可以向老的master进程发送HUP信号,即可通过老版本的master进程重新生成老版本的worker进程,当老版本的worker进程重新被拉起后,即可向新版本的master进程发送QUIT信号,以便优雅的关闭新版本的nginx进程,回滚操作完毕。

大概的命令如下:

 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
# 下载源码包
$ 命令省略

# 获取目前nginx的编译参数
$ nginx -V

# 解压和编译新的nginx(注意:不能使用make install)
$ 解压命令省略
$ make

# 备份nginx程序
$ 备份命令省略

# 备份modules,如果你编译的时候有模块是动态的(dynamic),可能就需要替换动态库
$ 备份命令省略

# 替换新的程序和动态库到程序目录,make之后可以在objs目录找到
$ 命令省略

# 向老版本的nginx的master进程发送USR2信号,老master进程会使用新版本的二进制文件来启动新的master进程
$ kill -USR2 $(ps aux | grep nginx | grep master | awk 'NR==1 {print $2}')

# 此时,新老版本的nginx进程同时存在,但是我们的最终目的是使用新版本的nignx提供服务,于是,我们需要先优雅的停止老版本的worker进程,此时就需要用到”WINCH”信号了,当老版本的master进程接收到”WINCH”信号后,会停止老版本的worker进程,但是老版本的master进程并不会停止,我们留下老版本的master进程是为了以防万一
$ kill -WINCH $(ps aux | grep nginx | grep master | awk 'NR==1 {print $2}')

# 老版本的worker进程已经优雅的停止了(优雅的停止是在处理完当前连接的请求后再行停止),但是老版本的master进程还在。你可以随时通过老的master进程从新启动一个老版本的worker进程,然后优雅的停止新版本的nginx进程(有问题才执行下面的)
$ kill -HUB $(ps aux | grep nginx | grep master | awk 'NR==1 {print $2}')
$ kill -QUIT $(ps aux | grep nginx | grep master | awk 'NR==2 {print $2}')