天天看點

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

前言

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

介紹

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

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

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

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

應用的加載是

漫長

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

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

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

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

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

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

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

如何實作優雅下線

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

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

優雅下線實作路徑

大緻分為一個完整的過程,需要經曆一下四個關鍵的節點,如下圖;

SpringCloud 應用在 Kubernetes 上的最佳實踐 — 線上釋出(優雅上下線)
  • 接收信号:停止信号可能從程序内部觸發(比如 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

,簡單羅列如下:

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

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

  • InitializingBean or @PostConstruct:在Bean裝配完後,被回調,如完成資料源初始化連接配接。
  • ApplicationReadyEvent、ApplicationRunner、CommandLineRunner:如開始監聽消息隊列,處理消息;注冊到SLB等;先通過配置禁用服務的自動注冊,在這裡做手動服務注冊。

Spring Cloud應用的優雅下線

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

場景一:直接通路WEB服務

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

用戶端直接通路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上的讨論

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

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

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

  • 反向代理支援失敗轉移到其它應用執行個體
  • 在關閉應用前,如将健康探測接口傳回不健康以及等待足夠的逾時,讓反向代理感覺并摘除執行個體的路由資訊。

對于仍在使用2.3以前版本的Spring Cloud應用,可參見

一個方案

,實作方式:

  • 使用自身的shutdownHook替換Spring的shutdownHook
  • 先改變health狀态,等待一段時間,讓反向代理感覺并摘除執行個體的路由資訊

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

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

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

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

抛開實作成本,以下線的情況來說,一個 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 的關鍵部分,簡單的了解是流量洪峰到來如何保證系統不會受到影響?當然我們還有一部分要達成的是洪峰退去之後資源是否存在浪費?敬請期待 ...