從問(wèn)題開(kāi)始
成都創(chuàng)新互聯(lián)公司主要從事網(wǎng)站制作、成都做網(wǎng)站、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)新河,十載網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來(lái)電咨詢建站服務(wù):028-86922220
先來(lái)拋一塊磚,對(duì)于靜態(tài)編譯的應(yīng)用程序,比如用C、C++、Golang或者其它的語(yǔ)言編寫的程序,如果我們修改一個(gè)BUG或者添加一個(gè)新的特性后,如何在服務(wù)不下線的情況下更遠(yuǎn)應(yīng)用程序呢?
拋出了一個(gè)問(wèn)題,一個(gè)很平常的問(wèn)題,有人對(duì)問(wèn)題思考比較透徹,比如牛頓,被蘋果砸中了之后,引起了很多的思考,最后發(fā)現(xiàn)了萬(wàn)有引力定律。
如果你被蘋果砸中了怎么辦?
玩笑話一句,那我們?nèi)绻惶O果砸中了會(huì)不死變成智障呢?
那么我們回到剛才這個(gè)問(wèn)題 :
當(dāng)我們修復(fù)BUG,添加新的需求后,如何如絲般順滑地升級(jí)服務(wù)器應(yīng)用程序,而不會(huì)中斷服務(wù)?
這個(gè)問(wèn)題意味著:
C / C++ / GO都是靜態(tài)語(yǔ)言,所有的指令都編譯在可執(zhí)行文件,升級(jí)就意味著編譯新的執(zhí)行文件替換舊的執(zhí)行文件,已經(jīng)運(yùn)行的進(jìn)程如何加載新的image(可執(zhí)行程序文件)去執(zhí)行呢?
正在處理的業(yè)務(wù)邏輯不能中斷,正在處理的連接不能暴力中斷?
這種如絲般順滑地升級(jí)應(yīng)用程序,我們稱之為熱更新。
用個(gè)形象上的比喻表示就是:
你現(xiàn)在在坐卡車,卡車開(kāi)到了150KM/H
然后,有個(gè)輪胎,爆了
然后,司機(jī)說(shuō),你就直接換吧,我不停車。你小心點(diǎn)換
哦,Lee哥,我明白了,在這些情況下,我們是不能使用哪個(gè)萬(wàn)能地“重啟”去解決問(wèn)題的。
第一種解決方案:灰度發(fā)布和A/B測(cè)試引起的思考
灰度發(fā)布(又名金絲雀發(fā)布)是指在黑與白之間,能夠平滑過(guò)渡的一種發(fā)布方式。在其上可以進(jìn)行A/B testing,即讓一部分用戶繼續(xù)用產(chǎn)品特性A,一部分用戶開(kāi)始用產(chǎn)品特性B,如果用戶對(duì)B沒(méi)有什么反對(duì)意見(jiàn),那么逐步擴(kuò)大范圍,把所有用戶都遷移到B 上面來(lái)。灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時(shí)候就可以發(fā)現(xiàn)、調(diào)整問(wèn)題,以保證其影響度。利用nginx做灰度發(fā)布的方案如下圖:
nginx是一個(gè)反向代理軟件,可以把外網(wǎng)的請(qǐng)求轉(zhuǎn)發(fā)到內(nèi)網(wǎng)的業(yè)務(wù)服務(wù)器上,系統(tǒng)的分層的設(shè)計(jì),一般我們把nginx歸為接入層,當(dāng)然LVS/F5/Apache等等都能去轉(zhuǎn)發(fā)用戶請(qǐng)求。比如我們來(lái)看一個(gè)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;
}
}
}
我們對(duì)8080端口的訪問(wèn),都會(huì)轉(zhuǎn)發(fā)到cluster說(shuō)定義的upstream里,upstream里會(huì)根據(jù)IP hash的策略轉(zhuǎn)發(fā)給192.168.2.128和192.168.2.130的8086端口的服務(wù)上。這里配置的是ip hash,當(dāng)然nginx還支持其他策略。
那么通過(guò)nginx如何去如絲般升級(jí)服務(wù)程序呢?
比如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;
}
}
}
假如我們的服務(wù)部署在192.168.2.128上,現(xiàn)在我們修復(fù)BUG或者增加新的特性后,我們重新部署了一臺(tái)服務(wù)(比如192.168.2.130上),那么我們就可以修改nginx配置如上,然后執(zhí)行nginx -s reload加載新的配置,這樣我們現(xiàn)有的連接和服務(wù)都沒(méi)有斷掉,但是新的業(yè)務(wù)服務(wù)已經(jīng)可以開(kāi)始服務(wù)了,這就是通過(guò)nginx做的灰度發(fā)布,依據(jù)這樣的方法做的測(cè)試稱之為A/B測(cè)試,好了,那如何讓老的服務(wù)徹底停掉呢?
可以修改nginx的配置如下,即在對(duì)應(yīng)的upstream的服務(wù)器上添加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;
}
}
}
這樣等過(guò)一段時(shí)間,就可以把192.168.2.128上的服務(wù)給停掉了。
這就是通過(guò)接入層nginx的一個(gè)如絲般順滑的一個(gè)方案,這種思想同樣可以應(yīng)用于其他的比如LVS、apache等,當(dāng)然還可以通過(guò)DNS,zookeeper,etcd等,就是把流量全都打到新的系統(tǒng)上去。
灰度發(fā)布解決的流量轉(zhuǎn)發(fā)到新的系統(tǒng)中去,但是如果對(duì)于nginx這樣的應(yīng)用程序,或者我就是要在這臺(tái)機(jī)器上升級(jí)image,那怎么辦呢?這就必須要實(shí)現(xiàn)熱更新,這里需要考慮的問(wèn)題是舊的服務(wù)如果緩存了數(shù)據(jù)怎么辦?如果正在處理業(yè)務(wù)邏輯怎么辦?
第二種解決方案:nginx的熱更新方案
nginx采用Master/Worker的多進(jìn)程模型,Master進(jìn)程負(fù)責(zé)整個(gè)nginx進(jìn)程的管理,比如停機(jī)、日志重啟和熱更新等等,worker進(jìn)程負(fù)責(zé)用戶的請(qǐng)求處理。
如上一個(gè)nginx里配置的所有的監(jiān)聽(tīng)端口都是首先在Master進(jìn)程里create的socket(sfd)、bind、listen,然后Master在創(chuàng)建worker進(jìn)程的時(shí)候把這些socket通過(guò)unix domain socket復(fù)制給了Worker進(jìn)程,Worker進(jìn)程把這些socket全都添加到epoll,之后如果有客戶端連接進(jìn)來(lái)了,則由worker進(jìn)程負(fù)責(zé)處理,那么也就是說(shuō)用戶的請(qǐng)求是由worker進(jìn)程處理的。
先交代了nginx的IO處理模型的背景,然后我們?cè)倏磏ginx的熱更新方案:
升級(jí)的步驟:
第一步:升級(jí)nginx二進(jìn)制文件,需要先將新的nginx可執(zhí)行文件替換原有舊的nginx文件,然后給nginx master進(jìn)程發(fā)送USR2信號(hào),告知其開(kāi)始升級(jí)可執(zhí)行文件;nginx master進(jìn)程會(huì)將老的pid文件增加.oldbin后綴,然后調(diào)用exec函數(shù)拉起新的master和worker進(jìn)程,并寫入新的master進(jìn)程的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
關(guān)于exec家族的函數(shù)說(shuō)明見(jiàn)下:
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a file
SYNOPSIS
#include
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.
第二步:在此之后,所有工作進(jìn)程(包括舊進(jìn)程和新進(jìn)程)將會(huì)繼續(xù)接受請(qǐng)求。這時(shí)候,需要發(fā)送WINCH信號(hào)給nginx master進(jìn)程,master進(jìn)程將會(huì)向worker進(jìn)程發(fā)送消息,告知其需要進(jìn)行g(shù)raceful shutdown,worker進(jìn)程會(huì)在連接處理完之后進(jìn)行退出。
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進(jìn)程還需要處理連接,則worker進(jìn)程不會(huì)立即退出,需要待消息處理完后再退出。
第三步:經(jīng)過(guò)一段時(shí)間之后,將會(huì)只會(huì)有新的worker進(jìn)程處理新的連接。
注意,舊master進(jìn)程并不會(huì)關(guān)閉它的listen socket;因?yàn)槿绻鰡?wèn)題后,需要回滾,master進(jìn)程需要法重新啟動(dòng)它的worker進(jìn)程。
第四步:如果升級(jí)成功,則可以向舊master進(jìn)程發(fā)送QUIT信號(hào),停止老的master進(jìn)程;如果新的master進(jìn)程(意外)退出,那么舊master進(jìn)程將會(huì)去掉自己的pid文件的.oldbin后綴。
幾個(gè)核心的步驟和命令說(shuō)明如下:
操作的命令
master進(jìn)程相關(guān)信號(hào)
worker進(jìn)程相關(guān)信號(hào)
nginx本身是一個(gè)代理組件(代理http TCP UDP),本身并沒(méi)有什么業(yè)務(wù)邏輯,也即沒(méi)有什么狀態(tài)數(shù)據(jù)可言,即使有業(yè)務(wù)邏輯這套方案也是可以的。
nginx是如何graceful shutdown的?也即正在處理的http請(qǐng)求和長(zhǎng)連接怎么處理?
如何啟動(dòng)新的的image:
好了,以上就是zero down-time update的一些方案,如果還有不明白可以看下面這個(gè)視頻。
https://www.bilibili.com/video/av57429199