15 分鐘實現企業級應用無損上下線

阿里巴巴雲原生發表於2022-03-04

很多使用者量大併發度高的應用系統為了避免釋出過程中的流量有損,一般選擇在流量較小的半夜釋出,雖然這樣做有效果,但不可控導致背後的研發運維成本對企業來說是一筆不小的負擔。基於此,阿里雲微服務引擎 MSE 在應用釋出過程中,通過應用下線時進行自適應等待+主動通知,應用上線時就緒檢查與微服務生命週期對齊+服務預熱等技術手段所提供的微服務應用無損上下線功能,能有效幫助企業規避線上釋出所出現的流量資損。

無損上下線功能設計

常見的流量有損現象出現的原因包括但不限於以下幾種:
• 服務無法及時下線:服務消費者感知註冊中心服務列表存在延時,導致應用下線後在一段時間內服務消費者仍然呼叫已下線應用造成請求報錯。
• 初始化慢:應用剛啟動接收線上流量進行資源初始化載入,由於流量太大,初始化過程慢,出現大量請求響應超時、阻塞、資源耗盡從而造成剛啟動應用當機。
• 註冊太早:服務存在非同步資源載入問題,當服務還未初始化完全就被註冊到註冊中心,導致呼叫時資源未載入完畢出現請求響應慢、呼叫超時報錯等現象。
• 釋出態與執行態未對齊:使用 Kubernetes 的滾動釋出功能進行應用釋出,由於Kubernetes 的滾動釋出一般關聯的就緒檢查機制,是通過檢查應用特定埠是否啟動作為應用就緒的標誌來觸發下一批次的例項釋出,但在微服務應用中只有當應用完成了服務註冊才可對外提供服務呼叫。因此某些情況下會出現新應用還未註冊到註冊中心,老應用例項就被下線,導致無服務可用。

無損下線

其中的服務無法及時下線問題,如下圖 1 所示:
在這裡插入圖片描述

圖1. Spring Cloud 應用消費者無法及時感知提供者服務下線

對於 Spring Cloud 應用,當應用的兩個例項 A’ 和 A 中的 A 下線時,由於 Spring Cloud 框架為了在可用性和效能方面做平衡,消費者預設是 30s 去註冊中心拉取最新的服務列表,因此 A 例項的下線不能被實時感知,此時消費者繼續通過本地快取繼續呼叫 A 就會出現呼叫已下線例項出現流量有損。

針對該問題,阿里雲微服務引擎 MSE 基於 Java Agent 位元組碼技術設計實現的無損下線功能如下圖 2 所示:
在這裡插入圖片描述圖2. 無損下線方案

在該套無損下線方案中,服務提供者應用僅需接入 MSE,相比一般的有損下線。應用在下線前會有一段自適應等待時期,該時期待下線應用會通過主動通知的方式,向其在自適應等待階段傳送了請求的服務消費者傳送下線事件,消費者接收到下線事件後會主動拉取註冊中心服務例項列表以便實時感知應用下線事件避免呼叫已下線例項造成應用下線流量有損。

無損上線

延遲載入是軟體框架設計中最常見的一種策略,例如在 Spring Cloud 框架中 Ribbon 元件的拉取服務列表初始化時機預設是要等到服務的第 1 次呼叫時刻,例如下圖 3 是 Spring Cloud 應用中第 1 次和第 2 次通過 RestTemplate 呼叫遠端服務的請求耗時情況:
在這裡插入圖片描述

圖3. 應用啟動資源初始化與正常執行過程中耗時情況對比

由測試結果可見,第一次呼叫由於進行了一些資源初始化,耗時是正常情況的數倍之多。因此把新應用釋出到線上直接處理大流量極易出現大量請求響應慢,資源阻塞,應用例項當機的現象。針對該類大流量下應用資源初始化慢問題,MSE 提供的小流量預熱功能通過調節剛上線應用所分配的流量幫助其在進行充分預熱後再處理正常流量從而對新例項進行保護。小流量預熱過程如下圖 4 所示:
在這裡插入圖片描述
圖4. 小流量服務預熱過程 QPS 與啟動時間關係圖

