優(yōu)雅發(fā)布的概念
DevOps、敏捷作為CI/CD思想的延伸已經(jīng)成為研發(fā)的基本共識(shí)和文化基礎(chǔ),今天談到的優(yōu)雅發(fā)布和和CD中的Deployment是同一個(gè)單詞但是側(cè)重點(diǎn)卻不同,CI/CD的過(guò)程講究的是如何將開(kāi)發(fā)、測(cè)試和部署的過(guò)程串起來(lái)達(dá)到一氣呵成的效果,所以有持續(xù)集成(CI)和持續(xù)部署(CD)的概念,也有Pipeline的概念,基于敏捷的研發(fā)理念迭代速度很快,在流水線上的每一個(gè)角色,甚至包括產(chǎn)品經(jīng)理都像是在流水線旁等待任務(wù)一樣,PRD一旦發(fā)布,這個(gè)流水線就開(kāi)始運(yùn)轉(zhuǎn)了,爭(zhēng)分奪秒地上線去搶奪用戶(hù)和流量。尤其是上線前的Dev、QA和Staging階段流水線的特性更是被嚴(yán)重依賴(lài),優(yōu)雅發(fā)布這個(gè)時(shí)候并不是重點(diǎn),沒(méi)有人會(huì)在乎短暫的停機(jī),而且很多非生產(chǎn)環(huán)境都是單機(jī)的,無(wú)論如何都無(wú)法避免發(fā)布造成的服務(wù)停止。但是到了真正的上線階段,特別是有了一定的用戶(hù)群和流量的時(shí)候,拉新客和留老客的重要度就是同等重要的了。而面向C端用戶(hù)的產(chǎn)品迭代又非??欤珻I/CD的理念下,到了真正的發(fā)布的時(shí)候如果不能做到優(yōu)雅發(fā)布即通常也稱(chēng)為不停機(jī)發(fā)布(Zero-Downtime-Deployment),用戶(hù)的流失的鍋就會(huì)由研發(fā)團(tuán)隊(duì)來(lái)背,進(jìn)而轉(zhuǎn)嫁給由運(yùn)維來(lái)背(想象一下用戶(hù)注冊(cè)環(huán)節(jié)恰巧在流水線發(fā)布,一批用戶(hù)注冊(cè)失敗了)。而由于概念的不清晰,研發(fā)團(tuán)隊(duì)經(jīng)常被挑戰(zhàn)的就是:不都已經(jīng)敏捷研發(fā)了,流水線部署了嘛,怎么還不能隨時(shí)上線?
傳統(tǒng)應(yīng)用如何優(yōu)雅發(fā)布
開(kāi)始主題之前讓我們先看看傳統(tǒng)意義上的“不停機(jī)發(fā)布”是如何進(jìn)行的,首先看下圖,這是一個(gè)最簡(jiǎn)單的典型的由負(fù)載均衡來(lái)代理,假設(shè)場(chǎng)景是我們要發(fā)布App2,從圖中我們可以看到App2有3個(gè)節(jié)點(diǎn),一般我們選擇在非高峰時(shí)間段,2B應(yīng)用通常在晚上就可以進(jìn)行發(fā)布,2C應(yīng)用可能會(huì)選擇在凌晨2點(diǎn)左右。發(fā)布的步驟就是通過(guò)Pipeline逐步的對(duì)192.168.10.21 ~ 23進(jìn)行逐個(gè)發(fā)布,這樣的發(fā)布看似總有兩個(gè)節(jié)點(diǎn)在提供服務(wù),用戶(hù)是沒(méi)有感知的,但是細(xì)究之下有很多推敲之處:一般負(fù)載均衡的心跳檢測(cè)都有一定的時(shí)間,從60S到180S不等,所以在進(jìn)行具體一個(gè)節(jié)點(diǎn)發(fā)布的時(shí)候,由于心跳檢測(cè)還沒(méi)有來(lái)的及檢測(cè)出來(lái)當(dāng)前的節(jié)點(diǎn)已經(jīng)不能提供服務(wù)了,而這種情況實(shí)際有兩種細(xì)分場(chǎng)景:一個(gè)場(chǎng)景實(shí)際就是應(yīng)用服務(wù)端口已經(jīng)不提供服務(wù)了(tomcat的8080端口響應(yīng)超時(shí)),所以一部分請(qǐng)求就被轉(zhuǎn)發(fā)到當(dāng)前節(jié)點(diǎn),但是應(yīng)用服務(wù)器不能響應(yīng),一般也就是超時(shí)響應(yīng),此時(shí)的表現(xiàn)就是用戶(hù)在白屏等待直至超時(shí)。另外一個(gè)場(chǎng)景是盡管應(yīng)用服務(wù)端口已經(jīng)啟動(dòng),但是啟動(dòng)應(yīng)用是需要一定的時(shí)間的,在初始化過(guò)程中被轉(zhuǎn)發(fā)過(guò)來(lái)的請(qǐng)求就沒(méi)有辦法響應(yīng)了,而此時(shí)端口實(shí)際是work的,但是不能處理正常的業(yè)務(wù),這種情況下用戶(hù)得到的是立即響應(yīng)的服務(wù)器錯(cuò)500錯(cuò)誤,如果沒(méi)有針對(duì)性的錯(cuò)誤處理,這是一般就會(huì)報(bào)出來(lái)tomcat的500錯(cuò)誤。因此這樣的發(fā)布實(shí)際上有很長(zhǎng)的一段錯(cuò)誤發(fā)生率,是完全無(wú)法達(dá)到優(yōu)雅發(fā)布的標(biāo)準(zhǔn)的。

