从nginx热更新聊一聊Golang中的服务器热更新(上)

静态语言在服务器编程时都会遇到这样的问题:如何保证已有的连接服务不中断同时又升级版本?
最近花了点时间看了下nginx热更新代码流程,想了下结合之前的经验一并总结下热更新

1. 热更新是什么?

简单翻译成人类可读的实例是如下这个样子:

举个例子,你现在在坐卡车,卡车开到了150KM/H
然后,有个轮胎,爆了
然后,司机说,你就直接换吧,我不停车。你小心点换
嗯。就这个意思

2.网关中的热更新

服务程序热更新这个问题在层7网关中尤其严重,网关中承载着大量的请求,包括HTTP/HTTPS短连接HTTP/HTTPS长连接、甚至是websocket这种超长连接(websocket通常连接时间会很长,十几分钟到几天不等)。这样的话我们势必不能讲服务程序停止再启动的冷更新服务进程热更新是非常有必要的。

网关作为一个基础组件,需要保证高可用,是很难将其先停下来再更新的;

有人说可以使用负载均衡将需要更新的组件先隔离,再停机更新,但是如果是一个很小的集群没有负载均衡呢,又或者这样手动一台一台升级也着实麻烦,部分情况下就算隔离了也不过是不会有新的连接过来,旧的连接/请求依旧需要处理完成,否则就会造成部分服务不可用。

不过实际上线上操作是集群隔离加热更新一起操作

3.nginx热更新(Upgrading Executable on the Fly)

nginx [engine x]Igor Sysoev编写的一个HTTP和反向代理服务器,另外它也可以作为邮件代理服务器。 它已经在众多流量很大的俄罗斯网站上使用了很长时间,这些网站包括YandexMail.RuVKontakte,以及Rambler。据Netcraft统计,在2012年8月份,世界上最繁忙的网站中有11.48%使用Nginx作为其服务器或者代理服务器。

NginX采用Master/Worker的多进程模型,Master进程负责整个NginX进程的管理。Nginx模块化热更新Http处理流程日志等机制都非常经典。这里将会简要介绍一下热更新的机制

3.1 nginx热升级流程

步骤1、升级nginx二进制文件,需要先将新的nginx可执行文件替换原有旧的nginx文件,然后给nginx master进程发送USR2信号,告知其开始升级可执行文件;nginx master进程会将老的pid文件增加.oldbin后缀,然后拉起新的masterworker进程,并写入新的master进程的pid

UID        PID  PPID  C STIME TTY          TIME CMD
root      4584     1  0 Oct17 ?        00:00:00 nginx: master process /usr/local/apigw/apigw_nginx/nginx
root     12936  4584  0 Oct26 ?        00:03:24 nginx: worker process
root     12937  4584  0 Oct26 ?        00:00:04 nginx: worker process
root     12938  4584  0 Oct26 ?        00:00:04 nginx: worker process
root     23692  4584  0 21:28 ?        00:00:00 nginx: master process /usr/local/apigw/apigw_nginx/nginx
root     23693 23692  3 21:28 ?        00:00:00 nginx: worker process
root     23694 23692  3 21:28 ?        00:00:00 nginx: worker process
root     23695 23692  3 21:28 ?        00:00:00 nginx: worker process

步骤2、在此之后,所有工作进程(包括旧进程和新进程)将会继续接受请求。这时候,需要发送WINCH信号给nginx master进程master进程将会向worker进程发送消息,告知其需要进行graceful shutdownworker进程会在连接处理完之后进行退出。

UID        PID  PPID  C STIME TTY          TIME CMD
root      4584     1  0 Oct17 ?        00:00:00 nginx: master process /usr/local/apigw/apigw_nginx/nginx
root     12936  4584  0 Oct26 ?        00:03:24 nginx: worker process
root     12937  4584  0 Oct26 ?        00:00:04 nginx: worker process
root     12938  4584  0 Oct26 ?        00:00:04 nginx: worker process
root     23692  4584  0 21:28 ?        00:00:00 nginx: master process /usr/local/apigw/apigw_nginx/nginx
#若旧的worker进程还需要处理连接,则worker进程不会立即退出,需要待消息处理完后再退出

步骤3、经过一段时间之后,将会只会有新的worker进程处理新的连接。

注意,旧master进程并不会关闭它的listen socket;因为如果出问题后,需要回滚,master进程需要法重新启动它的worker进程。