除了針對上述應用第一次呼叫初始化慢所造成的有損上線問題,MSE 還提供了資源預建連線、延遲註冊、確保 Kubernetes 就緒檢查通過前完成服務註冊和確保 Kubernetes 就緒檢查通過前完成服務預熱等一整套無損上線手段來滿足各類不同應用的無損上線需求,整套方案如圖 5 所示:

在這裡插入圖片描述

圖5. MSE 無損上線方案

如何使用 MSE 的無損上下線

接下來將演示阿里雲微服務引擎 MSE 在應用釋出時提供的無損上下線和服務預熱能力最佳實踐。假設應用的架構由 Zuul 閘道器以及後端的微服務應用例項(Spring Cloud)構成。具體的後端呼叫鏈路有購物車應用 A,交易中心應用 B,庫存中心應用 C,這些應用中的服務之間通過 Nacos 註冊中心實現服務註冊與發現。

前提條件

開啟 MSE 微服務治理

• 已建立 Kubernetes 叢集,請參見建立 Kubernetes 託管版叢集[1]。
• 已開通 MSE 微服務治理專業版,請參見開通 MSE 微服務治理[2]。

準備工作

注意,本實踐所使用的 Agent 目前還在灰度中,需要對應用 Agent 進行灰度升級,升級文件:https://help.aliyun.com/docum...

應用部署在不同的 Region(暫時僅支援國內 Region)請使用對應的 Agent 下載地址:http://arms-apm-cn-[regionId].oss-cn-[regionId].aliyuncs.com/2.7.1.3-mse-beta/,注意替換地址中的[RegionId],RegionId 是阿里雲 RegionId,

例如 Region 北京 Agent 地址為:http://arms-apm-cn-beijing.os...

應用部署流量架構圖
在這裡插入圖片描述

圖6. 演示應用部署架構

流量壓力來源

在 spring-cloud-zuul 應用中,如圖 6 所示,其分別向 spring-cloud-a 的灰度版本和正常版本以 QPS 為 100 的速率同時進行服務呼叫。

部署 Demo 應用程式

將下面的內容儲存到一個檔案中,假設取名為 mse-demo.yaml,並執行 kubectl apply -f mse-demo.yaml 以部署應用到提前建立好的 Kubernetes 叢集中(注意因為 demo 中有 CronHPA 任務,所以請先在叢集中安裝 ack-kubernetes-cronhpa-controller 元件,具體在容器服務-Kubernetes->市場->應用目錄中搜尋元件在測試叢集中進行安裝),這裡我們將要部署 Zuul,A, B 和 C 三個應用,其中 A、B 兩個應用分別部署一個基線版本和一個灰度版本,B 應用的基線版本關閉了無損下線能力,灰度版本開啟了無損下線能力。C 應用開啟了服務預熱能力,其中預熱時長為 120 秒。

# Nacos Server
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nacos-server
  name: nacos-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nacos-server
  template:
    metadata:
      labels:
        app: nacos-server
    spec:
      containers:
      - env:
        - name: MODE
          value: standalone
        image: registry.cn-shanghai.aliyuncs.com/yizhan/nacos-server:latest
        imagePullPolicy: Always
        name: nacos-server
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
      dnsPolicy: ClusterFirst
      restartPolicy: Always
# Nacos Server Service 配置
---
apiVersion: v1
kind: Service
metadata:
  name: nacos-server
spec:
  ports:
  - port: 8848
    protocol: TCP
    targetPort: 8848
  selector:
    app: nacos-server
  type: ClusterIP
#入口 zuul 應用
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-cloud-zuul
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-cloud-zuul
  template:
    metadata:
      annotations:
        msePilotAutoEnable: "on"
        msePilotCreateAppName: spring-cloud-zuul
      labels:
        app: spring-cloud-zuul
    spec:
      containers:
        - env:
            - name: JAVA_HOME
              value: /usr/lib/jvm/java-1.8-openjdk/jre
            - name: LANG
              value: C.UTF-8
          image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-zuul:1.0.1
          imagePullPolicy: Always
          name: spring-cloud-zuul
          ports:
            - containerPort: 20000
