在現代軟件開發領域,微服務架構與容器化部署已迅速成為行業新趨勢。微服務架構通過將應用拆分成多個小型、自治的服務單元,每個服務承擔某項特定的業務功能。而容器化部署則以其輕量級和高度可移植的特性,為這些微服務的有效打包、分發和運行提供了強大支持。
在這樣的環境中,實現微服務的優雅上下線變得至關重要。優雅上下線意味著在進行服務更新、擴展或縮減服務規模時,能夠無縫切換,避免或最小化對用戶的影響。這種做法不僅保障了系統的高可用性和穩定性,還大幅提升了開發和運維團隊的工作效率。
本文將深入探討如何借助容器化技術,實現微服務的優雅上下線。我們將分享一系列實用的方法和策略,包括滾動升級、就緒檢查以及優雅關閉等。通過采用這些策略,您能在進行版本更新、規模調整或故障恢復期間,確保系統的連續穩定運行,從而顯著提升整體的系統可靠性和穩定性。
1 項目背景
BOSS物業管理系統(以下簡稱“BOSS系統”)是碧桂園服務(以下簡稱“碧服”)體系中的核心主營收費系統,它主要負責管理客戶、房屋及車位等基礎數據,并支持物業費、合同類、表計類、車位類及臨時類費用的全自動化計費。BOSS系統采用微服務架構和容器化部署,拆分成30個不同功能的微服務。這種設計雖然大幅提升了系統的靈活性和可維護性,卻也增加了服務發版部署的時長。
此外,在發版過程中,服務可能會出現短暫的中斷,或者在服務停止時還有未完成的異步線程的任務,導致業務數據的不完整,進而引發大量的運維工單,增加運維成本的同時也影響了用戶體驗。
當前,BOSS系統采用了敏捷開發模式,其顯著特點之一是小步快跑。這種模式使我們能夠以更快的速度推出新功能和優化現有功能,迅速響應用戶業務需求的變化。在這種開發模式下,發版效率顯得尤為重要。以往,每次部署時長約兩小時,再加上發版后的驗證回歸和測試,整個流程可能需要數小時才能完成。這樣漫長的發版流程不僅占用了團隊大量的時間和資源,還增加了出錯的風險。
2 如何實現高效
2.1?引入發版的checklist
由于BOSS系統的服務拆分的比較細,若全量發版則需要發布30個服務。每次發版不僅包括數據庫更改腳本、nacos配置更新及XXL-Job任務調度等內容,還有服務清單和代碼遷入的情況。如果在發版前沒有進行充分的檢查與準備,后續可能需要多次更新服務,極大地增加了整個發版時長。
為了解決這一問題,引入發版checklist顯得尤為重要。該checklist能夠幫助盤點上線事項,并回顧開發過程中的各個細節。通過checklist,團隊可以更有序地執行發版流程,從而提高發版的效率和準確性。
上線checklist包括以下幾個關鍵內容:
1、上線前準備:此階段需準備數據庫腳本、nacos配置、XXL-Job任務以及一些提前編寫好的配置文件等;
2、上線步驟:包括更新的SQL、各個模塊的更新順序以及是否依賴公共包等。對于C端應用,需要注意服務端與前端的發布先后順序;
3、需驗證的事項:在每個模塊更新完成后,需采取相應的措施來驗證其是否正常,例如觀察頁面、檢查日志和監控是否正常等;
4、明確人與時間:checklist應盡可能詳細,明確具體的人員和特定時間段的任務安排;
5、評估對用戶的影響:在每個步驟完成后,需要評估對用戶的影響,并關注相應的內容;
6、提前做好預發回歸:預發環境應與生產環境的數據源相通。在預發環境中,可以模擬線上更新的步驟,提前預演一遍。為避免預發環境對線上的影響,可考慮使用白名單控制訪問權限,同時注意用戶權限的回收,以防止誤操作影響線上環境。
2.2 容器升級策略
在容器化部署中,滾動更新允許逐個替換Pod實例以實現零停機的Deployment更新。新創建的Pod將會被調度到可用資源的節點上。
在阿里云k8s中,默認采用滾動升級策略。此策略下“不可用Pod最大數量”和“超出期望的Pod數量”都是25%。然而,當節點資源的內存嚴重緊張時,日常使用平均內存利用率已經超過80%,并且需要同時更新30個服務,尤其是這些服務配置的內存需求多集中在8至16GB之間,就可能導致發版過程中節點池的內存資源不足以支撐這么多Pod的同時申請,導致容器嘗試滾動升級時大量Pod處于pending狀態,等待分配資源。
我們通過優化容器升級策略,在不增加節點服務器資源前提下,實現了快速的滾動升級。考慮到常規發布操作安排在非高峰時段,因此可以接受不可用Pod的最大數量控制在25%至80%之間。這種調整顯著釋放了節點資源,極大地提升了后續的容器滾動升級速率。
2.3 發版匯總
通過最近幾次的發版匯總記錄進行分析,我們可以發現,初次執行全量發布30個服務的操作耗時約兩小時。然而,在引入發版checklist和優化容器升級策略后,第二次進行全量發版的時間大幅縮短至半小時內。目前,全量發版僅需20分鐘即可完成,而對于日常的少量服務發版,則僅需10分鐘。發版時間的顯著縮短,為后續的測試驗證工作提供了更加充裕的時間,從而提高了整個發版與驗證的效率。
3?微服務優雅上下線設計與實踐
3.1?什么是微服務優雅上下線
微服務優雅上下線的基本原理是指在微服務更新發布過程中確保服務的穩定性和可用性,防止由于服務變更引起的流量中斷或錯誤。
實現微服務的優雅上下線,旨在避免以下問題:
-
過早的注冊服務:服務尚未完全就緒時就注冊到了注冊中心,開始接受請求,導致業務異常;
-
過早退出應用程序:服務還在處理請求時,應用程序被強制終止,導致正在進行的請求出現錯誤。
針對這些問題,我們可以采取以下優化措施:
-
優雅上線:在服務啟動后,等待服務完全就緒后再對外提供服務,或者有一個服務預熱過程;
-
優雅下線:在服務停止前,先從服務注冊中心注銷,拒絕新的請求,并等待舊的請求處理完畢后再下線服務,從而確保所有請求都能得到妥善處理。
3.2?實現微服務在容器中優雅上下線?
1、優雅上線
在實現微服務的優雅上線過程中,我們可以利用k8s的就緒檢查與微服務生命周期對齊,等完成服務注冊與準備就緒后,再開始接受外部流量。
就緒檢查接口一般包括數據庫連接狀態、redis連接狀態、nacos注冊狀態及調用預熱接口等工作。
我們可使用Spring Boot Actuator提供的健康檢查接口/health來做就緒檢查:
引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
啟用liveness和readiness探針
management:server:port: 8088endpoints:web:exposure:include: health,info,metrics,prometheus,monitoring,deregisterendpoint:health:show-details: alwaysprobes:enabled: truehealth:livenessstate:enabled: truereadinessstate:enabled: true
health/readiness接口會嚴格檢查SpringBoot的各項組件服務,比如郵件服務、數據庫服務及MQ服務等。當所有組件處于正常狀態時,它會返回內容{"status": "UP"},否則返回{"status": "down"}。
2、優雅下線
在實現微服務的優雅下線過程中,我們可以結合使用SpringBoot的優雅停機方案和k8s生命周期管理(停止前處理)來實現服務的優雅退出。
SpringBoot的優雅停機使用方式:
通過配置文件的方式即可開啟優雅停機,需要配置server.shutdown屬性和寬限期。寬限期會影響到同步請求的超時中斷。
# 開啟優雅關閉
server:shutdown: graceful
# 配置強制結束時間,不配置的話默認30s
spring:lifecycle:timeout-per-shutdown-phase: 60scloud:loadbalancer:cache:ttl: 10s
在Spring Cloud LoadBalancer中,為了優化服務調用的性能,減少對服務注冊中心的頻繁請求,LoadBalancer實現了對服務實例列表的本地緩存。默認設置下,這個緩存的時效為35秒。但是,這一默認緩存過期時間可能會導致在系統上下線過程中出現問題。如果緩存中仍然存儲著舊的服務列表,那么這可能會影響到服務的可用性和準確性。
優雅下線接口,這里采用的是手寫的方式,還可以用Spring Boot Actuator提供的接口/shutdown端點的方式,但該接口只支持POST的方式。
@Autowired
private NacosAutoServiceRegistration nacosAutoServiceRegistration;@ReadOperation
public String deregister() {Executors.newSingleThreadExecutor().submit(() -> {log.info("Ready to stop service: {}", serviceName);nacosAutoServiceRegistration.stop();log.info("Nacos instance has been de-registered.");});return "{\n" +" \"status\": \"UP\"\n" +"}";
}
注意:在優雅下線接口中,我們只需要執行退出nacos注冊操作即可,無需手動退出spring應用程序。這是因為配置文件已經啟用了服務器端的優雅關閉機制。另外,timeout-per-shutdown-phase參數的時間是影響同步請求的超時中斷。
容器停止前處理:配置調用優雅退出接口并等待30秒
容器生命周期:
容器終止流程:
1、Pod被刪除,狀態置為Terminating;
2、將Pod從service的endpoint列表中摘除掉;
3、如果Pod配置了preStop Hook,將會執行(容器停止前處理);
4、發送SIGTERM信號以通知容器進程開始優雅停止;
5、等待容器進程完全停止。如果在terminationGracePeriodSeconds內 (默認30s) 還未完全停止,就發送SIGKILL信號強制殺死進程;
6、容器進程終止,清理Pod資源。
在k8s的容器終止流程中,第五步為容器刪除預留了一個最大時間限制,即30秒。如果SpringBoot應用的優雅關閉超時時間和k8s的preStopHooks的總和超過30秒,那么k8s可能會在SpringBoot處理完所有請求之前強制刪除容器。
為了避免這種情況,我們可以調整優雅終止的時間。在k8s中,這個時間由terminationGracePeriodSeconds參數控制,其默認值是30s。我們可以根據實際情況調整這個值,但需要確保terminationGracePeriodSeconds的值要大于sleep時間。請注意,terminationGracePeriodSeconds設置的是最大等待時間,并不意味著每次終止都會等待這么長時間。
此外,探索JVM退出的鉤子函數(Runtime.addShutdownHook)的使用也是一個很好的實踐。通過添加關閉鉤子函數,可以實現在程序退出時的關閉資源、優雅退出的功能。這也是SpringBoot優雅退出的原理,ApplicationContext.registerShutdownHook方法是spring框架中的一個方法,用于注冊一個JVM關閉的鉤子(Shutdown Hook),當JVM關閉時,Spring容器可以優雅地關閉并釋放資源。
3、異步線程優雅退出
在實現服務優雅退出過程中,我們遇到了一個挑戰:異步線程的優雅退出。由于BOSS系統的業務復雜性,幾乎每個服務都使用了異步線程來處理一些耗時操作。然而,在發版期間,如果容器提前退出,那些尚未完成的異步任務可能會被中斷,導致業務數據的不完整,進而需要人工介入進行數據修正。
異步線程優雅退出的解決辦法:
-
使用統一的自定義線程池;
-
配置線程池優雅退出和任務最大結束時間。
@Bean("bossTaskExecutor")
public ThreadPoolTaskExecutor bossTaskExecutor() {log.info("start taskExecutor");ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 配置核心線程數executor.setCorePoolSize(threadPoolCorePoolSize);// 設置最大線程數executor.setMaxPoolSize(threadPoolMaxPoolSize);// 設置隊列容量executor.setQueueCapacity(threadPoolQueueCapacity);// 設置線程活躍時間(秒)executor.setKeepAliveSeconds(threadPoolKeepAliveSeconds);// 配置線程池中的線程的名稱前綴executor.setThreadNamePrefix("async-service-");// 設置拒絕策略// rejection-policy:當pool已經達到max size的時候,如何處理新任務// CALLER_RUNS:不在新線程中執行任務,而是有調用者所在的線程來執行executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());// 等待所有任務結束后再關閉線程池executor.setWaitForTasksToCompleteOnShutdown(true);// 等待所有任務結束的最長時間executor.setAwaitTerminationSeconds(threadAwaitTerminationSeconds);// 執行初始化executor.initialize();log.info("創建一個線程池 threadPoolCorePoolSize is [" + threadPoolCorePoolSize + "] threadPoolMaxPoolSize is ["] threadPoolKeepAliveSeconds is [" + threadPoolKeepAliveSeconds + "].");return executor;
}
?關鍵配置:
等待所有任務結束后再關閉線程池:
executor.setWaitForTasksToCompleteOnShutdown(true)
等待所有任務結束的最長時間:
executor.setAwaitTerminationSeconds(awaitTerminationSeconds)
需要注意的是:要保證異步線程的任務處理完才退出,容器端的
terminationGracePeriodSeconds時間要大于等于awaitTerminationSeconds,這樣才能夠確保異步線程任務的優雅退出。此外,上述的timeout-per-shutdown-phase時間和異步線程的任務最長時間沒沖突。
4、測試結果
為了測試異步線程在發版中是否被中斷,我們可以編寫一個測試接口來模擬這種情況:
@Autowired
@Qualifier("bossTaskExecutor")
private ThreadPoolTaskExecutor executorService;@ApiOperation(value = "測試異步耗時任務", notes = "測試異步耗時任務")
@GetMapping("/testAsyncTask")
public Response testAsyncTask() throws InterruptedException {executorService.execute(new Runnable() {@SneakyThrows@Overridepublic void run() {for (int i=0;i<=200;i++){Thread.sleep(1000);log.info("testAsyncTask-Thread:"+i);}}});for (int i=0;i<=120;i++){Thread.sleep(1000);log.info("testAsyncTask:"+i);}return Response.ok("200");
}
我們在容器開始部署時調用接口,并通過打印的日志可以觀察到異步線程能夠處理完,日志打印到了200。然而,在觀察容器滾動升級的過程中,我們會發現有一個Pod在Terminating的狀態停留了較久時間才退出,這是因為它正在等待異步線程的任務處理完再銷毀容器。?
4 總結
綜上,通過Spring Boot Actuator的優雅配置和健康檢查接口,以及配合k8s的就緒檢查策略,我們實現了優雅上線。對于優雅下線,我們通過SpringBoot的優雅停機配置和自定義的優雅下線接口,再配合k8s生命周期中的停止前處理,實現微服務的優雅退出。此外,我們還采用了統一的自定義線程池,并配置了線程池優雅退出機制和任務最大結束時間,以確保發版期間能夠妥善處理所有異步任務。
通過微服務優雅上下線實踐,我們取得了以下成果:
1、最小化服務中斷:通過優雅上下線,可以最小化服務中斷的時間和影響范圍,從而確保服務的可用性和穩定性;
2、數據一致性和完整性:優雅下線可以確保正在處理的請求能夠完成,避免數據丟失和請求失敗;
3、提升用戶體驗:優雅上下線可以確保用戶在使用服務時不會遇到任何中斷或錯誤,從而提高用戶的使用體驗和滿意度。
本文作者:
蔡冠怡:碧桂園服務后端開發高級工程師
指導人:
余儉:碧桂園服務技術總監
岳黎明:碧桂園服務架構師
黃志鴻:碧桂園服務運維高級工程師