應用在虛機和容器場景下如何優(yōu)雅上下線
一、微服務應用上/下線發(fā)布過程中存在的問題
在應用上下線發(fā)布過程中,如何做到流量的無損上/下線,是一個系統(tǒng)能保證SLA的關鍵。如果應用上下線不平滑,就會出現(xiàn)短時間的服務調(diào)用報錯,比如連接被拒絕、請求超時、沒有實例和請求異常等問題。
1.1上線過程中的問題
在應用上線發(fā)布過程中,由于過早暴露服務,實例可能仍處在JVM JIT編譯或者使用的中間件還在加載,若此時大量流量進入,可能會瞬間壓垮新起的服務實例。我們在實際場景中,曾經(jīng)遇到provider服務啟動后,但是數(shù)據(jù)庫連接出現(xiàn)異常,未做好啟動前的資源準備,導致該provider服務在注冊中心暴露后DB異常還未修復,無法正常提供被consumer調(diào)用的能力,導致大量請求異常返回。如下圖日志所示,應用初始化時,DB連接失?。ㄔ摲諏B是弱依賴)。
1.2下線過程中的問題
在應用下線過程中,服務消費者感知服務提供者下線有延遲,在一段時間內(nèi),被路由到已下線服務提供者實例的請求都拋連接被拒絕異常。其次服務實例在接收到SIGKILL信號時,會立即關閉,但是這時候可能在請求隊列中存在一部分請求還在處理,如果立即關閉這些請求都會損失掉。實際應用中,我們在環(huán)境上部署了provider的唯一一個實例,該服務被consumer調(diào)用,然后再執(zhí)行kill -9 強殺應用provider的唯一實例后,服務進程實際上已經(jīng)被終止,但是服務的注冊信息還會在注冊中心(該場景使用的是ServiceComb)保留一段時間,未及時清除,如下圖所示。若此時消費者服務consumer調(diào)到該實例會報連接拒絕錯誤。因為消費者consumer服務還能發(fā)現(xiàn)該實例,獲取其IP和端口嘗試去調(diào)用,但是該provider服務實例其實已經(jīng)被銷毀了。
二、如何處理應用上/下線問題
那么有哪些優(yōu)化措施,可以減少應用上/下線中流量的損失?
2.1處理應用上線問題
應用上線發(fā)布主要問題是:其中一個原因是注冊太早,過早的暴露了服務;另一個原因是一些應用初始化緩慢,若遇到大量流量,應用容易宕機。可以采取以下優(yōu)化措施:
1.延遲注冊:微服務應用可以采用延遲注冊的方式,即在應用啟動之后一定時間再進行注冊。這樣可以確保應用完全就緒后再注冊,避免了服務未就緒就被外部訪問的情況。
2.健康檢查:微服務應用可以實現(xiàn)健康檢查接口,通過該接口可以檢查服務是否就緒。注冊中心可以通過定期調(diào)用該接口來判斷服務是否可以對外提供服務,從而避免了服務未就緒就被外部訪問的情況。
3.預熱:對新實例進行預熱,而不是突然將所有流量轉(zhuǎn)移到新實例上,從而避免新實例遇到大量流量,應用容易宕機的情況。
4.啟動優(yōu)化:對于整個服務啟動的過程,可以進行一些優(yōu)化措施,比如減少不必要的依賴、調(diào)整啟動順序等,從而加快服務啟動速度。
2.2應用合理的上線過程
合理的應用上線大致分為這樣一個過程:當應用啟動后,通過設置延遲注冊時間(服務對外暴露的時間)確保應用多久后可提供服務,其次可依賴平臺檢查服務的就緒狀態(tài)(比如K8S的就緒探針)確保服務對外提供服務為就緒狀態(tài),然后通過預熱對剛啟動應用進行保護,確保流量慢慢進入剛啟動的應用,最后流量逐漸增到正常情況。
2.3處理應用下線問題
應用下線過程最主要問題是:消費者應用無法及時感知到注冊中心列表的刷新,導致可能還有新流量訪問下線應用??梢圆扇∫韵聝?yōu)化措施:
1.減少注冊中心緩存時間:將注冊中心中服務列表的緩存時間縮短,可以使消費者應用更快地獲取到服務列表的最新信息。這樣可以減少因服務列表緩存而導致的訪問下線應用的流量。
2.實時性優(yōu)化:在服務消費者和注冊中心之間使用長連接、實時通知等機制,從而能夠?qū)崟r獲取注冊中心中服務列表的變化。
3.實現(xiàn)熔斷機制:在消費者應用中實現(xiàn)熔斷機制,當某個服務實例出現(xiàn)故障或不可用時,可以快速切換到其他可用的服務實例。這樣可以避免將流量發(fā)送到已下線的應用程序上,并確保消費者應用的可用性。
2.4應用合理的下線過程
合理的應用下線大致分為這樣一個過程:當應用接受到外部的關閉(停止服務)請求后,不能在接收新的業(yè)務請求,但是會存在一些正在處理的業(yè)務請求,需等這些請求處理完后再銷毀應用使用的資源,最后就可以通知主進程退出。
三、應用下線注意點
針對應用下線在虛機場景和容器場景需要關注一些注意點。
3.1虛機場景
當我們要關閉虛擬機應用時,我們一般會使用ps -ef | grep xxx查找到進程ID,然后再執(zhí)行kill -9 PID操作。
kill 命令使用科普:
1.kill -9, 系統(tǒng)會發(fā)出SIGKILL(9)信號,由操作系統(tǒng)內(nèi)核完成殺進程操作,該信號不允許忽略和阻塞,應用程序會立即終止(強制殺死)。
2.kill -15,默認使用信號,系統(tǒng)向應用發(fā)送SIGTERM(15)信號,給目標進程一個清理善后工作的機會是一種優(yōu)雅終止進程的方式,告訴進程需要停止運行并開始清理資源。
因為kill -9 PID會強制殺死應用,以合理的應用下線流程看,應需處理完相關舊業(yè)務請求,清理相關資源后再退出進程,所以當要關閉虛擬機應用時,請執(zhí)行kill PID——以優(yōu)雅的方式停止運行。
3.2容器場景
Kubernetes目前是業(yè)界容器編排領域的事實標準,業(yè)界一般默認都是用K8S來管理容器。K8S提供了Pod優(yōu)雅退出機制,允許Pod在退出前完成一些清理工作。preStop會先執(zhí)行完,然后K8S才會給Pod發(fā)送TERM信號。在容器場景利用K8S提供的preStop機制,配合延遲下線API使用,這樣就能保證流量的無損下線。
...
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","to do xxx; do sleep 30; done"]
...
(1)為什么容器應用(K8S環(huán)境)要配置preStop?首先要介紹一下Pod的終止過程。
參考:https://kubernetes.renkeju.com/chapter_4/4.5.5.pod_termination_process.html
1.用戶發(fā)送刪除 Pod 對象的命令。
2.API 服務器中的 Pod 對象會隨著時間的推移而更新,在寬限期內(nèi)(默認為30秒),Pod被視為“dead”。
3.將 Pod 標記為 “Terminating” 狀態(tài)。
4.(與第3步同時運行)kubelet 在監(jiān)控到 Pod 對象轉(zhuǎn)為 “Terminating” 狀態(tài)的同時啟動 Pod 關閉程序。
5.(與第3步同時運行)端點控制器監(jiān)控到 Pod 對象的關閉行為時將其從所有匹配到此端點的 Service 資源的端點列表中移除。
6.Pod 對象中的容器進程收到 TERM 信號。
7.如果當前當前 Pod 對象定義了 preStop 鉤子處理器,則在其標記為 “Terminating” 后即會以同步的方式啟動執(zhí)行;如若寬限期結(jié)束后,preStop 仍未執(zhí)行結(jié)束,則第2步會被重新執(zhí)行并額外獲取一個時長為2秒的小寬限期。
8.寬限期結(jié)束后,若存在任何一個仍在運行的進程,那么 Pod 對象即會收到 SIGKILL 信號。
9.kubelet 請求 API Server 將此 Pod 資源的寬限期設置為0從而完成刪除操作,它變得對用戶不在可見。
默認情況下,所有刪除操作的寬限期都是30秒,不過,kubectl delete 命令可以使用“--grace-period=”選項自定義其時長,若使用0值則表示直接強制刪除指定的資源,不過,此時需要同時為命令使用 “--force” 選項。
從上述Pod終止過程的時序圖可知,關閉Pod流程(關注紅色框),給Pod內(nèi)的進程發(fā)送TERM信號(即kill, kill -15),如果配置了preStop鉤子也會同時處理,最后寬限期結(jié)束后,若存在任何一個仍在運行的進程,那么Pod對象即會收到SIGKILL(kill-9)信號。
(2) 存在這樣一種情況Pod中的業(yè)務進程接受不到SIGTERM信號
存在這樣一種情況Pod中的業(yè)務進程接受不到SIGTERM信號(而且沒有配置preStop鉤子),等待一段時間業(yè)務進程直接被SIGKILL強制殺死了。
為什么業(yè)務進程接受不到SIGTERM信號?
通常都是因為容器啟動入口使用了 shell,比如使用了類似 /bin/sh -c my-app 或 /docker-entrypoint.sh 這樣的 ENTRYPOINT 或 CMD,這就可能就會導致容器內(nèi)的業(yè)務進程收不到SIGTERM信號,原因是:
1.容器主進程是shell,業(yè)務進程是在shell中啟動的,成為了shell進程的子進程。
2.shell 進程默認不會處理 SIGTERM 信號,自己不會退出,也不會將信號傳遞給子進程,導致業(yè)務進程不會觸發(fā)停止邏輯。
3.當?shù)鹊?K8S 優(yōu)雅停止超時時間 (terminationGracePeriodSeconds,默認30s),發(fā)送SIGKILL強制殺死shell及其子進程。
(3) 如何解決上述Pod中的業(yè)務進程接收不到SIGTERM信號問題
1.配置preStop鉤子(K8S場景),處理退出前完成一些清理工作,比如使用無損上下線插件的應用服務需在停止前通知實例進行下線。
2.如果可以的話,盡量不使用 shell 啟動業(yè)務進程。
3.如果一定要通過 shell 啟動,比如在啟動前需要用 shell 進程一些判斷和處理,或者需要啟動多個進程,那么就需要在 shell 中傳遞下 SIGTERM 信號了。
參考:
https://imroc.cc/k8s/faq/why-cannot-receive-sigterm/
所以容器應用(K8S環(huán)境)要配置preStop,在停止前通知實例進行下線,加了一層防護,保證Pod中的業(yè)務能優(yōu)雅的結(jié)束。
四、Sermant如何解決應用上/下線問題
針對應用上下線發(fā)布過程中的問題,Sermant插件提供預熱和延遲下線機制,為應用提供無損上下線的能力。預熱是無損上線的核心機制,延遲下線是無損下線的核心機制,而且為了無損上線,還做了延遲注冊機制。
4.1上線問題的解決方式
延遲注冊: 若服務還未完全初始化就已經(jīng)注冊到注冊中心提供給消費者調(diào)用,很有可能因資源為加載完成導致請求報錯。可以通過設置延遲注冊,讓服務充分初始化后再注冊到注冊中心對外提供服務。
預熱:是基于客戶端實現(xiàn)的,當流量進入時,Sermant會動態(tài)調(diào)整流量,根據(jù)服務的預熱配置,對流量進行動態(tài)分配。對于開啟服務預熱的實例,在剛啟動時,相對于其他已啟動的實例,分配的流量會更少,流量將以曲線方式隨時間推移增加直至與其他實例近乎持平。目的是采用少流量對服務實例進行初始化,防止服務崩潰。
4.2下線問題的解決方式
上圖描述了Sermant是如何解決服務下線問題的:
0. 微服務應用consumerA、providerA、consumerB、providerB攜帶Sermant啟動,并將相關ip:port等信息注冊到注冊中心;
1.微服務應用consumerA可以正常調(diào)用providerA和providerB;
2. 若要重啟providerA,providerA會標記自身將下線(通知注冊中心將下線),并開始統(tǒng)計請求確保當前請求已全部處理完成;
3. providerA會通知其上游應用其自身的下線信息;
4. consumerA接受到providerA下線信息后,將其從緩存實例列表移除;
5. providerA在處理完當前的所有請求后,即可重啟。
總的來說,Sermant對于服務下線的機制概括為:
延遲下線:即對下線的實例提供保護,插件基于下線實時通知+刷新緩存的機制快速更新上游的實例緩存,同時基于流量統(tǒng)計的方式,確保即將下線的實例盡可能的將流量處理完成,最大程度避免流量丟失。提供了延遲下線API,方便在K8S環(huán)境中配置preStop。
延遲下線接口為:
http://127.0.0.1:16688/\$\$sermant\$\$/shutdown
流量統(tǒng)計: 為確保當前請求已全部處理完成,在服務下線時,Sermant會嘗試等待30s(可配置),定時統(tǒng)計和判斷當前實例請求是否均處理完成,處理完成后最終下線。
Sermant優(yōu)雅上下線能力的詳細介紹和使用見:
https://sermant.io/zh/document/plugin/graceful.html
五、總結(jié)
Sermant插件為微服務應用提供無損上下線的能力,若要下線應用,針對虛擬場景,請使用kill PID;針對容器場景(K8S環(huán)境),請配置preStop鉤子。
Sermant 作為專注于服務治理領域的字節(jié)碼增強框架,致力于提供高性能、可擴展、易接入、功能豐富的服務治理體驗,并會在每個版本中做好性能、功能、體驗的看護。