# A 應用 base 版本,開啟按照機器緯度全鏈路透傳
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-a
  name: spring-cloud-a
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-a
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-a
        msePilotAutoEnable: "on"
      labels:
        app: spring-cloud-a
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk/jre
        - name: profiler.micro.service.tag.trace.enable
          value: "true"
        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT
        imagePullPolicy: Always
        name: spring-cloud-a
        ports:
        - containerPort: 20001
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
        livenessProbe:
          tcpSocket:
            port: 20001
          initialDelaySeconds: 10
          periodSeconds: 30
      
# A 應用 gray 版本,開啟按照機器緯度全鏈路透傳
---            
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-a-gray
  name: spring-cloud-a-gray
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-a-gray
  strategy:
  template:
    metadata:
      annotations:
        alicloud.service.tag: gray
        msePilotCreateAppName: spring-cloud -a
        msePilotAutoEnable: "on"
      labels:
        app: spring-cloud-a-gray
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk/jre
        - name: profiler.micro.service.tag.trace.enable
          value: "true"
        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT
        imagePullPolicy: Always
        name: spring-cloud-a-gray
        ports:
        - containerPort: 20001
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
        livenessProbe:
          tcpSocket:
            port: 20001
          initialDelaySeconds: 10
          periodSeconds: 30
            
# B 應用 base 版本,關閉無損下線能力
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-b
  name: spring-cloud-b
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-b
  strategy:
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-b
        msePilotAutoEnable: "on"
      labels:
        app: spring-cloud-b
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk/jre
        - name: micro.service.shutdown.server.enable
          value: "false"
        - name: profiler.micro.service.http.server.enable
          value: "false"
        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT
        imagePullPolicy: Always
        name: spring-cloud-b
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
        livenessProbe:
          tcpSocket:
            port: 20002
          initialDelaySeconds: 10
          periodSeconds: 30
            
# B 應用 gray 版本,預設開啟無損下線功能
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-b-gray
  name: spring-cloud-b-gray
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-b-gray
  template:
    metadata:
      annotations:
        alicloud.service.tag: gray
        msePilotCreateAppName: spring-cloud-b
        msePilotAutoEnable: "on"
      labels:
        app: spring-cloud-b-gray
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk/jre
        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT
        imagePullPolicy: Always
        name: spring-cloud-b-gray
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
        lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - '-c'
                  - >-
                    wget http://127.0.0.1:54199/offline 2>/tmp/null;sleep
                    30;exit 0
        livenessProbe:
          tcpSocket:
            port: 20002
          initialDelaySeconds: 10
          periodSeconds: 30
            
# C 應用 base 版本
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-cloud-c
  name: spring-cloud-c
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-cloud-c
  template:
    metadata:
      annotations:
        msePilotCreateAppName: spring-cloud-c
        msePilotAutoEnable: "on"
      labels:
        app: spring-cloud-c
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk/jre
        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT
        imagePullPolicy: Always
        name: spring-cloud-c
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
        livenessProbe:
          tcpSocket:
            port: 20003
          initialDelaySeconds: 10
          periodSeconds: 30
#HPA 配置
---
apiVersion: autoscaling.alibabacloud.com/v1beta1
kind: CronHorizontalPodAutoscaler
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: spring-cloud-b
spec:
   scaleTargetRef:
      apiVersion: apps/v1beta2
      kind: Deployment
      name: spring-cloud-b
   jobs:
   - name: "scale-down"
     schedule: "0 0/5 * * * *"
     targetSize: 1
   - name: "scale-up"
     schedule: "10 0/5 * * * *"
     targetSize: 2
---
apiVersion: autoscaling.alibabacloud.com/v1beta1
kind: CronHorizontalPodAutoscaler
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: spring-cloud-b-gray
spec:
   scaleTargetRef:
      apiVersion: apps/v1beta2
      kind: Deployment
      name: spring-cloud-b-gray
   jobs:
   - name: "scale-down"
     schedule: "0 0/5 * * * *"
     targetSize: 1
   - name: "scale-up"
     schedule: "10 0/5 * * * *"
     targetSize: 2
