手把手教你搭建一個(gè)灰度發(fā)布環(huán)境

https://segmentfault.com/a/1190000022612488
引言
灰度發(fā)布,又稱金絲雀發(fā)布。
金絲雀發(fā)布這一術(shù)語(yǔ)源于煤礦工人把籠養(yǎng)的金絲雀帶入礦井的傳統(tǒng)。礦工通過(guò)金絲雀來(lái)了解礦井中一氧化碳的濃度,如果一氧化碳的濃度過(guò)高,金絲雀就會(huì)中毒,從而使礦工知道應(yīng)該立刻撤離。——《DevOps實(shí)踐指南》
對(duì)應(yīng)到軟件開中,則是指在發(fā)布新的產(chǎn)品特性時(shí)通過(guò)少量的用戶試點(diǎn)確認(rèn)新特性沒(méi)有問(wèn)題,確保無(wú)誤后推廣到更大的用戶使用群體。
集成灰度發(fā)布的流水線在DevOps中是一個(gè)非常重要的工具和高效的實(shí)踐,然而筆者在入職以前對(duì)流水線和灰度發(fā)布知之甚少。在了解一個(gè)新東西時(shí),先從邏輯上打通所有的關(guān)鍵環(huán)節(jié),然后再完成一個(gè)最簡(jiǎn)單的Demo,對(duì)于我們來(lái)說(shuō)是比較有意思的學(xué)習(xí)路徑,因此便有了這篇文章。
本文理論內(nèi)容較少,主要是從零到一的搭建流程實(shí)踐,適合對(duì)工程化感興趣的初級(jí)前端開發(fā)者。
01 服務(wù)器準(zhǔn)備
獲取服務(wù)器
上面提到,灰度發(fā)布是通過(guò)少量的用戶試點(diǎn)來(lái)驗(yàn)證新功能有沒(méi)有問(wèn)題。所以要保證有兩批用戶能在同一時(shí)間體驗(yàn)到不同的功能。這就要求我們準(zhǔn)備兩臺(tái)服務(wù)器,分別部署不同的代碼版本。
如果你已經(jīng)有了一臺(tái)服務(wù)器,也可以通過(guò)在不同端口部署服務(wù)的方式來(lái)模擬兩臺(tái)服務(wù)器。如果你還一臺(tái)服務(wù)器都沒(méi)有,那么可以參考這個(gè)過(guò)程購(gòu)買兩臺(tái)云服務(wù)器,如果是按需購(gòu)買,完成本文的Demo,大概要花費(fèi)20塊錢。
獲取云服務(wù)器教程:github.com/TerminatorS…
工具安裝
Git
首先,確保你的服務(wù)器上已經(jīng)安裝了git,如果沒(méi)有的話使用以下命令進(jìn)行安裝,安裝好了以后生成ssh 公鑰,放到你的github 里,后面拉取代碼的時(shí)候會(huì)用到。
yum install git
Nginx
如果你的服務(wù)器沒(méi)有Nginx,先按照以下操作進(jìn)行安裝,Linux 下安裝Nginx非常簡(jiǎn)單:
sudo yum install nginx
安裝完了,在終端輸入nginx -t檢查一下是否安裝成功。如果安裝成功,它會(huì)顯示Nginx 配置文件的狀態(tài),以及位置。

此時(shí)nginx還沒(méi)有啟動(dòng),在終端中輸入nginx 或nginx \-s reload 命令即可啟動(dòng),此時(shí)看到的nginx相關(guān)進(jìn)程如下,表明已經(jīng)啟動(dòng)成功。

在瀏覽器里訪問(wèn)你的服務(wù)器公網(wǎng)IP,如果能看到下面的頁(yè)面說(shuō)明Nginx 可以正常工作。

