zero down-time update服务的方案

从问题开始 

先来抛一块砖,对于静态编译的应用程序,比如用C、C++、Golang或者其它的语言编写的程序,如果我们修改一个BUG或者添加一个新的特性后,如何在服务不下线的情况下更远应用程序呢?

抛出了一个问题,一个很平常的问题,有人对问题思考比较透彻,比如牛顿,被苹果砸中了之后,引起了很多的思考,最后发现了万有引力定律。


如果你被苹果砸中了怎么办?

玩笑话一句,那我们如果被苹果砸中了会不死变成智障呢?


那么我们回到刚才这个问题 :

当我们修复BUG,添加新的需求后,如何如丝般顺滑地升级服务器应用程序,而不会中断服务?

这个问题意味着:

C / C++ / GO都是静态语言,所有的指令都编译在可执行文件,升级就意味着编译新的执行文件替换旧的执行文件,已经运行的进程如何加载新的image(可执行程序文件)去执行呢?

正在处理的业务逻辑不能中断,正在处理的连接不能暴力中断?

这种如丝般顺滑地升级应用程序,我们称之为热更新。

用个形象上的比喻表示就是:

你现在在坐卡车,卡车开到了150KM/H

然后,有个轮胎,爆了

然后,司机说,你就直接换吧,我不停车。你小心点换

哦,Lee哥,我明白了,在这些情况下,我们是不能使用哪个万能地“重启”去解决问题的。

第一种解决方案:灰度发布和A/B测试引起的思考 

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。利用nginx做灰度发布的方案如下图:


nginx是一个反向代理软件,可以把外网的请求转发到内网的业务服务器上,系统的分层的设计,一般我们把nginx归为接入层,当然LVS/F5/Apache等等都能去转发用户请求。比如我们来看一个nginx的配置:

http {

    upstream cluster {

        ip_hash;

        server 192.168.2.128:8086 weight=1 fail_timeout=15 max_fails =3;

        server 192.168.2.130:8086 weight=2 fail_timeout=15 max_fails =3;

    }

    server {

        listen 8080;

        location / {

            proxy_pass http://cluster;

        }

    }

}

我们对8080端口的访问,都会转发到cluster说定义的upstream里,upstream里会根据IP hash的策略转发给192.168.2.128和192.168.2.130的8086端口的服务上。这里配置的是ip hash,当然nginx还支持其他策略。

那么通过nginx如何去如丝般升级服务程序呢?


比如nginx的配置:

http {  

    upstream cluster {  

        ip_hash;

        server 192.168.2.128:8086 weight=1 fail_timeout=15 max_fails =3;

        server 192.168.2.130:8086 weight=2 fail_timeout=15 max_fails =3;

    }  


    server {  

        listen 80;  


        location / {

            proxy_pass http://cluster;  

        }  

    }  

}

假如我们的服务部署在192.168.2.128上,现在我们修复BUG或者增加新的特性后,我们重新部署了一台服务(比如192.168.2.130上),那么我们就可以修改nginx配置如上,然后执行nginx -s reload加载新的配置,这样我们现有的连接和服务都没有断掉,但是新的业务服务已经可以开始服务了,这就是通过nginx做的灰度发布,依据这样的方法做的测试称之为A/B测试,好了,那如何让老的服务彻底停掉呢?

可以修改nginx的配置如下,即在对应的upstream的服务器上添加down字段:

http {  

    upstream cluster {  

        ip_hash;

server 192.168.2.128:8086 weight=1 fail_timeout=15 max_fails =3down;

        server 192.168.2.130:8086 weight=2 fail_timeout=15 max_fails =3;

    }  


    server {  

        listen 80;  


        location / {

            proxy_pass http://cluster;  

        }  

    }  

}

这样等过一段时间,就可以把192.168.2.128上的服务给停掉了。

这就是通过接入层nginx的一个如丝般顺滑的一个方案,这种思想同样可以应用于其他的比如LVS、apache等,当然还可以通过DNS,zookeeper,etcd等,就是把流量全都打到新的系统上去。