那么這種情況下,我們?cè)撊绾伪苊獍l(fā)布過(guò)程中的服務(wù)超時(shí)和服務(wù)器內(nèi)部錯(cuò)誤500的情況發(fā)生呢,簡(jiǎn)單一點(diǎn)的做法就是結(jié)合負(fù)載均衡的配置,但是這樣的做法對(duì)運(yùn)維有一定要求,而且要熟悉所有服務(wù)的upstream配置,下面讓我們先看看如何操作。
1、臨時(shí)修改負(fù)載均衡的配置,將要發(fā)布的節(jié)點(diǎn)從upstream中摘除
upstream app2_backends {
? ? ? ?
server 192.168.10.21:8080;? ? ? ? server 192.168.10.22:8080;
? ? ? ? server 192.168.10.23:8080;
}
2、重啟Nginx
nginx -s reload
3、觀察一下192.168.10.21的日志,看看是不是有新的流量打入(這里暫時(shí)不考慮定時(shí)任務(wù)等其他因素)
4、在沒(méi)有新流量打入,且原有流量處理完成的前提下進(jìn)行正常發(fā)布
5、確認(rèn)192.168.10.21的服務(wù)正常啟動(dòng)后(日志或者/status都可以)
6、修改負(fù)載均衡的配置,將192.168.10.21上線,192.168.10.22從upstream中摘除
upstream app2_backends {? ?
? ? server 192.168.10.21:8080;? ? ? ?
? ?
server 192.168.10.22:8080;? ? ? ?? ? server 192.168.10.23:8080;
}
7、重啟Nginx
重復(fù)上述步驟直至將所有節(jié)點(diǎn)發(fā)布完畢,但是這樣這樣的發(fā)布實(shí)在太多繁瑣,而且中間涉及大量的負(fù)載均衡的配置修改、反復(fù)重啟、app應(yīng)用日志的確認(rèn)等,很難將發(fā)布下放至一線運(yùn)維人員,所以很難推行。那么有沒(méi)有更簡(jiǎn)單的操作呢?當(dāng)然是有的,我們來(lái)回顧一下上述發(fā)布過(guò)程,中間涉及反復(fù)的配置修改和重啟命令,都是重復(fù)性勞動(dòng),而且容易出錯(cuò),主要就是每次修改了配置必須要重啟Nginx,如果加載后不用重啟了是不是就簡(jiǎn)化了很多步驟?嗯,所以動(dòng)態(tài)加載就是答案,實(shí)現(xiàn)動(dòng)態(tài)加載有如下兩種方式:
基于OpenResty的lua-upstream-nginx-module(https://github.com/openresty/lua-upstream-nginx-module) ,提供了細(xì)粒度的upstream管理方式,可以對(duì)某一個(gè)服務(wù)IP進(jìn)行管理,其中提供的set_peer_down方法,可以對(duì)upstream中的某個(gè)ip進(jìn)行上下線。
因?yàn)閚ginx-1.9.0以上支持zone的概念,而ngx_dynamic_upstream是需要zone的,所以可以使用ngx_dynamic_upstream(https://github.com/cubicdaiya/ngx_dynamic_upstream)來(lái)進(jìn)行upstream中的某個(gè)ip進(jìn)行上下線
我們以ngx_dynamic_stream為例來(lái)看如何簡(jiǎn)化操作
1、修改conf配置,這里注意看粗體的/dynamic部分,后面會(huì)用到
upstream app2_backends {
? ? zone zone_for_app2_backends 1m;
? ? server 192.168.10.21:8080;? ? ? ? ? ?
? ? server 192.168.10.22:8080;? ? ? ? ?
? ? server 192.168.10.23:8080;
}
server {
? ? ? ? listen 80;
? ? ? ? location /dynamic {
? ? ? ? allow 127.0.0.1;
? ? ? ? ? ? deny all;
? ? ? ? ? ? ? ? dynamic_upstream;
? ? ? ? }
? ? ? ? location / {
? ? ? ? ? ? proxy_pass http://app2_backends;
? ? ? ? }
}
2、下線192.168.10.21節(jié)點(diǎn)
$ curl "http://127.0.0.1/dynamic?upstream=zone_for_app2_backends&server=192.168.10.21:8080&down="
server 192.168.10.218080? down;
server 192.168.10.228080 ;
server 192.168.10.23:8080 ;
$
3、觀察一下192.168.10.21的日志,看看是不是有新的流量打入
4、在沒(méi)有新流量打入,且原有流量處理完成的前提下進(jìn)行正常發(fā)布192.168.10.21節(jié)點(diǎn)
5、確認(rèn)192.168.10.21的服務(wù)正常啟動(dòng)后(日志或者/status都可以)
6、上線192.168.10.21節(jié)點(diǎn)
$ curl "http://127.0.0.1/dynamic?upstream= zone_for_app2_backends &server= 192.168.10.21 :8080&up="
server 192.168.10.21 8080? ;
server 192.168.10.22 8080 ;
server 192.168.10.23 :8080 ;
$
7、重復(fù)2-6步驟發(fā)布另外兩個(gè)節(jié)點(diǎn)
到這里為止,大家可以看到從概念上的“不停機(jī)發(fā)布”到真正的不停機(jī)發(fā)布,我們做了一個(gè)深入的實(shí)際的可參考案例,那么夠不夠優(yōu)雅呢?這個(gè)我們暫時(shí)不表,后面我們?cè)龠M(jìn)一步闡述。我們接下來(lái)看一下面向微服務(wù)的優(yōu)雅發(fā)布如何操作,又有哪些細(xì)節(jié)等我們來(lái)探究?
微服務(wù)應(yīng)用如何優(yōu)雅發(fā)布
進(jìn)入這一章節(jié),第一印象是啥?都微服務(wù)集群了,天然都支持不停機(jī)發(fā)布了!這是大量的解決方案提供商跟我說(shuō)解決方案的時(shí)候的一個(gè)答案,好像這個(gè)問(wèn)題就不應(yīng)該問(wèn),潛臺(tái)詞是,你懂不懂微服務(wù)?實(shí)際真的是這樣么?
首先要聲明的就是在微服務(wù)應(yīng)用中負(fù)載均衡更加復(fù)雜,既涉及到客戶(hù)端又涉及到服務(wù)端,在上述章節(jié)傳統(tǒng)單體應(yīng)用還主要以服務(wù)端的負(fù)載均衡為主,因此并沒(méi)有對(duì)客戶(hù)端負(fù)載均衡進(jìn)行展開(kāi),而本章節(jié)我們就不贅述服務(wù)端的負(fù)載均衡,微服務(wù)的兩大陣營(yíng)Dubbo和Spring Cloud我們不會(huì)全部展開(kāi),今天僅以Spring Cloud為例進(jìn)行簡(jiǎn)單剖析。
我們知道通過(guò)Ribbon配置服務(wù)提供者地址后,Ribbon就可以基于某種負(fù)載均衡算法,自動(dòng)幫助服務(wù)消費(fèi)者去請(qǐng)求。Ribbon默認(rèn)為我們提供了很多負(fù)載均衡算法,例如輪詢(xún)、隨機(jī)等。當(dāng)然,我們也可為Ribbon實(shí)現(xiàn)自定義的負(fù)載均衡算法。在Spring Cloud中,當(dāng)Ribbon與Eureka配合使用時(shí),Ribbon可自動(dòng)從Eureka Server獲取服務(wù)提供者地址列表,并基于負(fù)載均衡算法,請(qǐng)求其中一個(gè)服務(wù)提供者實(shí)例。下圖展示了Ribbon與注冊(cè)中心(Eureka/Nacos均可)、微服務(wù)網(wǎng)關(guān)(Zuul/Spring Cloud Gateway)的負(fù)載均衡關(guān)系。稍加解釋一下:
S1線是從入口負(fù)載均衡到達(dá)微服務(wù)網(wǎng)關(guān)實(shí)例1(服務(wù)消費(fèi)者),然后通過(guò)網(wǎng)關(guān)的Ribbon負(fù)載到服務(wù)2(服務(wù)提供者/服務(wù)消費(fèi)者)實(shí)例1,再負(fù)載到服務(wù)1(服務(wù)提供者)實(shí)例2
S2線是從入口負(fù)載均衡到達(dá)服務(wù)網(wǎng)關(guān)實(shí)例3(服務(wù)消費(fèi)者),然后通過(guò)網(wǎng)關(guān)的Ribbon負(fù)載到服務(wù)1(服務(wù)提供者)實(shí)例3

