1. 程式人生 > 實用技巧 >SpringCloud 應用在 Kubernetes 上的最佳實踐 — 線上釋出(優雅上下線)

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

作者 | 驕龍

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

前言

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

介紹

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

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

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

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

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

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

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

如何實現優雅下線

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

  • 結構(元件)應形成層次關係;
  • 使用者執行緒需能收到停止訊號並響應退出;否則使用 daemon 執行緒;
  • 結構應按依賴關係自下向上構建:就像建房子一樣,自內向外構建而成;
  • 結構應按依賴關係自上向下銷燬:就像拆房子一樣,自外向內拆解。

優雅下線實現路徑

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

  • 接收訊號:停止訊號可能從程序內部觸發(比如 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,簡單羅列如下:

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

  • InitializingBean or @PostConstruct:在 Bean 裝配完後,被回撥,如完成資料來源初始化連線;

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

Spring Cloud 應用的優雅下線

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

場景一:直接訪問 WEB 服務

客戶端直接訪問 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 上的討論

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

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

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

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

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

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

在優雅關閉 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 自定義上下線的流程。

拋開實現成本,以下線的情況來說,一個 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 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”