SpringCloud 應用在 Kubernetes 上的最佳實踐 — 線上發布(優雅上下線)


頭圖.png

作者 | 驕龍

導讀:本篇是《SpringCloud 應用在 Kubernetes 上的最佳實踐》系列文章的第八篇,主要介紹了如何做到流量的無損上/下線。更多相關文章閱讀可查看文末。

前言

上篇我們講的是發布回滾過程,尤其是在 Kubernetes 的回滾過程中,原生有提供 Rollout 到上一個版本的能力,能保證我們在發布過程中遇到問題時快速回退的能力。然而在每一次上線的過程中,我們最難處理的就是正在運行中的流量,如何做到流量的無損上/下線,是一個系統能保證 SLA 的關鍵。

介紹

什么是優雅上線?就如下面這個房子一樣,未建好的房子,人住進去會有危險,房子應該建好,裝修好,人才能住進去。

1.jpeg

那么如何做到優雅上線呢?我們先來看一個 WEB 應用的加載過程,就像上面造房子一樣,是個漫長的過程:

2.png

應用的加載是漫長的,在加載過程,服務是不可預期的;如過早地打開 Socket 監聽,則客戶端可能感受到漫長的等待;如果數據庫、消息隊列、REDIS 客戶端未完成初始化,則服務可能因缺少關鍵的底層服務而異常。

所以在應用准備完成后,才接入服務,即做到優雅上線。當然應用上線后,也可能因如數據庫斷連等情況引起服務不可用;或是准備完成了,但在上線前又發生數據庫斷連,導致服務異常。為了簡化問題,后面兩種情況作為一個應用自愈的問題來看待。

什么是優雅下線?與建房子相反就像下面的危房一樣,人住在里面很危險,人應該先從房子出來,然后推掉房子。

3.jpeg

那么如何做到優雅下線呢?我們先來看一個 WEB 應用的停止過程:

4.png

所以關閉服務接入(轉移服務接入),完成正在處理的服務,清理自身占用的資源后退出即做到優雅下線。

如何實現優雅下線

從上面介紹看,似乎不難,但事實上,很少有系統真正實現了優雅上下線。因為軟件本身由無數各種各樣相互依賴的結構組成,每個結構都使用一些資源,污染一些資源;通常在設計之初優雅上下線也不被作為優先考慮的需求,所以對於下線的過程,通常都沒被充分考慮,在設計上通常要求:

  • 結構(組件)應形成層次關系;
  • 用戶線程需能收到停止信號並響應退出;否則使用 daemon 線程;
  • 結構應按依賴關系自下向上構建:就像建房子一樣,自內向外構建而成;
  • 結構應按依賴關系自上向下銷毀:就像拆房子一樣,自外向內拆解。

優雅下線實現路徑

大致分為一個完整的過程,需要經歷一下四個關鍵的節點,如下圖:

5.png

  • 接收信號:停止信號可能從進程內部觸發(比如 Crash 場景),如果自退出的話基本上無法保證優雅下線;所以能保證優雅下線的前提就是需要正確處理來自進程外部的信號;

  • 停止流量接收:由於在停止之前,我們會有一些正在處理的請求,貿然退出會對這些請求產生損耗。但是在這段時間之內我們絕不能再接收新的業務請求,如果這是一個后台任務型(消息消費型或任務調度型)的程序,也要停止接收新的消息和任務。對於一個普通的 WEB 場景,這一塊不同的場景實現的方式也會不一樣,下面的 Srping Cloud 應用的下線流程會詳細講解;

  • 銷毀資源:常見的是一些系統資源,也包括一些緩存、鎖的清理、同時也包括線程池、關閉阻塞中的的 IO 操作,等到我們這些服務器資源銷毀之后,就可以通知主線程退出。

Spring Cloud 應用