那么是不是通過(guò)這樣的一個(gè)架構(gòu)就可以說(shuō)是能提供不停機(jī)發(fā)布了呢?答案是否定的。這個(gè)原理同上一章節(jié)中是一致的,就是說(shuō)負(fù)載均衡算法都是有心跳時(shí)間的,換句話說(shuō),都是通過(guò)被動(dòng)的檢測(cè)服務(wù)提供方的狀態(tài)來(lái)決定是否剔除不健康的節(jié)點(diǎn),因此雖然在微服務(wù)中都提供了健康檢查的接口,因?yàn)槭潜粍?dòng)的,所以總會(huì)有失效狀態(tài)和失效時(shí)間。因?yàn)槲⒎?wù)的負(fù)載均衡既要考慮到服務(wù)端的負(fù)載又要考慮到客戶(hù)端的負(fù)載,實(shí)際上,更需要提前摘除不健康節(jié)點(diǎn)。這里我們可以看下阿里云提供的EDAS服務(wù)是如何做的,具體參考無(wú)損下線 Spring Cloud 應(yīng)用。那么我這里就簡(jiǎn)單通過(guò)一張圖來(lái)解釋一下,EDAS如何做的,如果沒(méi)有EDAS我們又如何做?

請(qǐng)大家注意這張圖與上圖的變化:
1、運(yùn)維發(fā)布的時(shí)候主動(dòng)注銷(xiāo)某一服務(wù)的實(shí)例(服務(wù)1的實(shí)例1)
2、確認(rèn)待發(fā)布的節(jié)點(diǎn)(服務(wù)1的實(shí)例1)下線之后進(jìn)行真正的發(fā)布
3、此時(shí)同樣是上述的S1和S2兩個(gè)請(qǐng)求,從注冊(cè)中心中就無(wú)法查到待發(fā)布節(jié)點(diǎn)(服務(wù)1的實(shí)例1),將僅負(fù)載到剩余的兩個(gè)實(shí)例上
運(yùn)行在EDAS上的微服務(wù),通過(guò)主動(dòng)監(jiān)聽(tīng)事件,捕獲到進(jìn)程注銷(xiāo)事件后,增加了一個(gè)prestop階段,向注冊(cè)中心推送注銷(xiāo)服務(wù)實(shí)例的消息,主動(dòng)下線,確認(rèn)服務(wù)節(jié)點(diǎn)摘除之后再進(jìn)行真正的注銷(xiāo)。
那么如果我們不跑在EDAS上要如何進(jìn)行做呢?從原理上來(lái)講,我們手工就可以做到,只是比較繁瑣,下面由我來(lái)以Eureka為例進(jìn)行拆解一下,大致做法如下:通過(guò)/eurekaUnregister或者/offline來(lái)通知注冊(cè)中心下線,多解釋一點(diǎn),收到下線通知之后并不會(huì)馬上通知所有的注冊(cè)客戶(hù)端,而是等到下一次心跳的時(shí)候再通知,這樣處理的好處:首先不會(huì)增加注冊(cè)中心的消息處理邏輯,其次是所有的服務(wù)調(diào)用都是在同一心跳的時(shí)候被通知不會(huì)因?yàn)榱⒓赐ㄖ戮€而產(chǎn)生服務(wù)終端從而產(chǎn)生失敗。
補(bǔ)充一點(diǎn),有一些操作是可以通過(guò)DELETE操作/eureka/apps/{application.name}/{instance.id}來(lái)主動(dòng)從Eureka摘除節(jié)點(diǎn),但是這樣的做法無(wú)法從根本上將該節(jié)點(diǎn)摘除,下一次心跳續(xù)約后,Eureka因?yàn)闄z測(cè)到該節(jié)點(diǎn)處于online狀態(tài),會(huì)繼續(xù)把該節(jié)點(diǎn)加入Eureka注冊(cè)中心。