Jenkins (耗時(shí)比較久)
第一次接觸Jenkins 可能會(huì)有很多疑問(wèn),Jenkins 是什么?能完成什么事情?我為什么要使用Jenkins 等諸如此類。很難講清楚Jenkins 是什么東西,所以這里簡(jiǎn)單介紹一下Jenkins 可以做什么。簡(jiǎn)單來(lái)講,你在任何一臺(tái)服務(wù)器上進(jìn)行的任何操作命令,Jenkins 都可以幫你完成,只要你提前在Jenkins上創(chuàng)建好任務(wù),指定任務(wù)內(nèi)容和觸發(fā)時(shí)機(jī),比如定時(shí)觸發(fā)或者在特定的情況下觸發(fā)。
(1)安裝
Jenkins穩(wěn)定版本list:pkg.jenkins-ci.org/redhat-stab…
// 科學(xué)上網(wǎng)會(huì)快一些,記得留意網(wǎng)站上java和jenkins版本匹配信息,別下錯(cuò)了
wget http://pkg.jenkins-ci.org/redhat-stable/jenkins-2.204.5-1.1.noarch.rpm
rpm -ivh jenkins-2.204.5-1.1.noarch.rpm
修改Jenkins端口,不沖突可不修改
// line 56 JENKINS_PORT
vi /etc/sysconfig/jenkins
(2)啟動(dòng)
啟動(dòng)jenkins
service jenkins start/stop/restart
// 密碼位置
/var/lib/jenkins/secrets/initialAdminPassword
(3)訪問(wèn)
訪問(wèn)服務(wù)器的8080端口,輸入從上述位置獲取的密碼,點(diǎn)擊繼續(xù)

創(chuàng)建一個(gè)賬戶然后登錄

看到Jenkins 已就緒的頁(yè)面表示安裝已經(jīng)完成,服務(wù)器準(zhǔn)備工作到此結(jié)束。

02 代碼準(zhǔn)備
準(zhǔn)備兩份代碼
因?yàn)橐龌叶炔渴穑孕枰獪?zhǔn)備兩份不一樣的代碼,以驗(yàn)證我們實(shí)施的灰度操作是否生效。這里選擇使用Angular 的Angular-CLI 來(lái)創(chuàng)建代碼。創(chuàng)建的項(xiàng)目并不簡(jiǎn)潔,但是勝在操作簡(jiǎn)單。我們一次性把兩份代碼準(zhǔn)備好,簡(jiǎn)化開發(fā)側(cè)工作。
// 安裝angular-cli,前提是已經(jīng)安裝了node,如果沒(méi)有node真的要去自行百度了...
npm install -g @angular/cli
// 快速創(chuàng)建一個(gè)新項(xiàng)目,一路回車
ng new canaryDemocd canaryDemo
// 運(yùn)行完這個(gè)命令后訪問(wèn)http://localhost:4200 查看頁(yè)面信息
ng serve
訪問(wèn)localhost 的4200 端口查看頁(yè)面,然后把項(xiàng)目根目錄下src 中的index.html 的title 改成A-CanaryDemo,可以看到頁(yè)面會(huì)進(jìn)行實(shí)時(shí)地刷新。在這個(gè)例子中,我們用title 來(lái)標(biāo)識(shí)灰度發(fā)布過(guò)程中兩邊不同的服務(wù)需要部署的代碼。

接下來(lái),我們進(jìn)行兩次打包,兩次打包的title 分別為A-CanaryDemo 和 B-CanaryDemo, 把這兩個(gè)文件夾放好備用,作為一會(huì)灰度發(fā)布的新老代碼。
ng build --prod

配置Nginx
在上述完成Nginx 的安裝操作時(shí),我們?cè)L問(wèn)服務(wù)器的IP 看到的是Nginx 的頁(yè)面,現(xiàn)在我們想訪問(wèn)到自己的頁(yè)面,首先把上面打包得到的A-CanaryDemo 發(fā)送到兩臺(tái)服務(wù)器上任意位置,這里我們把它放到/var/canaryDemo。
// 將A-CanaryDemo 文件夾復(fù)制到你的公網(wǎng)服務(wù)器上,xx部分是你的服務(wù)器公網(wǎng)ip
scp -r ./dist/A-CanaryDemo [email protected]:/var/canaryDemo
去服務(wù)器上/var 的位置上看一下,是否已經(jīng)有了這個(gè)文件,如果有了的話,接著到下一步。即修改Nginx 配置把訪問(wèn)該服務(wù)器IP 的請(qǐng)求轉(zhuǎn)發(fā)到我們剛剛上傳上來(lái)的頁(yè)面上。上面提到過(guò)可以通過(guò)nginx -t 這個(gè)命令來(lái)查看Nginx 配置文件的位置,在這一步,我們要去編輯那個(gè)文件。
vi /etc/nginx/nginx.conf
修改47-50行添加下圖相關(guān)的內(nèi)容,即將訪問(wèn)到該服務(wù)器IP 的流量轉(zhuǎn)發(fā)到/var/canaryDemo 下的index.html.

