Nginx 平滑升级

语言: CN / TW / HK

随着 Nginx 越来越流行,Nginx 的版本迭代也越来越频繁,当我们需要使用某些新版本的特性或者修复某个旧版本的 BUG 时,就要对 Nginx 进行升级。然而线上业务大多是 7*24 小时不间断运行的,我们需要在升级的时候保证不影响在线用户的访问。Nginx 的热升级功能可以解决上述问题,它允许新老版本灰度地平滑过渡,这受益于 Nginx 的多进程架构。

Nginx 多进程架构

  • master 是 Nginx 的主进程,master 进程负责启动 worker 进程,接收来自外界的信号,管理 worker 进程。
  • worker 进程负责处理客户端的连接请求。

配置了缓存功能后,Nginx 就会启动 Cache Manager 进程和 Cache Loader 进程。

  • Cache Manager 是一个常驻进程,它周期性地运行来淘汰过期缓存或者强制删除某些缓存文件来释放磁盘空间。在两次缓存管理器启动的间隔,缓存的数据量可能短暂超过配置的大小。
  • Cache Loader 进程只在启动时运行一次,读取对应目录中存在的缓存文件,在内存中生成对应的文件元数据。

操作系统规定,每一个进程都必须由另一个进程启动,这两个进程就称为父子进程,其中,子进程自动继承父进程已经申请到的资源,比如监听的 80 端口。在Linux中,子进程是由 fork 函数创建的,最初它只是父进程的副本。比如在生产环境中启动 Nginx 时(即 master_process on),Nginx 会在绑定 80 端口后再用 fork 函数生成 worker 子进程(注意,Nginx 会自动将父进程名字改为 nginx: master process),这样,worker 进程也可以通过 80 端口与客户端建立 TCP 连接。当然,多个 worker 进程同时监听 80 端口时,系统内核会有一套算法决定某个连接由哪个 worker 进程处理(可以参考Linux 3.9 内核版本后提供的SO_REUSEPORT选项),从而均衡多个 worker 子进程间的负载。

Nginx 支持的信号

# master进程支持的信号
TERM,INT: 立刻退出,相当于 nginx -s stop。
QUIT: 等待工作进程结束后再退出,优雅地退出,相当于 nginx -s quit。
HUP: 重新加载配置文件,使用新的配置启动工作进程,并逐步关闭旧进程。相当于 nginx -s reload 命令。
USR1: 重新打开日志文件,相当于 nginx -s reopen。
USR2: 启动新的主进程,实现热升级。
WINCH: 逐步关闭工作进程

#worker进程支持的信号
TERM,INT: 立刻退出
QUIT: 等待请求处理结束后再退出
USR1: 重新打开日志文件

热升级主要用到了 USER2 和 WINCH/QUIT 信号。

Nginx 配置文件

Nginx 的配置文件如下:

worker_processes  2; #启动2个worker进程
user nginx; #worker用户为nginx

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;  #监听80端口
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

使用以下命令启动 Nginx:

sbin/nginx

此时 Nginx 服务已经启动并且可以正常访问:

平滑升级步骤

查看当前 Nginx 进程,可以看到有一个 master 进程,进程号为 14912,并且有两个 worker 进程负责处理客户端连接请求。

查看端口号的监听情况,80 端口被进程号为 14912 的进程监听,也就是 master 进程,由于 worker 进程是由 master 进程 fork 出来的,因此 worker 进程也会监听 80 端口。

[root@nginx-plus1 sbin]# netstat -antlp | grep 80
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      14912/nginx: master 