灰度发布解决的流量转发到新的系统中去,但是如果对于nginx这样的应用程序,或者我就是要在这台机器上升级image,那怎么办呢?这就必须要实现热更新,这里需要考虑的问题是旧的服务如果缓存了数据怎么办?如果正在处理业务逻辑怎么办?

第二种解决方案:nginx的热更新方案 

nginx采用Master/Worker的多进程模型,Master进程负责整个nginx进程的管理,比如停机、日志重启和热更新等等,worker进程负责用户的请求处理。


如上一个nginx里配置的所有的监听端口都是首先在Master进程里create的socket(sfd)、bind、listen,然后Master在创建worker进程的时候把这些socket通过unix domain socket复制给了Worker进程,Worker进程把这些socket全都添加到epoll,之后如果有客户端连接进来了,则由worker进程负责处理,那么也就是说用户的请求是由worker进程处理的。

先交代了nginx的IO处理模型的背景,然后我们再看nginx的热更新方案:

升级的步骤:

第一步:升级nginx二进制文件,需要先将新的nginx可执行文件替换原有旧的nginx文件,然后给nginx master进程发送USR2信号,告知其开始升级可执行文件;nginx master进程会将老的pid文件增加.oldbin后缀,然后调用exec函数拉起新的master和worker进程,并写入新的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

关于exec家族的函数说明见下:

NAME

       execl, execlp, execle, execv, execvp, execvpe - execute a file

SYNOPSIS

       #include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...

                       /* (char  *) NULL */);

       int execlp(const char *file, const char *arg, ...

                       /* (char  *) NULL */);

       int execle(const char *path, const char *arg, ...

                       /*, (char *) NULL, char * const envp[] */);

       int execv(const char *path, char *const argv[]);

       int execvp(const char *file, char *const argv[]);

       int execvpe(const char *file, char *const argv[],

                       char *const envp[]);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       execvpe(): _GNU_SOURCE

DESCRIPTION

The  exec()  family of functions replaces the current process image with a new process image.  The functions described in this manual page are front-ends for execve(2).

       (See the manual page for execve(2) for further details about the replacement of the current process image.)

       The initial argument for these functions is the name of a file that is to be executed.

       The const char *arg and subsequent ellipses in the execl(), execlp(), and execle() functions can be thought of as arg0, arg1, ..., argn.  Together they describe a  list

       of  one or more pointers to null-terminated strings that represent the argument list available to the executed program.  The first argument, by convention, should point

       to the filename associated with the file being executed.  The list of arguments must be terminated by a null pointer, and, since  these  are  variadic  functions,  this

       pointer must be cast (char *) NULL.

       The  execv(),  execvp(),  and execvpe() functions provide an array of pointers to null-terminated strings that represent the argument list available to the new program.

       The first argument, by convention, should point to the filename associated with the file being executed.  The array of pointers must be terminated by a null pointer.

       The execle() and execvpe() functions allow the caller to specify the environment of the executed program via the argument envp.  The envp argument is an array of point‐

       ers  to null-terminated strings and must be terminated by a null pointer.  The other functions take the environment for the new process image from the external variable

       environ in the calling process.


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

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进程不会立即退出,需要待消息处理完后再退出。

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

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

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

几个核心的步骤和命令说明如下:

操作的命令

master进程相关信号

USR2 升级可执行文件

WINCH 优雅停止worker进程

QUIT 优雅停止master进程

worker进程相关信号

TERM, INT 快速退出进程

QUIT 优雅停止进程


nginx本身是一个代理组件(代理http TCP UDP),本身并没有什么业务逻辑,也即没有什么状态数据可言,即使有业务逻辑这套方案也是可以的。

nginx是如何graceful shutdown的?也即正在处理的http请求和长连接怎么处理?


如何启动新的的image:


好了,以上就是zero down-time update的一些方案,如果还有不明白可以看下面这个视频。

https://www.bilibili.com/video/av57429199

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,273评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,349评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,709评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,520评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,515评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,158评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,755评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,660评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,203评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,287评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,427评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,122评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,801评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,272评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,393评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,808评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,440评论 2 359

推荐阅读更多精彩内容