修改完畢,保存退出,重啟一下nginx
nginx -s reload
這時(shí)候去訪問(wèn)我們服務(wù)器的IP 地址可以看到頁(yè)面已經(jīng)變成了剛剛我們?cè)诒镜馗牡捻?yè)面,而且title 確實(shí)是A-CanaryDemo。兩臺(tái)服務(wù)器都操作完成后,兩邊都可以訪問(wèn)到title 為A-CanaryDemo 的頁(yè)面。此時(shí)的狀態(tài)相當(dāng)于生產(chǎn)環(huán)境已經(jīng)在提供穩(wěn)定服務(wù)的兩臺(tái)機(jī)器。

03 定義灰度策略
接下來(lái),我們要開始進(jìn)行灰度發(fā)布的部分,在進(jìn)行相關(guān)操作之前,我們需要定義一個(gè)灰度策略,即滿足什么情況下的流量會(huì)走到灰度邊,而其他流量走向正常邊。這里為了簡(jiǎn)單起見,我們使用名字為canary 的cookie 來(lái)區(qū)分,如果檢測(cè)到這個(gè)cookie 的值為devui,就訪問(wèn)灰度邊機(jī)器,否則就訪問(wèn)正常邊機(jī)器。按照此規(guī)則配置Nginx 結(jié)果如下,此處分別使用11.11.11.11和22.22.22.22代表兩臺(tái)服務(wù)器的IP地址:
# Canary Deployment
map $COOKIE_canary $group {
# canary account
~*devui$ server_canary;
default server_default;
}
upstream server_canary {
# 兩臺(tái)機(jī)器的IP,第一臺(tái)設(shè)置端口號(hào)8000是為了防止nginx轉(zhuǎn)發(fā)出現(xiàn)死循環(huán)導(dǎo)致頁(yè)面報(bào)錯(cuò)
server 11.11.11.11:8000 weight=1 max_fails=1 fail_timeout=30s;
server 22.22.22.22 weight=1 max_fails=1 fail_timeout=30s;
}
upstream server_default {
server 11.11.11.11:8000 weight=2 max_fails=1 fail_timeout=30s;
server 22.22.22.22 weight=2 max_fails=1 fail_timeout=30s;
}
# 相應(yīng)地,要配置8000端口的轉(zhuǎn)發(fā)規(guī)則,8000端口默認(rèn)不開啟訪問(wèn),需要去云服務(wù)器控制臺(tái)安全組新增8000
server {
listen 8000;
server_name _;
root /var/canaryDemo;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
root /var/canaryDemo;
index index.html;
}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# root /usr/share/nginx/html;
root /var/canaryDemo;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://$group;
# root /var/canaryDemo;
# index index.html;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.h
}
此時(shí),灰度流量和正常流量都會(huì)隨機(jī)分配到AB兩邊的機(jī)器。下面,我們通過(guò)建立Jenkins 任務(wù)執(zhí)行Nginx 文件修改的方式實(shí)現(xiàn)灰度發(fā)布。
04 實(shí)現(xiàn)灰度發(fā)布
流程梳理
在創(chuàng)建用于實(shí)現(xiàn)灰度發(fā)布的Jenkins任務(wù)之前我們先梳理一下要達(dá)到灰度發(fā)布的目標(biāo)需要哪幾個(gè)任務(wù),以及每個(gè)任務(wù)負(fù)責(zé)完成什么事情。灰度發(fā)布一般遵循這樣的流程(假設(shè)我們有AB兩臺(tái)服務(wù)器用于提供生產(chǎn)環(huán)境的服務(wù),我們稱之為AB邊):
(1)新代碼部署到A邊
(2)符合灰度策略的小部分流量切到A邊,剩余大部分流量仍去往B邊
(3)手動(dòng)驗(yàn)證A邊功能是否正常可用
(4)驗(yàn)證無(wú)誤后,大部分流量轉(zhuǎn)到A邊,灰度流量去往B邊
(5)手動(dòng)驗(yàn)證B邊功能是否正常可用
(6)驗(yàn)證無(wú)誤后,流量像往常一樣均分到AB邊
任務(wù)拆解
通過(guò)上述的拆解,我們得出灰度發(fā)布的6個(gè)步驟,其中(3)和(5)是需要手動(dòng)驗(yàn)證的環(huán)節(jié),所以我們以這兩個(gè)任務(wù)為分割點(diǎn),建立三個(gè)Jenkins 任務(wù)(Jenkins 任務(wù)建立在A 邊機(jī)器上)如下:
(1)Canary_A(灰度測(cè)試A),這個(gè)任務(wù)又包含兩個(gè)部分,更新A邊的代碼,然后修改流量分發(fā)策略使得灰度流量到達(dá)A,其他流量到達(dá)B
(2)Canary_AB(上線A灰度測(cè)試B),更新B邊代碼,灰度流量達(dá)到B,其他流量到達(dá)A
(3)Canary_B(上線B),所有流量均分到AB