一個 Spring boot 應用通常由應用本身和一系列的 Starter 組成,對於 Spring boot 體系,需要了解如下核心概念:

  • Starter:提供一系列的模塊,由 Spring boot 核心通過 auto-configuration 機制加載;

  • Bean:一切皆 Bean,starter 模塊的加載產生各種 Bean;

  • Context:Bean 的容器,容器擁有生命周期,Bean 需要感知生命周期事件;

  • LifeCycle:生命周期管理接口;

  • ApplicationEvent:模塊之間,模塊與容器之間,通過發送或監聽事件來達到互相通訊的目的。

所以對於應用上下線這個主題,我們應盡可能利用其豐富的原生事件機制,Spring Cloud 中內置的 Starter 機制針對整個生命周期管理的過程有了很好的封裝。

Spring Cloud 應用的優雅上線

Spring Cloud 啟動過程觸發回調及事件如下,詳細介紹見 application-events-and-listeners,簡單羅列如下:

6.png

Spring 自身及其組件大量基於這些事件構建,如響應 WebServerInitializedEvent 事件向服務注冊中心注冊服務,對於應用一般可利用:

  • InitializingBean or @PostConstruct:在 Bean 裝配完后,被回調,如完成數據源初始化連接;

  • ApplicationReadyEvent、ApplicationRunner、CommandLineRunner:如開始監聽消息隊列,處理消息;注冊到SLB等;先通過配置禁用服務的自動注冊,在這里做手動服務注冊。

Spring Cloud 應用的優雅下線

Spring Cloud 本身可以作為一個應用單獨存在,也可以是依附在一個微服務集群中,同時還能作為反向代理架構中的一個網關。不同的場景,需要用到的方法也不一樣,我們就常用的三種場景針對性的加以說明。

場景一:直接訪問 WEB 服務

7.png

客戶端直接訪問 WEB 應用,在這個用例下,優雅下線需要做的事情有:

  • 正在處理的請求完成處理
  • 應用自身完成安全下線並正常退出
  • 客戶端感知到連接異常

Spring-boot 從 2.3 開始內置了 WEB 應用優雅下線的能力,需配置如下,具體介紹參見 graceful-shutdown

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s

其實現方式:

  • 首先關閉 socket 監聽,等待正在處理的所有請求完成:具體可見 WebServerGracefulShutdownLifecycle,通過 getPhase 返回最大值,達到早於 WEB 容器關閉執行的目的;

  • 然后觸發 WEB 容器關閉:具體可見 WebServerStartStopLifecycle。

但其實,對於未被 WEB 容器完全接收的請求,客戶端仍會收到連接被重置的異常,只是這個時間窗口極小。該需求從提出到實現的時間跨度較長,感興趣的可參見 github 上的討論

場景二:經由反向代理的服務優雅下線

8.jpeg

因為實例前面還有反向代理,相比上個場景,需要新增“反向代理下線”這個處理流程。即若應用已經下線,但反向代理未摘除該應用實例時客戶端將感知到失敗。一般采取的策略有:

  • 反向代理支持失敗轉移到其它應用實例;
  • 在關閉應用前,如將健康探測接口返回不健康以及等待足夠的超時,讓反向代理感知並摘除實例的路由信息。

對於仍在使用 2.3 以前版本的 Spring Cloud 應用,可參見一個方案,實現方式:

  • 使用自身的 shutdownHook 替換 Spring 的 shutdownHook;
  • 先改變 health 狀態,等待一段時間,讓反向代理感知並摘除實例的路由信息。

場景三:在微服務集群中下線單個服務

9.jpeg

在優雅關閉 Spring Cloud 應用自身之前,我們除了完成場景一之中的目標之外,還需要將自身節點從注冊中心中下線。目前在 Spring Cloud 中針對注冊中心下線的場景暫未提供開箱即用的方法,下面介紹兩種可能的實現方案:

方案 1:先通過腳本、或通過監聽 ContextClosedEvent 反注冊服務摘除流量;等待足夠時間,如使用 ribbon 負載均衡器,需要長於配置的刷新時間;對於基於 HTTP 的服務,若 Spring Cloud 版本小於 2.3,則時間需加上預期的請求處理時間;

方案 2:客戶端支持連接感知重試,如重試,實現方案可參考Spring-retry,針對連接異常 RemoteConnectFailureException 做重試。

針對 Eureka 中的場景,有一個很好的參考的例子,請參見:https://home1-oss.github.io/home1-oss-gitbook/release/docs/oss-eureka/GRACEFUL_SHUTDOWN.html

Kubernetes 下的機制

Kubernetes 中針對應用的的管控提供了豐富的手段,正常的情況它提供了應用生命周期中的靈活擴展點,同時也支持自己擴展它的 Operator 自定義上下線的流程。

10.jpeg

拋開實現成本,以下線的情況來說,一個 Kubernetes 應用實例下線之前,管控程序會向 POD 發送一個 SIGTERM 的信號,應用響應時除了額外響應這一個信號之外,還能觸發一段自定義的 PreStop 的掛在腳本,代碼樣例如下:

yaml
lifecycle:                   
      preStop:                   
        exec:                    
          command:               
          - sh
          - -c
          - "sleep 5"

上面的例子一點特殊說明:因服務控制面刷新與 POD 收到 SIGTERM 同時發生,所以這里通過 sleep 5 讓服務控制面先完成刷新,應用進程再響應 SIGTERM 信號。

Spring Cloud 與 Kubernetes 的結合

Kubernetes 會根據健康檢查的情況來更新服務(Service)列表,其中如果 Liveness 失敗,則會觸發容器重建,這是一個相對很重的操作;若 Readiness 失敗,則 Kubenetes 則默認不會將路由服務流量到相應的容器;基於這一機理,Spring Cloud 2.3 開始,也做了原生的的支持,具體參見 liveness-and-readiness-probes-with-Spring-boot,這些健康檢查端點可對接 Kubnetes 相應的 probe:

  • /actuator/health/liveness
  • /actuator/health/readiness

同時,Spring Boot 內置了相應的 API、事件、Health Check 監控,部分代碼/配置片段如下:

java
// Available as a component in the application context
ApplicationAvailability availability;
LivenessState livenessState = availabilityProvider.getLivenessState();
ReadinessState readinessState = availabilityProvider.getReadinessState();
....
// 對於應用,也可以通過API,發布相應的事件,來改變應用的狀態
AvailabilityChangeEvent.publish(this.eventPublisher, ex, LivenessState.BROKEN);
// 同時,應用監控也可影響這健康狀態,將監控與健康關聯,在K8S體系下,可以實現如離群摘除,應用自愈的能力
// application.properties
management.endpoint.health.group.liveness.include=livenessProbe,cacheCheck

回到 Spring Cloud 應用 在微服務集群中下線單個服務 的章節中,我們的應用如果跑在 Kuberntes 中,如果我們使用了原生的 Kubernetes 機制去管理應用生命周期的話,只需要發布一個應用事件 (LivenessState.BROKEN) 即可實現優雅下線的能力。

EDAS提供內置的優雅上下線能力

通過上面兩部分了解了 Spring Cloud 和 K8s 中的機制,EDAS 基於原生的機制,衍生出來了自己的方法,除了最大化利用這些能力:主動更新 Liveness、Readiness、Ribbon 服務列表之外,我們還提供了無代碼侵入的開箱即用的能力,列舉如下:

后續

這一章節之后,和發布相關的內容都已經更新完畢,下一章節我們要開始高可用部分的能力,高可用也是系統保障 SLA 的關鍵部分,簡單的理解是流量洪峰到來如何保證系統不會受到影響?當然我們還有一部分要達成的是洪峰退去之后資源是否存在浪費?敬請期待 ...

相關文章推薦:

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM