哦,原來(lái)大廠(chǎng)是這樣發(fā)布應(yīng)用的!
大家好,我是魚(yú)皮,今天我們來(lái)討論一個(gè)話(huà)題:一個(gè)項(xiàng)目上下線(xiàn)的正確姿勢(shì),由【碼?!坷蠋熤髦v??赡軙?huì)出現(xiàn)很多 “新鮮” 的名詞,大家慢慢消化~
稍微正規(guī)一點(diǎn)的公司都會(huì)有自動(dòng)化上下線(xiàn)流程,因?yàn)樯舷戮€(xiàn)看起來(lái)簡(jiǎn)單,只有兩步,「停掉應(yīng)用」,「重啟應(yīng)用」,但里面其實(shí)還是有挺多門(mén)道的,比如:
1. 如何優(yōu)雅地上下線(xiàn), 涉及到 dubbo 的優(yōu)雅停機(jī),服務(wù)上線(xiàn)時(shí)的 JVM 參數(shù)配置等
2. 如何保證應(yīng)用發(fā)布上線(xiàn)發(fā)現(xiàn)問(wèn)題后快速回滾,或者上線(xiàn)后將新功能可能帶來(lái)的影響降至最小
所以這套流程必須自動(dòng)化,以下我們就以 SpringBoot 工程部署為例來(lái)對(duì)優(yōu)雅的發(fā)布流程一探究竟
先來(lái)看第一個(gè)問(wèn)題:
如何優(yōu)雅上下線(xiàn)
這里面涉及到兩個(gè)方面
如何優(yōu)雅的下線(xiàn)線(xiàn)上正在運(yùn)行的服務(wù)
如何優(yōu)雅上線(xiàn)將要發(fā)布的服務(wù)
先來(lái)看第一個(gè)問(wèn)題
如何優(yōu)雅停機(jī)
目前業(yè)界在微服務(wù)架構(gòu)上大多使用了 dubbo ,我們的工程也不例外。
我們知道 consumer 是通過(guò) registry 來(lái)感知到 provider 存在的,現(xiàn)在要將 provider 下線(xiàn)自然也要通過(guò) registry 來(lái)通知 consumer

注冊(cè)中心一般為 ZK 或者 nacos,所以第一步,我們首先要讓服務(wù)提供者調(diào)用 unregister 以刪除其在注冊(cè)中心上的臨時(shí)節(jié)點(diǎn),這樣消費(fèi)者由于一直監(jiān)聽(tīng)此臨時(shí)節(jié)點(diǎn),所以能感知到,于是消費(fèi)者就不會(huì)再向此即將下線(xiàn)的提供者發(fā)起調(diào)用,但此時(shí)就能立刻停掉提供者的進(jìn)程了嗎?不能,因?yàn)橛锌赡芴峁┱哌€在執(zhí)行消費(fèi)者發(fā)起的請(qǐng)求,如果此時(shí)停掉很可能中斷正在執(zhí)行的請(qǐng)求導(dǎo)致報(bào)錯(cuò),所以我們還需要等待 10 s (一般 10s 足以處理完相關(guān)的請(qǐng)求)左右讓服務(wù)提供者把任務(wù)處理完成,然后就可以停掉當(dāng)前服務(wù)了
可能有人會(huì)說(shuō)你這不是說(shuō)的 dubbo 的優(yōu)雅停機(jī)嗎,發(fā)起 kill 后讓 dubbo 優(yōu)雅停機(jī)停機(jī)即可,何必多此一舉呢?
沒(méi)錯(cuò),理論上直接 kill 確實(shí)可以依賴(lài) dubbo 的優(yōu)雅停機(jī)做一些收尾的工作,但實(shí)際上在我們的下線(xiàn)腳本中,依然是手動(dòng)執(zhí)行了 unregister 并指定了 sleep 10s 然后才 kill 服務(wù)進(jìn)程,為什么呢,因?yàn)樵?dubbo 的 2.5.x 和 2.6.x 版本中優(yōu)雅停機(jī)其實(shí)存在一定的瑕疵及需要滿(mǎn)足一定的使用條件,在 2.7 版本中才算相對(duì)完美地實(shí)現(xiàn)了優(yōu)雅停機(jī)方案,而我們很長(zhǎng)一段時(shí)間使用的都是 2.6.x 的版本,所以我們相當(dāng)于前置了 dubbo 優(yōu)雅停機(jī)的一些工作以讓下線(xiàn)更可靠一些
在執(zhí)行完 unregister 并 sleep 10s 后此時(shí)我們就可以「kill 服務(wù)進(jìn)程id」了,注意是 kill(-15) 而不是 kill -9,使用 kill -9 會(huì)立刻殺死 JVM 進(jìn)程,但實(shí)際上在關(guān)閉 JVM 進(jìn)程前,需要清理文件,網(wǎng)絡(luò)套接字等資源,所以使用 kill 會(huì)更合適,這樣的話(huà) JVM 就能接收到 kill 信號(hào),通過(guò) shutdown hook 就能做些系統(tǒng)資源清理方面的工作(dubbo 的優(yōu)雅停機(jī)也是依賴(lài)這個(gè)原理)了,如下

