1 概念
優(yōu)雅停機(jī)是什么?網(wǎng)上說的優(yōu)雅下線、無損下線,都是一個(gè)意思。
優(yōu)雅停機(jī),通常是指在設(shè)備、系統(tǒng)或應(yīng)用程序中止運(yùn)作前,先執(zhí)行一定的流程或動作,以確保數(shù)據(jù)的安全、預(yù)防錯(cuò)誤并保證系統(tǒng)的整體穩(wěn)定。
一般來說,優(yōu)雅停機(jī)可以參考以下步驟以實(shí)現(xiàn):
備份數(shù)據(jù) :立即將內(nèi)存中的所有未保存的修改、緩存等數(shù)據(jù)保存到數(shù)據(jù)庫或磁盤中。
停止接收新的請求
處理未完成的請求
通知其他依賴組件
等待所有要素安全退出后,關(guān)閉系統(tǒng)
在具體實(shí)施時(shí),不同的設(shè)備、不同的系統(tǒng)、不同的應(yīng)用,所需要的優(yōu)雅停機(jī)步驟也不盡相同,甚至需要根據(jù)不同的場景來選擇不同的方法。
例如,在某些情況下,你可能需要讓用戶知道,系統(tǒng)即將關(guān)閉,并告訴他們應(yīng)當(dāng)保存所有的工作并退出系統(tǒng);而在另一些情況下,你可能需要設(shè)計(jì)一種策略,能夠讓系統(tǒng)在無用戶介入的情況下,自動保存所有的狀態(tài),并在下次啟動時(shí)恢復(fù)之。
但是,無論在哪種情況下,優(yōu)雅停機(jī)的目標(biāo)都是保護(hù)數(shù)據(jù),避免錯(cuò)誤,并盡量減少到訪用戶或使用者的不便。
上面的步驟,其實(shí)還缺了不少基礎(chǔ)的內(nèi)容,比如,停止請求外,還要停止接收定時(shí)任務(wù)、停止接收mq消息,等待他們的完成,這2項(xiàng)都是我們微服務(wù)中必不可缺的能力。
因此,我希望通過本文,能夠更清晰,更詳細(xì)的講解,在我已知的真實(shí)業(yè)務(wù)場景下,如何做優(yōu)雅停機(jī)。
文中,很多內(nèi)容不會講得太詳細(xì),需要大家有一定的搜索能力或者經(jīng)驗(yàn)!
2 用案例說話
隨著微服務(wù)的興起,運(yùn)維方式由docker -> k8s 變化,優(yōu)雅停機(jī)涉及到的點(diǎn)就越來越多!下面,我們用一個(gè)案例,說明優(yōu)雅停機(jī)中的問題和問題解決方案。
案例前:k8s 停機(jī)流程
當(dāng)程序員執(zhí)行 kubectl delete pod 命令時(shí),兩個(gè)過程開始:
網(wǎng)絡(luò)規(guī)則即將生效:
Kube-apiserver 收到 pod 刪除請求,并將 pod 的狀態(tài)更新為 Extinating at Etcd;
終結(jié)點(diǎn)控制器從終結(jié)點(diǎn)對象中刪除 Pod 的 IP;
Kuber-proxy 根據(jù) Endpoint 對象的更改更新 iptables 的規(guī)則,并且不再將流量路由到已刪除的 pod。
刪除容器:
Kube-apiserver 收到 pod 刪除請求,并將 pod 的狀態(tài)更新為 Extinating at Etcd;
Kubelet 清理節(jié)點(diǎn)處的容器相關(guān)資源,如存儲、網(wǎng)絡(luò);
添加 Prestop hook 鉤子,等待流量不再發(fā)給pod;
Kubelet 將SIGTERM發(fā)送到容器;
如果容器在默認(rèn)的 30 秒內(nèi)沒有退出,Kubelet 將發(fā)送 SIGKILL 并強(qiáng)制其退出。
k8s + springboot + nacos 案例
PreStopHook 做了2件事情:
nacos反注冊
休眠35秒
通過信號量關(guān)閉springboot程序;
其中,k8s的 terminationGracePeriodSeconds(寬限期)設(shè)置為35s。
問題
springBoot程序關(guān)閉時(shí)間只有2s, 那么該程序就無法處理完一些線程任務(wù)、異步消息、定時(shí)任務(wù)等。為什么呢?
寬限期設(shè)置了35s,PreStop休眠了35s + 一個(gè)請求的時(shí)間,超過了寬限期,那么 kubelet 就會給與 pod 增加一次性2s的寬限時(shí)間。Pod 的生命周期,2s不管程序是否正常結(jié)束,都會被Kill -9。
為什么反注冊之后需要休眠35s?
這里涉及到nacos服務(wù)發(fā)現(xiàn)原理,nacos服務(wù)變更響應(yīng)時(shí)間:實(shí)時(shí);ribbon 默認(rèn)緩存刷新時(shí)間30s;因此,一開始是設(shè)置30s的,發(fā)現(xiàn)還有feign請求失敗的情況,所以設(shè)置成了35s以解決這個(gè)問題!
nacos服務(wù)變更響應(yīng)時(shí)間真的是實(shí)時(shí)嗎?
其實(shí)并不一定,nacos服務(wù)發(fā)現(xiàn)是通過http和udp實(shí)現(xiàn)的,udp是實(shí)時(shí)的,http最大等待時(shí)間是10s,但是,udp端口生產(chǎn)環(huán)境可能沒有開放!所以,案例中的nacos服務(wù)發(fā)現(xiàn)僅通過http定時(shí)輪詢實(shí)現(xiàn)。
案例優(yōu)化
上面的案例可以優(yōu)化的點(diǎn)
nacos 反注冊后休眠35s,是否可以減少;
terminationGracePeriodSeconds 設(shè)置多少合理?
優(yōu)化點(diǎn)1
反注冊后休眠的35s時(shí)候受到nacos服務(wù)發(fā)現(xiàn) + ribbon 緩存刷新時(shí)間影響,正常應(yīng)該是 服務(wù)發(fā)現(xiàn)時(shí)間 + 緩存刷新時(shí)間 40s才能在極端情況下保證服務(wù)停機(jī)時(shí),不會再有feign 請求進(jìn)入。
如果想要縮短這個(gè)時(shí)間
啟用udp,這個(gè)需要和運(yùn)維同學(xué)商量,否則10s等待少不了;
監(jiān)聽nacos服務(wù)變更通知,發(fā)現(xiàn)服務(wù)下線后,及時(shí)刷新ribbon緩存;
/** *訂閱nacos實(shí)例變更通知 *手動刷新ribbon服務(wù)實(shí)例緩存 *nacosclient1.4.6【1.4.1有重大缺陷,要注意】 */ @Component @Slf4j publicclassNacosInstancesChangeEventListenerextendsSubscriber{ @Resource privateSpringClientFactoryspringClientFactory; @PostConstruct publicvoidregisterToNotifyCenter(){ NotifyCenter.registerSubscriber(this); } @Override publicvoidonEvent(InstancesChangeEventevent){ Stringservice=event.getServiceName(); //service:DEFAULT_GROUP@@demoribbonService:demo StringribbonService=service.substring(service.indexOf("@@")+2); log.info("####接收到微服務(wù)nacos實(shí)例變更事件:{}ribbonServiceName:{}",event.getServiceName(),ribbonService); ILoadBalancerloadBalancer=springClientFactory.getLoadBalancer(ribbonService); if(loadBalancer!=null){ ((ZoneAwareLoadBalancer>)loadBalancer).updateListOfServers(); log.info("刷新 ribbon 服務(wù)實(shí)例:{}緩存成功",ribbonService); } } @Override publicClass?extends?com.alibaba.nacos.common.notify.Event>subscribeType(){ returnInstancesChangeEvent.class; } /** *nacos1.4.4~1.4.6需要加這個(gè)方法的實(shí)現(xiàn),2.1.2以后版本修復(fù)了該問題 *多注冊中心時(shí),變更事件沒有隔離,因此需要實(shí)現(xiàn)該方法來判斷事件是否需要處理 *@seeISSUE#8428-NacosInstancesChangeEventScope ***/ @Override publicbooleanscopeMatches(InstancesChangeEventevent){ returntrue; } }
優(yōu)化點(diǎn)2
terminationGracePeriodSeconds 的值應(yīng)該略大于 PreStop耗時(shí) + springBoot 停機(jī)時(shí)間,springBoot 停機(jī)時(shí)間是由程序業(yè)務(wù)決定的(mq消息、定時(shí)任務(wù)、線程池任務(wù)、以及備份數(shù)據(jù)),網(wǎng)上的推薦做法是啟用springBoot的優(yōu)雅停機(jī)功能,并實(shí)現(xiàn)自定義的關(guān)閉邏輯。
springBoot優(yōu)雅停機(jī)的默認(rèn)緩沖時(shí)間是30s,因此,terminationGracePeriodSeconds的時(shí)間個(gè)人建議10 + 30s即可。
經(jīng)過優(yōu)化后
使用 actuator shutdown 方案
有些網(wǎng)貼推薦使用 actuator shutdown 進(jìn)行優(yōu)雅停機(jī),那么看下其流程圖:
其實(shí),真正的情況并非如上圖所示,因?yàn)檎{(diào)用shutdown后,springBoot就會進(jìn)入優(yōu)雅停機(jī)流程,但是這個(gè)流程沒有結(jié)束,然后就會被kill -15 中斷,如果線程池沒有做好配置,線程池任務(wù)沒有結(jié)束,服務(wù)就會關(guān)閉。
//沒有設(shè)置下面參數(shù),在kill-15時(shí),線程池沒有執(zhí)行結(jié)束,會被強(qiáng)制關(guān)閉 threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); threadPoolTaskExecutor.setAwaitTerminationSeconds(30);
3 再次優(yōu)化
mq 和 定時(shí)任務(wù)
上面的方案中,提到nacos反注冊時(shí),其他服務(wù)監(jiān)聽反注冊事件,進(jìn)行ribbon緩存刷新,那么,反注冊的服務(wù)(停機(jī)服務(wù))自身,是否可以也監(jiān)聽該事件呢?答案是可以的。
停機(jī)的服務(wù)監(jiān)聽nacos反注冊事件,判斷是自己反注冊了,表示準(zhǔn)備關(guān)機(jī),那么就可以停止對mq消息的監(jiān)聽,停止定時(shí)任務(wù),這樣就比在優(yōu)雅停機(jī)時(shí),進(jìn)行mq 和 定時(shí)任務(wù)的停止更完美。
流量控制
如果沒有使用k8s進(jìn)行pod節(jié)點(diǎn)的流量控制,那么大概率會使用 springCloud gateway作為服務(wù)網(wǎng)關(guān),因此,gateway 服務(wù)也應(yīng)該監(jiān)聽nacos的反注冊事件,從而及時(shí)刷新ribbon的緩存,關(guān)閉停機(jī)服務(wù)的流量。
4 小結(jié)
經(jīng)過大量的資料參考、學(xué)習(xí),最終得到的一份自己認(rèn)為合格的優(yōu)雅停機(jī)方案,里面可能有較多的不專業(yè)表述,敬請諒解和指正,謝謝。
在本文的最后,還要說下,優(yōu)雅停機(jī)最大的挑戰(zhàn)并不是來源于這個(gè)優(yōu)雅停機(jī)流程,機(jī)械化的流程前人都幫忙躺過了,剩下的是業(yè)務(wù)服務(wù)自身的邏輯:
有沒有包含超過30s的業(yè)務(wù)邏輯,如執(zhí)行超過30s的請求,定時(shí)任務(wù)、線程池任務(wù)或mq消息;
服務(wù)關(guān)閉時(shí),如何保存未完成的任務(wù)、數(shù)據(jù),實(shí)現(xiàn)自定義的關(guān)閉邏輯;
接口邏輯是否做了冪等;
審核編輯:劉清
-
控制器
+關(guān)注
關(guān)注
112文章
16361瀏覽量
178071 -
UDP通信
+關(guān)注
關(guān)注
0文章
21瀏覽量
1909 -
nacos
+關(guān)注
關(guān)注
0文章
10瀏覽量
203
原文標(biāo)題:SpringBoot + Nacos + k8s 優(yōu)雅停機(jī)
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論