首頁 > 軟體

阿里雲k8s服務springboot專案應用升級時出現502錯誤

2022-04-08 19:00:25

隨著小步快跑、快速迭代的開發模式被越來越多的網際網路企業認同和採用,應用的變更、升級頻率變得越來越頻繁。為了應對不同的升級需求,保證升級過程平穩順利地進行,誕生了一系列的部署釋出模式。

  • 停機發布 - 把老版的應用範例完全停止,再發布新的版本。這種釋出模式主要為了解決新老版本互不相容、無法共存的問題,缺點是一段時間內服務完全不可用。
  • 藍綠髮布 - 線上上同時部署相同數量的新老版本應用範例。待新版本測試通過後,將流量一次性地切到新的服務範例上來。這種釋出模式解決了停機發布中存在的服務完全不可用問題,但會造成比較大的資源消耗。
  • 捲動釋出 - 分批次逐步替換應用範例。這種釋出模式不會中斷服務,同時也不會消耗過多額外的資源,但由於新老版本範例同時線上,可能導致來自相同使用者端的請求在新老版中切換而產生相容性問題。
  • 金絲雀釋出 - 逐漸將流量從老版本切換到新版本上。如果觀察一段時間後沒有發現問題,就進一步擴大新版本流量,同時減少老版本上流量。
  • A/B 測試 - 同時上線兩個或多個版本,收集使用者對這些版本的反饋,分析評估出最好版本正式採用。

隨著越來越多的應用被容器化,如何方便地讓容器應用平穩順利升級受到了廣泛關注。本文將介紹 k8s 中不同部署形式下應用的升級方法,並重點介紹如何對 Deployment 中的應用實施捲動釋出(本文所作的調研基於k8s 1.13)。

K8s 應用升級

在 k8s 中,pod 是部署和升級的基本單位。一般來說,一個 pod 代表一個應用範例,而 pod 又會以 DeploymentStatefulSetDaemonSetJob 等形式部署執行,下面依次介紹在這些部署形式下 pod 的升級方法。

Deployment

Deployment 是 pod 最常見的部署形式,這裡將以基於 spring boot 的 java 應用為例進行介紹。該應用是基於真實應用抽象出來的簡單版本,非常具有代表性,它有如下特點:

  • 應用啟動後,需要花費一定的時間載入設定,在這段時間內,無法對外提供服務。
  • 應用能夠啟動並不意味著它能夠正常提供服務。
  • 應用如果無法提供服務不一定能自動退出。
  • 在升級過程中需要保證即將下線的應用範例不會接收到新的請求且有足夠時間處理完當前請求。

引數設定

為了讓具有上述特點的應用實現零宕機時間和無生產中斷的升級,需要精心地設定 Deployment 中的相關引數。這裡和升級有關的設定如下(完整設定參見 spring-boot-probes-v1.yaml)。

kind: Deployment
...
spec:
  replicas: 8
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3
      maxUnavailable: 2
  minReadySeconds: 120
  ...
  template:
    ...
    spec:
      containers:
      - name: spring-boot-probes
        image: registry.cn-hangzhou.aliyuncs.com/log-service/spring-boot-probes:1.0.0
        ports:
        - containerPort: 8080
        terminationGracePeriodSeconds: 60
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 1
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 40
          periodSeconds: 20
          successThreshold: 1
          failureThreshold: 3
        ...

設定 strategy

通過 strategy 可以設定 pod 的替換策略,主要引數如下。

  • .spec.strategy.type - 用於指定替換 pod 的策略型別。該引數可取值 Recreate 或 RollingUpdate,預設為 RollingUpdate。

    • Recreate - K8s 會先刪掉全部原有 pod 再建立新的 pod。該方式適用於新老版本互不相容、無法共存的場景。但由於該方式會造成一段時間內服務完全不可用,在上述場景之外須慎用。
    • RollingUpdate - K8s 會將 pod 分批次逐步替換掉,可用來實現服務熱升級。
  • .spec.strategy.rollingUpdate.maxSurge - 指定在捲動更新過程中最多可建立多少個額外的 pod,可以是數位或百分比。該值設定得越大、升級速度越快,但會消耗更多的系統資源。
  • .spec.strategy.rollingUpdate.maxUnavailable - 指定在捲動更新過程中最多允許多少個 pod 不可用, 可以是數位或百分比。該值設定得越大、升級速度越快,但服務會越不穩定。

通過調節 maxSurge 和 maxUnavailable,可以滿足不同場景下的升級需求。

  • 如果您希望在保證系統可用性和穩定性的前提下儘可能快地進行升級,可以將 maxUnavailable 設定為 0,同時為 maxSurge 賦予一個較大值。
  • 如果系統資源比較緊張,pod 負載又比較低,為了加快升級速度,可以將 maxSurge 設定為 0,同時為 maxUnavailable 賦予一個較大值。需要注意的是,如果 maxSurge 為 0,maxUnavailable 為 DESIRED,可能造成整個服務的不可用,此時 RollingUpdate 將退化成停機發布。