當(dāng)執(zhí)行 kill 后,我們每隔一秒檢查一下 JVM 進(jìn)程是否還存活,共檢測(cè) 10 次,一共 10s
畫(huà)外音:檢測(cè) 10s 足夠了,加上之前的 10s,總共有 20s 的時(shí)候讓服務(wù)提供者來(lái)執(zhí)行任務(wù)和清理資源,另外 dubbo 的優(yōu)雅停機(jī)默認(rèn)超時(shí)時(shí)間為 10s,超時(shí)則會(huì)強(qiáng)制關(guān)閉,所以 20s 理論上足夠了
10s 后如果發(fā)現(xiàn)進(jìn)程還在,那此時(shí)就要祭出大招 kill -9 直接殺死進(jìn)程了,這種情況下直接殺死進(jìn)程是沒(méi)有問(wèn)題的,因?yàn)榇藭r(shí)服務(wù)已經(jīng)被摘除 20s 了,基本上可以認(rèn)為不會(huì)影響線(xiàn)上運(yùn)行。
簡(jiǎn)單總結(jié)一下,服務(wù)下線(xiàn)流程如下

經(jīng)過(guò)以上步驟我們才算做到了優(yōu)雅停機(jī),接下來(lái)我們?cè)倏纯慈绾蝺?yōu)雅地上線(xiàn)
正確的上線(xiàn)姿勢(shì)
可能有人會(huì)說(shuō)上線(xiàn)還不簡(jiǎn)單,通過(guò)以下形式來(lái)啟動(dòng) SpringBoot 不就完了
java?-jar?jar包路徑?--spring.config.location=xxx?--spring.pid.file=xxx
如果在業(yè)務(wù)剛起步階段這樣設(shè)置確實(shí)沒(méi)有問(wèn)題,畢竟本身也沒(méi)啥業(yè)務(wù)量,但如果你們的業(yè)務(wù)量上升到一定規(guī)模后這樣簡(jiǎn)單的啟動(dòng)是不行的,不然動(dòng)不動(dòng)就 YGC/FullGC 或者 ?OOM 了沒(méi)有日志咋辦,所以你需要根據(jù)你的業(yè)務(wù)量來(lái)壓測(cè),設(shè)置類(lèi)似如下的 JVM 的參數(shù)
-server?-Xmx5g?-Xms5g?-Xmn2g?-XX:MetaspaceSize=256m?-XX:MaxMetaspaceSize=512m?-Xss256k?-XX:SurvivorRatio=8?\
-XX:+UseParNewGC??-XX:+UseConcMarkSweepGC?-XX:CMSInitiatingOccupancyFraction=70?\
-XX:+CMSParallelRemarkEnabled?-XX:+UseCMSCompactAtFullCollection?-XX:+UseFastAccessorMethods?-XX:+UseCMSInitiatingOccupancyOnly?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=${APPLICATION_LOG_DIR}?-Djava.security.egd=file:/dev/./urandom
注意上面的參數(shù)中有個(gè)?-Djava.security.egd=file:/dev/./urandom?,這個(gè)是干啥用的呢?
在工程中很可能會(huì)有使用隨機(jī)數(shù)的場(chǎng)景,SecureRandom 在 java 各種組件中使用廣泛,可以可靠的產(chǎn)生隨機(jī)數(shù),但大量產(chǎn)生隨機(jī)數(shù)據(jù)的時(shí)候,性能會(huì)降低,所以我們上面這個(gè)設(shè)置將會(huì)加快隨機(jī)數(shù)的產(chǎn)生過(guò)程。
光設(shè)置 JVM 參數(shù)還不夠,你總要監(jiān)控你的應(yīng)用是否健康吧,這需要采集你機(jī)器的相關(guān)指標(biāo)以便以可視化的形式展現(xiàn)出來(lái),如下