創(chuàng)建任務(wù)
先按照任務(wù)拆解部分的設(shè)定創(chuàng)建三個(gè)FreeStyle 類型的Jenkins 任務(wù),記得使用英文名字,中文名字后面建文件夾比較麻煩。任務(wù)詳情信息可以不填,直接保存就好,下一步我們?cè)賮?lái)配置每個(gè)任務(wù)的具體信息。

配置任務(wù)
現(xiàn)在已經(jīng)創(chuàng)建好了三個(gè)任務(wù),先點(diǎn)擊進(jìn)入每一個(gè)任務(wù)進(jìn)行一次空的構(gòu)建(否則后面可能導(dǎo)致修改后的構(gòu)建任務(wù)無(wú)法啟動(dòng)),然后我們來(lái)對(duì)每個(gè)任務(wù)進(jìn)行詳細(xì)的配置。

現(xiàn)代前端項(xiàng)目都要進(jìn)行構(gòu)建打包這一步。但是廉價(jià)的云服務(wù)器在完成構(gòu)建方面有些力不從心,CPU 經(jīng)常爆表。所以我們?cè)谶@里把打包出得出的生產(chǎn)包納入git 管理,每次的代碼更新會(huì)同步最新的生產(chǎn)包到github,因此Jenkins 任務(wù)把生產(chǎn)包拉下來(lái),放在指定位置即可完成一次新代碼的部署。
這一步操作,其實(shí)我們?cè)谥熬鸵呀?jīng)完成了,我們?cè)谏厦娲蛄藘煞輙ilte 不一樣的生產(chǎn)包,此時(shí)可以派上用場(chǎng)了。
首先來(lái)配置灰度測(cè)試A,這個(gè)任務(wù)內(nèi)容上面也基本講清楚了,首先要關(guān)聯(lián)該任務(wù)到遠(yuǎn)程的github 倉(cāng)庫(kù)(需要手動(dòng)創(chuàng)建一個(gè),存放上面打包的B-CanaryDemo,并命名為dist)讓它知道可以去哪里拉取最新代碼。

執(zhí)行一次構(gòu)建任務(wù)(在git fetch 那一步耗時(shí)不穩(wěn)定,有時(shí)比較久),然后點(diǎn)擊本次構(gòu)建進(jìn)去查看Console Output,可以確定執(zhí)行Jenkins 任務(wù)的位置是位于服務(wù)器上的/var/lib/jenkins/workspace/Canary_A


繼續(xù)編輯灰度測(cè)試A 任務(wù),添加build shell,也就是每次任務(wù)執(zhí)行時(shí)要執(zhí)行的命令:
(1)先拉取最新的代碼
(2)把代碼根目錄下的dist目錄復(fù)制到部署代碼的位置,這里我們指定的位置是/var/canaryDemo
(3)修改Nginx 配置使灰度流量到達(dá)A邊
就步驟(3)而言,修改灰度流量的方式其實(shí)就是選擇性注釋Nginx 配置文件中的內(nèi)容,注釋方式如下即可實(shí)現(xiàn)灰度測(cè)試A。
upstream server_canary {
# 灰度流量訪問(wèn)A 邊
server 11.11.11.11:8080 weight=1 max_fails=1 fail_timeout=30s;
# server 22.22.22.22 weight=1 max_fails=1 fail_timeout=30s;
}
upstream server_default {
# 正常流量訪問(wèn)B 邊,為了在修改文件的時(shí)候把這段的配置和上面的server_canary 區(qū)分開,我們把這里的weight 設(shè)為2
# server 11.11.11.11:8080 weight=2 max_fails=1 fail_timeout=30s;
server 22.22.22.22 weight=2 max_fails=1 fail_timeout=30s;
}
這一步填寫的shell 命令在使用jenkins 用戶執(zhí)行時(shí)可能會(huì)遇到權(quán)限問(wèn)題,可以先用root 用戶登錄,把/var 目錄的歸屬改為jenkins 用戶,/etc/nginx/ngix.conf也需要新增可寫權(quán)限。由此,最終得到的shell 命令如下:
git pull
rm -rf /var/canaryDemo
scp -r dist /var/canaryDemo
sed -i 's/server 22.22.22.22 weight=1/# server 22.22.22.22 weight=1/' /etc/nginx/nginx.conf
sed -i 's/server 11.11.11.11:8000 weight=2/# server 11.11.11.11:8000 weight=2/' /etc/nginx/nginx.conf
nginx -s reload

灰度測(cè)試A 任務(wù)內(nèi)容配置完成,接下來(lái)依次配置上線A 灰度測(cè)試B 和上線B。
灰度測(cè)試B 的要執(zhí)行的任務(wù)是把最新的代碼拉到A 邊(因?yàn)槲覀兊腏enkins 任務(wù)都是建立在A 邊的),復(fù)制dist 下的代碼到B 邊Nginx 指定訪問(wèn)位置,然后修改A 邊Nginx 配置,使灰度流量到達(dá)B 邊。
git pull
rm -rf canaryDemo
mv dist canaryDemo
scp -r canaryDemo [email protected]:/var
sed -i 's/# server 22.22.22.22 weight=1/server 22.22.22.22 weight=1/' /etc/nginx/nginx.conf
sed -i 's/# server 11.11.11.11:8000 weight=2/server 11.11.11.11:8000 weight=2/' /etc/nginx/nginx.conf
sed -i 's/server 22.22.22.22 weight=2/# server 22.22.22.22 weight=2/' /etc/nginx/nginx.conf
sed -i 's/server 11.11.11.11:8000 weight=1/# server 11.11.11.11:8000 weight=1/' /etc/nginx/nginx.conf
nginx -s reload
這一步的任務(wù)內(nèi)容涉及到從A 邊服務(wù)器向B 邊服務(wù)器發(fā)送代碼,這個(gè)過(guò)程一般來(lái)說(shuō)需要輸入B 邊服務(wù)器的密碼。我們想要做到免密發(fā)送,因此要通過(guò)把A 邊機(jī)器~/.ssh/id_rsa.pub 中的內(nèi)容添加到B 邊服務(wù)器~/.ssh/authorized_keys 中使得A 獲得免密像B 發(fā)送文件的權(quán)限。
上線B 則是通過(guò)取消對(duì)A 邊Nginx 配置的注釋使所有流量均分到AB 邊.
sed -i 's/# server 22.22.22.22 weight=2/server 22.22.22.22 weight=2/' /etc/nginx/nginx.conf
sed -i 's/# server 11.11.11.11:8000 weight=1/server 11.11.11.11:8000 weight=1/' /etc/nginx/nginx.conf
nginx -s reload
至此,我們就從零到一搭建了一個(gè)灰度發(fā)布環(huán)境。在代碼更新后,通過(guò)手動(dòng)執(zhí)行Jenkins 任務(wù)的方式實(shí)現(xiàn)灰度部署和手工測(cè)試,保證新功能平滑上線。
總結(jié)
本文從服務(wù)器準(zhǔn)備、代碼準(zhǔn)備、灰度策略制定和實(shí)現(xiàn)灰度發(fā)布四個(gè)方面介紹了從零搭建一個(gè)灰度發(fā)布環(huán)境的必備流程。灰度發(fā)布的核心其實(shí)就是通過(guò)對(duì)Nginx 文件的修改實(shí)現(xiàn)流量的定向分發(fā)。內(nèi)容頗為簡(jiǎn)單,但是從零到一的整個(gè)流程操作下來(lái)還是比較繁瑣,希望各位看官能夠有所收獲。