樣例選擇了一個折中方案,將 maxSurge 設定為 3,將 maxUnavailable 設定為 2,平衡了穩定性、資源消耗和升級速度。

設定探針

K8s 提供以下兩類探針:

  • ReadinessProbe - 預設情況下,一旦某個 pod 中的所有容器全部啟動,k8s 就會認為該 pod 處於就緒狀態,從而將流量發往該 pod。但某些應用啟動後,還需要完成資料或組態檔的載入工作才能對外提供服務,因此通過容器是否啟動來判斷其是否就緒並不嚴謹。通過為容器設定就緒探針,能讓 k8s 更準確地判斷容器是否就緒,從而構建出更健壯的應用。K8s 保證只有 pod 中的所有容器全部通過了就緒探測,才允許 service 將流量發往該 pod。一旦就緒探測失敗,k8s 會停止將流量發往該 pod。
  • LivenessProbe - 預設情況下,k8s 會認為處於執行狀態下的容器是可用的。但如果應用在出現問題或不健康時無法自動退出(例如發生嚴重死鎖),這種判斷就會出現問題。通過為容器設定活性探針,能讓 k8s 更準確地判斷容器是否正常執行。如果容器沒有通過活性探測,kubelet 會將其停止,並根據重啟策略決定下一步的動作。

探針的設定非常靈活,使用者可以指定探針的探測頻率、探測成功閾值、探測失敗閾值等。各引數的含義和設定方法可參考檔案 Configure Liveness and Readiness Probes

樣例為目標容器設定了就緒探針和活性探針:

  • 就緒探針的 initialDelaySeconds 設定成 30,這是因為應用平均需要 30 秒時間完成初始化工作。
  • 在設定活性探針時,需要保證容器有足夠時間到達就緒狀態。如果引數 initialDelaySeconds、periodSeconds、failureThreshold 設定得過小,可能造成容器還未就緒就被重啟,以至於永遠無法達到就緒狀態。樣例中的設定保證如果容器能在啟動後的 80 秒內就緒就不會被重啟,相對 30 秒的平均初始化時間有足夠的緩衝。
  • 就緒探針的 periodSeconds 設定成 10,failureThreshold 設定成 1。這樣當容器異常時,大約 10 秒後就不會有流量發往它。
  • 活性探針的 periodSeconds 設定成 20,failureThreshold 設定成 3。這樣當容器異常時,大約 60 秒後就不會被重啟。

設定 minReadySeconds

預設情況下,一旦新建立的 pod 變成就緒狀態 k8s 就會認為該 pod 是可用的,從而將老的 pod 刪除掉。但有時問題可能會在新 pod 真正處理使用者請求時才暴露,因此一個更穩健的做法是當某個新 pod 就緒後對其觀察一段時間再刪掉老的 pod。

引數 minReadySeconds 可以控制 pod 處於就緒狀態的觀察時間。如果 pod 中的容器在這段時間內都能正常執行,k8s 才會認為新 pod 可用,從而將老的 pod 刪除掉。在設定該引數時,需要仔細權衡,如果設定得過小,可能造成觀察不充分,如果設定得過大,又會拖慢升級進度。樣例將 minReadySeconds 設定成了 120 秒,這樣能保證處於就緒狀態的 pod 能經歷一個完整的活性探測週期。

設定 terminationGracePeriodSeconds

當 k8s 準備刪除一個 pod 時,會向該 pod 中的容器傳送 TERM 訊號並同時將 pod 從 service 的 endpoint 列表中移除。如果容器無法在規定時間(預設 30 秒)內終止,k8s 會向容器傳送 SIGKILL 訊號強制終止程序。Pod 終止的詳細流程可參考檔案 Termination of Pods

由於應用處理請求最長耗時 40 秒,為了讓其在關閉前能夠處理完已到達伺服器端的請求,樣例設定了 60 秒的優雅關閉時間。針對不同的應用,您可以根據實際情況調整 terminationGracePeriodSeconds 的取值。

觀察升級行為

上述設定能夠保證目標應用的平滑升級。我們可以通過更改 Deployment 中 PodTemplateSpec 的任意欄位觸發 pod 升級,並通過執行命令kubectl get rs -w觀察升級行為。這裡觀察到的新老版本的 pod 副本數的變化情況如下:

  • 建立 maxSurge 個新 pod。這時 pod 總數達到了允許的上限,即 DESIRED + maxSurge。
  • 不等新 pod 就緒或可用,立刻啟動 maxUnavailable 個老 pod 的刪除流程。這時可用 pod 數為 DESIRED - maxUnavailable。
  • 某個老 pod 被完全刪除,這時會立刻補充一個新 pod。
  • 某個新 pod 通過了就緒探測變成了就緒態,k8s 會將流量發往該 pod。但由於未達到規定的觀察時間,該 pod 並不會被視作可用。
  • 某個就緒 pod 在觀察期內執行正常被視作可用,這時可以再次啟動某個老 pod 的刪除流程。
  • 重複步驟 3、4、5 直到所有老 pod 被刪除,並且可用的新 pod 達到目標副本數。