如果不通過(guò)上述兩種方式進(jìn)行下線,還有一種更簡(jiǎn)單的,直接調(diào)用Eureka的Restful API即可。
PUT /eureka/apps {application.name}/{instance.id} /status?value=OUT_OF_SERVICE
總結(jié)
本文通過(guò)簡(jiǎn)單的兩個(gè)場(chǎng)景來(lái)闡述優(yōu)雅發(fā)布在傳統(tǒng)應(yīng)用和微服務(wù)集群中如何做,從而真正減少由于版本更新迭代而帶來(lái)的服務(wù)不可用時(shí)間,尤其是隨著微服務(wù)集群的規(guī)模越來(lái)越大,如果不能提供這種不宕機(jī)發(fā)布服務(wù),是很難協(xié)調(diào)出來(lái)一個(gè)窗口來(lái)進(jìn)行更快的業(yè)務(wù)迭代,所以我們可以看到像BATJ這類(lèi)型的業(yè)務(wù)你很難見(jiàn)到有停止服務(wù)的運(yùn)營(yíng)公告,但是在企業(yè)內(nèi)部,甚至一些偽互聯(lián)網(wǎng)公司的服務(wù),提供的都是縮水的發(fā)布服務(wù),而在用戶(hù)和流量成本越來(lái)越高的時(shí)代,顯然這是不可接受的。