---
apiVersion: autoscaling.alibabacloud.com/v1beta1
kind: CronHorizontalPodAutoscaler
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: spring-cloud-c
spec:
   scaleTargetRef:
      apiVersion: apps/v1beta2
      kind: Deployment
      name: spring-cloud-c
   jobs:
   - name: "scale-down"
     schedule: "0 2/5 * * * *"
     targetSize: 1 
   - name: "scale-up"
     schedule: "10 2/5 * * * *"
     targetSize: 2
# zuul 閘道器開啟 SLB 暴露展示頁面   
---     
apiVersion: v1
kind: Service
metadata:
  name: zuul-slb
spec:
  ports:
    - port: 80
      protocol: TCP
      targetPort: 20000
  selector:
    app: spring-cloud-zuul
  type: ClusterIP
# a 應用暴露 k8s service
---
apiVersion: v1
kind: Service
metadata:
  name: spring-cloud-a-base
spec:
  ports:
    - name: http
      port: 20001
      protocol: TCP
      targetPort: 20001
  selector:
    app: spring-cloud-a
---
apiVersion: v1
kind: Service
metadata:
  name: spring-cloud-a-gray
spec:
  ports:
    - name: http
      port: 20001
      protocol: TCP
      targetPort: 20001
  selector:
    app: spring-cloud-a-gray
# Nacos Server SLB Service 配置
---
apiVersion: v1
kind: Service
metadata:
  name: nacos-slb
spec:
  ports:
  - port: 8848
    protocol: TCP
    targetPort: 8848
  selector:
    app: nacos-server
  type: LoadBalancer

結果驗證一:無損下線功能

由於我們對 spring-cloud-b 跟 spring-cloud-b-gray 應用均開啟了定時 HPA,模擬每 5 分鐘進行一次定時的擴縮容。

在這裡插入圖片描述

在這裡插入圖片描述

登入 MSE 控制檯,進入微服務治理中心->應用列表->spring-cloud-a->應用詳情,從應用監控曲線,我們可以看到 spring-cloud-a 應用的流量資料:
在這裡插入圖片描述

gray 版本的流量在 pod 擴縮容的過程中請求錯誤數為 0,無流量損失。未打標的版本由於關閉了無損下線功能,在 pod 擴縮容的過程中有 20 個從 spring-cloud-a 發到 spring-cloud-b 的請求出現報錯,發生了請求流量損耗。

結果驗證二:服務預熱功能

我們在 spring-cloud-c 應用開啟了定時 HPA 模擬應用上線過程,每隔 5 分鐘做一次伸縮,在擴縮容週期內第 2 分鐘第 0 秒縮容到 1 個節點,第 2 分鐘第 10 秒擴容到 2 個節點。
在這裡插入圖片描述

在預熱應用的消費端 spring-cloud-b 開啟服務預熱功能。

在這裡插入圖片描述

在預熱應用的服務提供端 spring-cloud-c 開啟服務預熱功能。預熱時長配置為 120 秒。

在這裡插入圖片描述

觀察節點的流量,發現節點流量緩慢上升。並且能看到節點的預熱開始和結束時間,以及相關的事件。

在這裡插入圖片描述

從上圖可以看到開啟預熱功能的應用重啟後的流量會隨時間緩慢增加,在一些應用啟動過程中需要預建連線池和快取等資源的慢啟動場景,開啟服務預熱能有效保護應用啟動過程中快取資源有序建立保障應用安全啟動從而實現應用上線的流量無損。

方案介紹 & 實操

更多方案設計細節,請觀看微服務應用如何實現無損上下線主題直播[3]視訊回放:
https://yqh.aliyun.com/live/d...

相關連結

[1]建立 Kubernetes 託管版叢集https://help.aliyun.com/docum...
[2]開通 MSE 微服務治理
https://help.aliyun.com/docum...
[3]無損上下線主題直播
https://yqh.aliyun.com/live/d...

相關文章