步骤4、如果升级成功,则可以向旧master进程发送QUIT信号,停止老的master进程;如果新的master进程(意外)退出,那么旧master进程将会去掉自己的pid文件的.oldbin后缀。

3.2 nginx热更新相关信号

master进程相关信号

USR2	升级可执行文件
WINCH	优雅停止worker进程
QUIT	优雅停止master进程

worker进程相关信号

TERM, INT	快速退出进程
QUIT	优雅停止进程

3.3 nginx相关代码走读

1、USR2流程

USR2流程
master收到USR2信号后,会拉起新的master nginx进程

新的master进程拉起新的worker进程

最终,老的worker进程新的worker进程共用一个listen socket,接受连接。

也就是此时这里实现了端口复用的技术,也就是REUSEPORT

2、WINCH流程

WINCH流程
master进程收到WINCH信号后,会给各个worker进程发送QUIT信号,让其优雅退出;master进程并不再处理新的连接。

WINCH流程

worker graceful shutdown流程,关闭listen socket,不再处理新的连接;待已有连接处理完后,清理连接,退出进程。

3、QUIT流程

master graceful shutdown流程,没什么好说的

3.4 nginx热升级 QA

1、如何防止多次可执行文件触发热更新?
相关代码
ngx_signal_handler
-->
case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
    if (ngx_getppid() == ngx_parent || ngx_new_binary > 0) {

        /*
            * Ignore the signal in the new binary if its parent is
            * not changed, i.e. the old binary's process is still
            * running.  Or ignore the signal in the old binary's
            * process if the new binary's process is already running.
            */

        action = ", ignoring";
        ignore = 1;
        break;
    }

    ngx_change_binary = 1;
    action = ", changing binary";
    break;

若老的nginx还在,nginx无法进行热更新二进制文件

2、nginx升级过程中,发现新的可执行文件出现问题该如何回滚?
  • a、向旧master进程发送HUP信号。旧进程将启动新的worker进程,而且不会重新读取配置。之后,通过向新的主master进程发送QUIT信号,可以优雅地关闭新的masterworker进程
  • b、将TERM信号发送到新的master进程,然后新的master进程将向其worker进程发送一条消息,让它们立即退出,这种退出不是graceful shutdown。当新的master进程退出时,旧的master进程将启动新的worker进程
  • c、如果新的进程没有退出,则应该向它们发送终止KILL信号。当新的master进程退出时,旧的master进程将启动新的工作进程。
3、什么是graceful shutdown

本文中的graceful shutdown是指server不再处理新的连接,但是进程不会立即退出,待所有连接断开后再退出进程。

4. 总结

总结一下个人在nginx二进制文件热升级时用的命令

cd /usr/local/nginx
cp nginx nginx_bak 
mv /data/nginx/nginx ./nginx #需要使用mv来更新二进制文件
./nginx -t #尝试启动,查看其加载配置文件等初始化功能是否正常

netstat -anp | grep -E "80|443" | grep nginx #检查连接状态
kill -USR2 `cat /usr/local/nginx/nginx.pid` #升级nginx可执行文件,此时会有两组nginx master和worker进程
kill -WINCH `cat /usr/local/nginx/nginx.pid.oldbin` #新的可执行文件启动ok,且能够正常处理数据流,告知老的master进程去通知其worker进程进行优雅退出

...
kill -QUIT `cat /usr/local/nginx/nginx.pid.oldbin` #待所有的老的nginx worker进程优雅退出后(处理完连接),停止老的master进程

TODO:nginx还会有依赖的so文件的热升级–其实更应该属于后台进程的so文件热升级流程,我在使用它的时候也踩过坑–主要原因还是操作不规范,对so其加载运行原理不够熟悉导致

5.额外

实际上,静态语言后端server有一套固定的热升级(单进程)流程,其基本流程如下:

若需要支持热升级的是多进程,那么nginx的热升级过程是最值得参考的

  • 1、通过调用 fork/exec 启动新的版本的进程

  • 2、子进程调用接口获取从父进程继承的 socket 文件描述符重新监听socket

  • 3、在此过程中,不会对用户请求造成任何中断。

nginx的热升级流程也是类似,只不过由于nginx工作是多进程,故它会先启动新版本的一组master/worker进程

然后停止老的worker进程,让其不处理连接,由新的worker进程来处理连接;

升级完毕后,即可退出老的master进程,热升级完成。

6.参考

http://tengine.taobao.org/nginx_docs/cn/docs/control.html