所以你需要安裝探針,我們用的 Skywalking,這樣就需要以 Java agent 的形式來(lái)啟動(dòng),如下
-javaagent:$SW_AGENT_JAR?-Dskywalking_config=$SW_AGENT_CONFIG
綜上,Java 的啟動(dòng)參數(shù)配置如下
java?-jar?jar包路徑?--spring.config.location=xxx?--spring.pid.file=xxx?-server?-Xmx5g?-Xms5g?-Xmn2g?-XX:MetaspaceSize=256m?-XX:MaxMetaspaceSize=512m?-Xss256k?-XX:SurvivorRatio=8?\
-XX:+UseParNewGC??-XX:+UseConcMarkSweepGC?-XX:CMSInitiatingOccupancyFraction=70?\
-XX:+CMSParallelRemarkEnabled?-XX:+UseCMSCompactAtFullCollection?-XX:+UseFastAccessorMethods?-XX:+UseCMSInitiatingOccupancyOnly?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=${APPLICATION_LOG_DIR}?-Djava.security.egd=file:/dev/./urandom?-javaagent:$SW_AGENT_JAR?-Dskywalking_config=$SW_AGENT_CONFIG
配置好之后終于可以啟動(dòng)了,項(xiàng)目大了后啟動(dòng)一般比較慢,所以我們可以等個(gè) 20s 然后再檢測(cè)是否啟動(dòng)成功,如果不成功通過(guò)告警的形式提醒開(kāi)發(fā)人員,但如果成功了的話(huà)你就可以放心上線(xiàn)了?非也,你還需要做健康檢查,檢測(cè) db 連接,redis 等是否真正可用,一般情況下, 項(xiàng)目都會(huì)有如下健康檢查邏輯
@Service(protocol?=?{"rest"})
public?class?HealthCheckServiceImpl?implements?HealthCheckService?{
????@Resource
????private?TestDAO?TestDAO;
????@Override
????public?String?getHealthStatus()?{
????????????List?testDOS?=
????????????????????TestDAO.getResult(123);
????????????Assert.isTrue(testDOS?!=?null,?"dao?有問(wèn)題?null");
????????????//?redis?檢測(cè)
????????????//?此處省略其它檢測(cè)
????????????return?"health";
????}
}
這樣 JVM 啟動(dòng)后腳本通過(guò)調(diào)用 curl http://ip:port/service/health/deepCheck 來(lái)判斷服務(wù)是否真正上線(xiàn)了,結(jié)果返回 「health」 才說(shuō)明服務(wù)真正可用了
滾動(dòng)發(fā)布與藍(lán)綠發(fā)布
接下來(lái)我們來(lái)看看業(yè)務(wù)普遍采用的兩種發(fā)布模式:滾動(dòng)發(fā)布和藍(lán)綠發(fā)布
什么是滾動(dòng)發(fā)布
滾動(dòng)發(fā)布:一般是取出一個(gè)或者多個(gè)服務(wù)器停止服務(wù),執(zhí)行更新,并重新將其投入使用。周而復(fù)始,直到集群中所有的實(shí)例都更新成新版本,一圖勝千言

滾動(dòng)發(fā)布應(yīng)該是業(yè)界普遍采用的方案了,這種方案簡(jiǎn)單,但是如果一旦發(fā)現(xiàn)問(wèn)題要回滾就麻煩了,得把服務(wù)一臺(tái)臺(tái)停掉并啟用老的包,影響時(shí)間會(huì)比較長(zhǎng),于是人們又提出了藍(lán)綠部署
什么是藍(lán)綠部署
為了解決滾動(dòng)發(fā)布回滾慢的問(wèn)題,人們提出了藍(lán)綠發(fā)布,首先把新包部署到新集群上,待新集群部署成功后,在網(wǎng)關(guān)基于 dubbo 路由來(lái)將流量打到新集群

這樣的話(huà)如果一旦發(fā)現(xiàn)新功能有問(wèn)題,網(wǎng)關(guān)可以根據(jù) tag 馬上將流量切回到老集群,相當(dāng)于實(shí)時(shí)回滾,用起來(lái)確實(shí)給力,唯一的缺點(diǎn)也是很明顯的,費(fèi)錢(qián)!因?yàn)橐獪?zhǔn)備和原集群一樣多的機(jī)器,所以適合土豪玩家,我們集團(tuán)曾經(jīng)搞過(guò),后來(lái)現(xiàn)金流緊張,把這玩意給停了
以上就是發(fā)布服務(wù)上下線(xiàn)需要注意的一些點(diǎn),雖然本文發(fā)布是以 Java 項(xiàng)目為例,但其實(shí)其他項(xiàng)目發(fā)布注意的點(diǎn)也是大同小異的,無(wú)非就是兩點(diǎn)
1 是注意下線(xiàn)時(shí)的優(yōu)雅停機(jī),預(yù)留足夠多的時(shí)候來(lái)讓服務(wù)做些資源清理的工作
2 是發(fā)布上線(xiàn)時(shí)需要根據(jù)你的業(yè)務(wù)量提前做壓測(cè),做好監(jiān)控相關(guān)的工作以便提升服務(wù)的可用性
往期推薦