那么,既然 master 与 worker 可以绑定同一端口,那么升级新版本 Nginx 时,也由现在的老 master 进程启动(子进程默认是父进程的副本,但通过 exec 函数可以载入新版本的 Nginx 程序。这样,新 master 进程就是老 master 进程的子进程,可以共享老版本 Nginx 已经打开的、包括端口在内的各类资源。至此,两个版本的 Nginx 同时运行并接收请求,然后只要老版本的 Nginx 停止建立新连接,内核自然只会将新的连接交给新版本的 Nginx 处理,等到老版本 Nginx 处理完现存的客户请求后可令其退出,这就完成了平滑升级。

平滑升级 Nginx 通常会经历 3 个阶段:

  • 1.仅老 Nginx 进程在运行,此时先备份 Nginx 二进制文件,再用新版本的 Nginx 二进制文件覆盖原位置,然后通过 kill 向老 master 进程发送 USR2 信号。这样老 master 进程就会生成新的子进程,同时用 exec 函数载入新版本 Nginx 的二进制文件,并将进程改名为nginx: master process。
  • 2.新老 Nginx 进程同时并存,此时需要通过信号 QUIT 命令老 master 进程优雅退出。
  • 3.当处理完所有请求后,老版本的 worker 和 master 进程依次退出。当老版本的 master、worker 进程都退出后,根据 Linux 内核的规则,pid 为 1 的系统守护进程将成为新 master 的父进程。此时平滑升级完毕。

平滑升级实践

备份并替换旧版本 Nginx 二进制文件

旧版本的 Nginx 为 1.14.2 版本:

[root@nginx-plus1 nginx]# /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.14.2

备份二进制文件:

cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old

下载新版本 Nginx 安装包,版本为 1.19.0,编译生成二进制文件:

tar -xzvf nginx-1.19.0.tar.gz
/root/nginx-1.19.0
./configure
make  #make这一步骤其实就已经生成二进制文件了,后面不需要make install了,make install其实就是把文件拷贝到指定目录

替换旧版本的二进制文件:

cp /root/nginx-1.19.0/objs/nginx /usr/local/nginx/sbin/nginx -f

通过 kill 命令向老 master 进程发送 USR2 信号,让老 master 生成新的子进程(新 master 进程),同时用 exec 函数载入新版本的 Nginx 二进制文件。

kill -USR2 14912

拉起新 Nginx 进程

可以看到新 master 的父进程是老 master 进程。并且 80 端口还是由老 master 进程在监听,由于新 master 进程和新 worker 进程会继承老 master 进程的资源,因此它们也能监听 80 端口。

此时新老 Nginx 同时处理服务。可以看到此时服务可以正常访问。

让老 Nginx 进程优雅地退出

向老 master 进程发送 QUIT 信号,当它的 worker 子进程退出后,老 master 进程也会自行退出。

kill -QUIT 14912

此时只剩下新的 master 进程和 worker 进程。新 master 进程的父进程变为 pid 为 1 的系统守护进程。可以注意到有个进程号为 15506 的 worker 进程,它的用户是 nobody,这个进程是在升级时出现的,等老 master 和 老 worker 进程退出后,新的 worker 进程的用户就是我们指定的 nginx 用户了。

并且 80 端口由新的 master 进程和新的 worker 进程监听。

[root@nginx-plus1 sbin]# netstat -antlp | grep 80
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      15505/nginx: master

等待老 master 和 worker 进程退出以后,客户端尝试访问服务,依然可以正常访问。

回滚

通过上述方式升级以后,只保留了新的 master 进程,这时如果需要从新版本回滚到老版本,就得重新执行一次“升级”。

还有一种更简单的回滚方法,就是向老 master 进程发送 WINCH 信号而不是 QUIT 信号,这样老 worker 进程全部退出后,老 master 进程仍然存在。由于老 master 进程是由老版本的 Nginx 二进制文件启动,这样回滚很容易,只要将它的 worker 进程重新拉起,即可向用户提供旧版本服务,同时要求新版本的 Nginx 进行优雅退出即可。

假设升级前的状态如下所示:

第一步和之前一样向老 master 进程发送 USR2 信号,拉起新 master 进程和新 worker 进程。

kill -USR2 9209

这次向老 master 进程发送 WINCH 信号,而不是 QUIT 信号,这样只会退出老 worker 进程,而保留老 master 进程,便于回滚。

kill -WINCH 9209

此时老 master 进程依然存在。

并且 80 端口是由老 master 进程监听的,新 master 进程和新 worker 进程会继承老 master 进程的资源从而监听 80 端口。

[root@nginx-plus1 sbin]# netstat -antlp | grep 80
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      9209/nginx: master  

此时向老 master 进程发送 HUP 信号,它会重新拉起 worker 子进程。

kill -HUP 9209

查看新拉起的 worker 进程:

然后再向新 master 进程发送 QUIT 信号,让新 master 进程和 新 worker 进程优雅地退出。

kill -QUIT 9523

此时就只剩下老 master 和 老 worker 进程,回滚完成。

最后记得把二进制文件改回老版本的二进制文件。

mv /usr/local/nginx/sbin/nginx.old /usr/local/nginx/sbin/nginx

参考链接

欢迎关注