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


背景

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

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

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

K8s 應用升級

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

Deployment

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

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

參數配置

為了讓具有上述特點的應用實現零宕機時間和無生產中斷的升級,需要精心地配置 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,可以滿足不同場景下的升級需求。

  1. 如果您希望在保證系統可用性和穩定性的前提下盡可能快地進行升級,可以將 maxUnavailable 設置為 0,同時為 maxSurge 賦予一個較大值。
  2. 如果系統資源比較緊張,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

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

  1. 就緒探針的 initialDelaySeconds 設置成 30,這是因為應用平均需要 30 秒時間完成初始化工作。
  2. 在配置活性探針時,需要保證容器有足夠時間到達就緒狀態。如果參數 initialDelaySeconds、periodSeconds、failureThreshold 設置得過小,可能造成容器還未就緒就被重啟,以至於永遠無法達到就緒狀態。樣例中的配置保證如果容器能在啟動后的 80 秒內就緒就不會被重啟,相對 30 秒的平均初始化時間有足夠的緩沖。
  3. 就緒探針的 periodSeconds 設置成 10,failureThreshold 設置成 1。這樣當容器異常時,大約 10 秒后就不會有流量發往它
  4. 活性探針的 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 副本數的變化情況如下:

  1. 創建 maxSurge 個新 pod。這時 pod 總數達到了允許的上限,即 DESIRED + maxSurge。
  2. 不等新 pod 就緒或可用,立刻啟動 maxUnavailable 個老 pod 的刪除流程。這時可用 pod 數為 DESIRED - maxUnavailable。
  3. 某個老 pod 被完全刪除,這時會立刻補充一個新 pod。
  4. 某個新 pod 通過了就緒探測變成了就緒態,k8s 會將流量發往該 pod。但由於未達到規定的觀察時間,該 pod 並不會被視作可用。
  5. 某個就緒 pod 在觀察期內運行正常被視作可用,這時可以再次啟動某個老 pod 的刪除流程。
  6. 重復步驟 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 提供的功能可以讓大部分應用實現零宕機時間和無生產中斷的升級,但也存在一些沒有解決的問題,主要包括以下幾點:

  1. 目前 k8s 原生僅支持停機發布、滾動發布兩類部署升級策略。如果應用有藍綠發布、金絲雀發布、A/B 測試等需求,需要進行二次開發或使用一些第三方工具。
  2. K8s 雖然提供了回滾功能,但回滾操作必須手動完成,無法根據條件自動回滾。
  3. 有些應用在擴容或縮容時同樣需要分批逐步執行,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的報錯。 


免責聲明!

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



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