失敗回滾

應用的升級並不總會一帆風順,在升級過程中或升級完成後都有可能遇到新版本行為不符合預期需要回滾到穩定版本的情況。K8s 會將 PodTemplateSpec 的每一次變更(如果更新模板標籤或容器映象)都記錄下來。這樣,如果新版本出現問題,就可以根據版本號方便地回滾到穩定版本。回滾 Deployment 的詳細操作步驟可參考檔案 Rolling Back a Deployment

StatefulSet

StatefulSet 是針對有狀態 pod 常用的部署形式。針對這類 pod,k8s 同樣提供了許多引數用於靈活地控制它們的升級行為。好訊息是這些引數大部分都和升級 Deployment 中的 pod 相同。這裡重點介紹兩者存在差異的地方。

策略型別

在 k8s 1.7 及之後的版本中,StatefulSet 支援 OnDelete 和 RollingUpdate 兩種策略型別。

  • OnDelete - 當更新了 StatefulSet 中的 PodTemplateSpec 後,只有手動刪除舊的 pod 後才會建立新版本 pod。這是預設的更新策略,一方面是為了相容 k8s 1.6 及之前的版本,另一方面也是為了支援升級過程中新老版本 pod 互不相容、無法共存的場景。
  • RollingUpdate - K8s 會將 StatefulSet 管理的 pod 分批次逐步替換掉。它與 Deployment 中 RollingUpdate 的區別在於 pod 的替換是有序的。例如一個 StatefulSet 中包含 N 個 pod,在部署的時候這些 pod 被分配了從 0 開始單調遞增的序號,而在捲動更新時,它們會按逆序依次被替換。

Partition

可以通過引數.spec.updateStrategy.rollingUpdate.partition實現只升級部分 pod 的目的。在設定了 partition 後,只有序號大於或等於 partition 的 pod 才會進行卷動升級,其餘 pod 將保持不變。

Partition 的另一個應用是可以通過不斷減少 partition 的取值實現金絲雀升級。具體操作方法可參考檔案 Rolling Out a Canary

DaemonSet

DaemonSet 保證在全部(或者一些)k8s 工作節點上執行一個 pod 的副本,常用來執行監控或紀錄檔收集程式。對於 DaemonSet 中的 pod,用於控制它們升級行為的引數與 Deployment 幾乎一致,只是在策略型別方面略有差異。DaemonSet 支援 OnDelete 和 RollingUpdate 兩種策略型別。

  • OnDelete - 當更新了 DaemonSet 中的 PodTemplateSpec 後,只有手動刪除舊的 pod 後才會建立新版本 pod。這是預設的更新策略,一方面是為了相容 k8s 1.5 及之前的版本,另一方面也是為了支援升級過程中新老版本 pod 互不相容、無法共存的場景。
  • RollingUpdate - 其含義和可配引數與 Deployment 的 RollingUpdate 一致。

捲動更新 DaemonSet 的具體操作步驟可參考檔案 Perform a Rolling Update on a DaemonSet

Job

Deployment、StatefulSet、DaemonSet 一般用於部署執行常駐程序,而 Job 中的 pod 在執行完特定任務後就會退出,因此不存在捲動更新的概念。當您更改了一個 Job 中的 PodTemplateSpec 後,需要手動刪掉老的 Job 和 pod,並以新的設定重新執行該 job。

總結

K8s 提供的功能可以讓大部分應用實現零宕機時間和無生產中斷的升級,但也存在一些沒有解決的問題,主要包括以下幾點:

  • 目前 k8s 原生僅支援停機發布、捲動釋出兩類部署升級策略。如果應用有藍綠髮布、金絲雀釋出、A/B 測試等需求,需要進行二次開發或使用一些第三方工具。
  • K8s 雖然提供了回滾功能,但回滾操作必須手動完成,無法根據條件自動回滾。
  • 有些應用在擴容或縮容時同樣需要分批逐步執行,k8s 還未提供類似的功能。

範例設定:

livenessProbe:
  failureThreshold: 3
  httpGet:
    path: /user/service/test
    port: 8080
    scheme: HTTP
  initialDelaySeconds: 40
  periodSeconds: 20
  successThreshold: 1
  timeoutSeconds: 1
name: dataline-dev
ports:
  - containerPort: 8080
    protocol: TCP
readinessProbe:
  failureThreshold: 1
  httpGet:
    path: /user/service/test
    port: 8080
    scheme: HTTP
  initialDelaySeconds: 30
  periodSeconds: 10
  successThreshold: 1
  timeoutSeconds: 1

經測試 , 再對sprintboot 應用進行更新時, 存取不再出現502的報錯。

更多關於阿里雲k8s服務springboot專案應用升級時出現502錯誤技術文章請檢視下面的相關連結


IT145.com E-mail:sddin#qq.com