基於騰訊雲微服務引擎(TSE) ,輕鬆實現雲上全鏈路灰度釋出

語言: CN / TW / HK

作者:劉遠

1.  概述

軟體開發過程中,應用釋出非常頻繁,通常情況下,開發或運維人員會將系統裡所有服務同時上線,使得所有使用者都使用上新版本。這樣的操作時常會導致釋出失敗,或因釋出前修改程式碼,線上出現 Bug。

假設一個線上商城,每天都有大量的使用者訪問,如果直接在所有使用者中部署新版本應用,一旦出現問題,所有使用者都可能受到影響。相比之下,通過引入灰度釋出策略,先將新版本的應用部署到少量的使用者中,檢查是否存在問題,如果沒有,再逐步擴充套件到更多的使用者中,由此解決全量釋出的各種弊端。

灰度釋出是一種軟體釋出策略,它允許你在生產環境中漸進式部署應用,新版本只對部分使用者可見,在問題出現時儘量減少影響。在微服務體系架構中,服務間的依賴關係錯綜複雜,有時單個服務發版依賴多個服務同時執行聯動,對這個新版本服務的上下游進行分組驗證,這是微服務架構中特有的全鏈路灰度釋出場景。

使用騰訊雲微服務引擎 TSE 提供的閘道器和服務治理能力,可以在不修改任何業務程式碼的情況下,視覺化配置灰度規則,實現雲上輕鬆易用的全鏈路灰度釋出。

圖1-1 全鏈路灰度場景架構

接下來演示雲原生 API 閘道器+北極星網格構建的全鏈路灰度釋出能力。下圖模擬的雲書城應用,由4個後端的微服務組成,採用 Spring Boot + Dubbo 實現,呼叫鏈包含:收藏功能、購買功能,使用者管理功能和訂單功能。使用者通過前端頁面訪問書城,進行內容瀏覽和書籍下單。

圖1-2 雲書城前端頁面

2.  環境

2.1 雲元件版本

本次實踐採用如下元件:

● TSE-雲原生閘道器:v2.5.1

● TSE-北極星網格: v1.12.0.1

● TKE:v1.20

● APM-SkyWalking: v8.5.0

我們將應用部署在騰訊雲 TKE 叢集中,在實際生產中,全鏈路灰度對於應用的部署模式沒有限制性要求,無論是 CVM 虛擬機器,還是自建容器環境,都能應用此方案。

2.2 灰度服務準備

專案模擬書城收藏服務改版,對 addUserFavoriteBook 介面進行修改:當前基線版本點選【收藏】按鈕後,僅顯示成功收藏字樣,程式碼示例如下:

public Response<String> addUserFavoriteBook(Long userId, Long isbn) {
    Response<String> resp = new Response<String>(ResponseCode.SUCCESS);
    try {
        FavoritesInfo entity = new FavoritesInfo(userId, isbn);
        if (favoritesPersistCmpt.favoriteExist(entity)) {
            resp.setMsg("已收藏(version:1.0.0)");
            return resp;
        }

        favoritesPersistCmpt.addUserFavorite(entity);
        resp.setMsg("收藏成功");
        BookInfoDto dto = storeClient.getBookInfoByIsbn(isbn);
        cacheCmpt.cashUserFavoriteBook(userId, dto);
    } catch (Exception e) {
        logger.error("failed to add FavoritesInfo", e);
        resp.setFailue("服務異常,請稍後重試!");
    }
    return resp;
}

灰度版本修改後,頁面點選【收藏】,會詳細顯示使用者 ID 及書籍 ID,程式碼示例如下:

public Response<String> addUserFavoriteBook(Long userId, Long isbn) {
        Response<String> resp = new Response<String>(ResponseCode.SUCCESS);
        try {
            FavoritesInfo entity = new FavoritesInfo(userId, isbn);
            if (favoritesPersistCmpt.favoriteExist(entity)) {
                resp.setMsg("已收藏(version:2.0.0)");
                return resp;
            }
            favoritesPersistCmpt.addUserFavorite(entity);
            resp.setMsg("使用者 userId = " + userId + " 成功收藏 book isbn = " + isbn);
            BookInfoDto dto = storeClient.getBookInfoByIsbn(isbn);
            cacheCmpt.cashUserFavoriteBook(userId, dto);
        } catch (Exception e) {
            logger.error("failed to add FavoritesInfo", e);
            resp.setFailue("服務異常,請稍後重試!");
        }
        return resp;
    }

為了方便檢視全鏈路服務當前版本,各服務將應用版本號回傳給前端,在前端頁面上顯示。

圖2-1 基線版本收藏服務

圖2-2 灰度版本收藏服務

2.3 北極星網格接入

雲書城架構中,服務發現能力目前是通過 Nacos 實現,在全鏈路灰度釋出中,服務間需要使用到治理能力,我們採用北極星網格對註冊發現功能進行替換。專案選擇 Polaris-Dubbo 框架方式接入,通過更新北極星程式碼依賴,無需修改程式碼即可完成。對比原專案,有以下幾點變化:

● 修改 POM.XML 檔案,增加北極星-Dubbo 服務註冊、服務熔斷及服務路由依賴包。

//服務註冊外掛
<dependency>
	<groupId>com.tencent.polaris</groupId>
	<artifactId>dubbo-registry-polaris</artifactId>
	<version>${polaris.version}</version>
</dependency>

//服務熔斷外掛
<dependency>
	<groupId>com.tencent.polaris</groupId>
	<artifactId>dubbo-circuitbreaker-polaris</artifactId>
	<version>${polaris.version}</version>
</dependency>

//服務路由外掛
<dependency>
	<groupId>com.tencent.polaris</groupId>
	<artifactId>dubbo-router-polaris</artifactId>
	<version>${polaris.version}</version>
</dependency>

● 在配置檔案中,修改 Dubbo 應用註冊中心接入地址。

dubbo.registry.address=polaris://x.x.x.x:8091?username=polaris&password=*****

修改後的專案,程式碼保持 Dubbo 標準方式進行註冊及呼叫,無需變更。

//註冊服務(服務端)
@DubboService(version = "${provicer.service.version}")
public class ProviderServiceImpl implements ProviderService {}

//服務呼叫(消費端)
@DubboReference(version = "1.0.0")
private ProviderService providerService;

2.4 容器服務部署

完成上述修改後,對微服務應用重新編譯打包,推送至映象倉庫。在 TKE 叢集中,我們以 Deployment 方式下發應用。其中,收藏服務將基線版本和灰度版本都部署在叢集中,其他服務僅部署一個版本,使用服務治理能力進行流量路由。

● 基線版本收藏服務 YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: favorites-service
  namespace: qcbm
  labels:
    app: favorites-service
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: favorites-service
      version: v1
  template:
    metadata:
      labels:
        app: favorites-service
        version: v1
    spec:
      containers:
        - name: favorites-service
          image: ccr.ccs.tencentyun.com/qcbm/favorites-service-polaris
          env:
            - name: MYSQL_HOST
              valueFrom:
                configMapKeyRef:
                  key: MYSQL_HOST
                  name: qcbm-env
                  optional: false
            - name: REDIS_HOST
              valueFrom:
                configMapKeyRef:
                  key: REDIS_HOST
                  name: qcbm-env
                  optional: false
            - name: MYSQL_ACCOUNT
              valueFrom:
                secretKeyRef:
                  key: MYSQL_ACCOUNT
                  name: qcbm-keys
                  optional: false
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: MYSQL_PASSWORD
                  name: qcbm-keys
                  optional: false
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: REDIS_PASSWORD
                  name: qcbm-keys
                  optional: false
          ports:
            - containerPort: 20880
              protocol: TCP

● 灰度版本收藏服務YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: favorites-service-new
  namespace: qcbm
  labels:
    app: favorites-service-new
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: favorites-service-new
      version: v1
  template:
    metadata:
      labels:
        app: favorites-service-new
        version: v1
    spec:
      containers:
        - name: favorites-service-new
          image: ccr.ccs.tencentyun.com/qcbm/favorites-service-new-polaris
          env:
            - name: MYSQL_HOST
              valueFrom:
                configMapKeyRef:
                  key: MYSQL_HOST
                  name: qcbm-env
                  optional: false
            - name: REDIS_HOST
              valueFrom:
                configMapKeyRef:
                  key: REDIS_HOST
                  name: qcbm-env
                  optional: false
            - name: MYSQL_ACCOUNT
              valueFrom:
                secretKeyRef:
                  key: MYSQL_ACCOUNT
                  name: qcbm-keys
                  optional: false
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: MYSQL_PASSWORD
                  name: qcbm-keys
                  optional: false
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: REDIS_PASSWORD
                  name: qcbm-keys
                  optional: false
          ports:
            - containerPort: 20880
              protocol: TCP

● 前端頁面服務以 Service 方式部署,通過 Ingress 模式對接雲原生閘道器,提供叢集外部訪問能力

apiVersion: v1
kind: Service
metadata:
  name: qcbm-front
  namespace: qcbm
spec:
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
  selector:
    app: qcbm-front
    version: v1
  type: NodePort

2.5 雲原生閘道器接入

雲原生閘道器支援將流量直通到 Service 所在的 Pod,無需通過 NodePort 中轉。在控制檯裡繫結 TKE 叢集,輸入 Service 名,閘道器通過 Endpoint 裡收集 Pod IP,在閘道器裡自動生成 Kong Services 和 Upstream。一旦 TKE Service 發生變化,Ingress Controller 會動態更新 Upstream 裡的 Target 資訊。

後續操作基於 Kong 裡自動生成的 Services,配置基線及灰度閘道器路由規則。

圖2-3 雲原生閘道器繫結TKE叢集服務

圖2-4 雲原生閘道器自動生成 Services

圖2-5 雲原生閘道器自動生成 Upstreams

2.6 鏈路追蹤接入

單體系統時代追蹤的範圍只侷限於棧追蹤,而在微服務環境中,追蹤不只限於呼叫棧,一個外部請求需要內部若干服務的聯動,完整的一次請求會跨越多個服務。鏈路追蹤的主要目的是排查故障,如當前問題點處於呼叫鏈的哪一部分,各服務間輸入輸出是否符合預期,通過鏈路追蹤,可以檢視到服務間的網路傳輸資訊,以及各服務內部的呼叫堆疊資訊。

採用 APM 的 SkyWalking 協議方式進行上報,首先修改 SkyWalking 資料夾裡的 agent.config 檔案,配置接入點、Token 、自定義空間和服務名稱。

collector.backend_service=x.x.x.x:11800
agent.authentication=xxxxxx
agent.service_name=favorites-service-new
agent.namespace=QCBM

在 Dockerfile 中,修改應用程式的啟動命令列,以 JavaAgent 方式指定 SkyWalking Agent 的路徑 :

java -javaagent:/app/skywalking/skywalking-agent.jar -jar favorites-service-new.jar

部署後,可以在控制檯裡驗證應用拓撲正確性。

圖2-6 應用拓撲圖

3.  解決方案

通過四個階段的操作,實現收藏服務的全鏈路灰度釋出,分別是例項打標、閘道器路由、微服務路和標籤透傳。

圖3-1 全鏈路灰度釋出方案

3.1 例項打標及標籤透傳

例項打標,指的是通過例項標籤標識不同的應用,將基線版本與灰度版本區分開。一般有兩種方式進行例項打標:一是框架自動同步,將應用名,環境變數等做為例項標籤;二是用 K8S 部署時的 CRD Label 作為例項標籤。本實踐中使用 Dubbo 框架裡的 applicaiton 欄位來區分基線版本和灰度版本應用。

圖3-2 閘道器路由規則

閘道器層對灰度流量進行了染色,在微服務呼叫過程中,需要將染色標籤在每一跳進行傳遞,使得各微服務都可以識別到灰度流量,並進行正確路由處理。

圖3-3 標籤透傳示意圖

外部染色標籤在入口處,以 HTTP Header 方式存在,在 Dubbo-Gateway 服務處,編碼將 HTTP Header 轉化為 Dubbo attachment,使得染色標籤在微服務內部中繼續透傳,最終根據 attachment 裡的取值做服務間呼叫依據。

private FavoriteService add(FavoriteService favoriteService, String result) {
        logger.info("header:{}", result);
        RpcContext.getContext().setAttachment("gray", result == null ? "false" : result);
        return favoriteService;
    }

3.2 閘道器路由

閘道器作為系統流量入口,負責將外部流量按照一定的使用者特徵,切分流入灰度版本和基線版本。並對灰度流量進行染色打標,供服務治理中心動態路由規則匹配使用。在實際生產中,一般有三種分流的方法:

● 通過匹配使用者請求的 Header 引數,進行流量區分。

● 通過匹配使用者請求的 Host 特徵,進行流量區分。

● 通過流量百分比進行區分,使用雲原生閘道器能力,將其中一部分流量進行染色處理。

本次實踐針對前兩種切分方式進行介紹。

圖3-4 閘道器路由示意圖

3.3 微服務路由

北極星網格在全鏈路灰度中,充當服務治理中心的角色,解決架構中註冊發現、故障容錯、流量控制和安全問題。通過北極星網格控制檯中的配置,把基線和灰度請求,路由到不同的例項分組上,並將灰度請求固定在灰度版本服務間進行傳遞和處理。

圖3-5 動態路由示意圖

我們建立了2條服務間動態路由規則,基線和灰度請求按照不同匹配規則,路由至對應例項分組。實現中,北極星基於請求訊息內容來對請求匹配,並根據優先順序進行流量排程。

圖3-6 治理中心路由規則

4.  場景

4.1 通過Header特徵全鏈路灰度

1)場景說明

如果客戶端訪問希望統一域名,比如實踐中的 gray.qcbm.yunnative.com,我們可以通過傳入不同的 Header,把請求分別路由到基線和灰度環境。當生產環境中存在多個客戶分組,或多條灰度路由規則,也可以通過雲原生閘道器進行自定義 Header 染色,使用不同染色標籤,進行服務間路由搭配。

圖4-1 通過Header特徵全鏈路灰度

2)配置方法

在雲原生閘道器上建立兩條路由規則:

● qcbm-front-router-web,HOST為gray.qcbm.yunnative.com,Header為app:web,路由到Dubbo-Gateway服務

● qcbm-front-router-mobile,HOST為gray.qcbm.yunnative.com,Header為app:mobile,路由到Dubbo-Gateway服務,開啟染色(gray:true)

圖4-2 雲原生閘道器路由規則

服務治理中心可以直接使用現成的 app:web 或 app:mobile 標籤路由,也可以對路由請求新增染色,使用染色標籤路由,優化複雜環境管理。這裡我們開啟雲原生閘道器的 Request-Transformer 外掛,對 qcbm-front-router-mobile 路由報文進行修改,新增 gray:true 頭,使用該染色標識進行路由管理。

圖4-3 路由染色外掛

圖4-4 新增染色標識

qcbm-front-router-mobile 路由規則的請求到達 Dubbo-Gateway 後,一旦需要訪問收藏服務(FavoriteService),gray:true 染色標籤會命中北極星網格灰度服務路由,選擇呼叫 remote.application為favorites-service-new 的服務例項。此例項分組為我們部署的 favorites-service-new 灰度版本 deployment。

圖4-5 灰度服務路由

qcbm-front-router-web 路由規則的請求會命中無染色標籤的基線服務路由,呼叫 remote.application 為 favorites-service 的服務例項。此例項分組為我們部署的 favorites-service 基線版本 deployment。

圖4-6 基線服務路由

3)結果驗證

我們借用 chrome 瀏覽器外掛 ModHeader,對訪問請求按需新增 Header。

● 當新增 app:mobile 後,瀏覽器訪問 gray.qcbm.yunnative.com,我們的訪問鏈路如下:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service-New](灰度)

頁面顯示如下:

圖4-7 灰度請求頁面

同時,也可以通過鏈路監控觀察到,gateway-service(基線服務)正確的請求到 favorite-service-new(灰度服務),同時 favorite-service-new 正確請求到 store-service(基線服務):

圖4-8 灰度請求鏈路詳情

● 當新增 app:web 後,瀏覽器訪問 gray.qcbm.yunnative.com,此時我們的訪問鏈路如下:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service](基線)

頁面顯示如下:

圖4-9 基線請求頁面

通過鏈路監控,可以觀察到,gateway-service(基線服務)正確的請求到 favorite-service(基線服務),同時 favorite-service 正確請求到 store-service(基線服務):

圖4-10 基線請求鏈路詳情

在北極星網格中,我們可以針對鏈路的每一跳配置路由規則,每個主調服務都可以定義屬於自己的匹配規則。

4.2 通過域名特徵全鏈路灰度

1)場景說明

同樣的,也可以採用域名對請求進行區分,預期 web 端使用者採用 gray.web.yunnative.com 訪問基線環境;mobile 端使用者採用 gray.mobile.yunnative.com 訪問灰度環境。這種分流方式,適用於閘道器根據使用者登入資訊,動態分流的場景,不同的使用者在登入時,登入模組根據驗證資訊,返回302報文,給予不同的重定向域名,使用者此時使用不同的域名去訪問,雲原生閘道器通過 HOST 來做流量區分,動態染色 HTTP 請求。

圖4-11 通過域名特徵全鏈路灰度

2)配置方法

在雲原生閘道器上建立兩條路由規則:

● qcbm-front-router-web,HOST 為 gray.web.yunnative.com,路由到 Dubbo-Gateway 服務。

● qcbm-front-router-mobile,HOST 為 gray.mobile.yunnative.com,路由到 Dubbo-Gateway 服務,開啟染色(gray:true)。

和場景1類似,qcbm-front-router-mobile 路由規則的請求到達 Dubbo-Gateway 後,一旦訪問收藏服務(FavoriteService),gray:true 染色標籤會命中北極星網格灰度路由,呼叫 remote.application 為 favorites-service-new 的例項分組;而 qcbm-front-router-web 路由規則的請求會命中無染色標籤的網格基線路由,呼叫 remote.application 為 favorites-service 的例項分組,訪問基線環境。

3)結果驗證

● 瀏覽器訪問gray.mobile.yunnative.com時,染色標籤會被打上,此時訪問鏈路如下:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service-New](灰度)

頁面顯示如下:

圖4-12 灰度請求頁面

同時,也可以通過鏈路監控觀察到,gateway-service(基線服務)正確的請求到 favorite-service-new(灰度服務),同時 favorite-service-new 正確請求到 store-service(基線服務):

圖4-13 灰度請求鏈路詳情

● 當訪問gray.web.yunnative.com時,無染色標籤,此時我們的鏈路如下:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service](基線)

頁面顯示如下:

圖4-14 灰度請求頁面

通過鏈路監控,可以觀察到,gateway-service(基線服務)正確的請求到 favorite-service(基線服務),同時 favorite-service 正確請求到 store-service(基線服務):

圖4-15 基線請求鏈路詳情

4.3 灰度服務故障轉移

1)場景說明

在灰度釋出過程中,可以通過監測系統性能和使用者反饋來評估新功能的質量。如果新功能在測試期間表現良好,可以繼續將其推向更多使用者,替換原版本應用。如果出現任何問題,可以對灰度服務進行訪問熔斷處理,及時修復問題,然後繼續灰度測試。

圖4-16 灰度服務故障轉移

2)配置方法

在北極星網格上配置熔斷規則,配合多例項分組路由規則,實現灰度服務故障 Failover。在全鏈路灰度場景基礎上,在北極星網格控制檯加上一條熔斷規則。

● 當 delUserFavoriteBook 介面錯誤率大於10%,閾值大於10個,熔斷 Favorite-Service-New 灰度服務例項分組,同一 Deployment 裡的所有 Pod 進入半開狀態。

● 半開時間60秒,期間一旦請求成功則認為服務恢復,關閉熔斷。

圖4-17 灰度服務熔斷規則

接下來,在網格灰度路由中,新增低優先順序例項分組,該分組為基線例項。一旦灰度例項分組被熔斷,請求會去訪問基線例項分組,直到灰度服務修復,熔斷關閉。

圖4-18 灰度服務路由規則

3)結果驗證

部署一個新的“故障“收藏服務,Dubbo 程式延用 application=favorites-service-new 標籤(為區分應用,這裡故障灰度服務命名為 Favorites-Service-New-Bad),保證原路由規則可用。該“故障”程式修改了收藏服務的 delUserFavoriteBook 介面程式碼,當訪問時直接丟擲異常,模擬服務故障。程式碼如下所示:

public Response<String> delUserFavoriteBook(Long userId, Long isbn) {
        String hostAddress;
        try {
            hostAddress = InetAddress.getLocalHost().getHostAddress();
        } catch (Exception e) {
            hostAddress = "ip獲取失敗";
        }
        throw new RuntimeException("刪除收藏-故障 ip:" + hostAddress);
}

瀏覽器訪問 gray.mobile.yunnative.com 時,此時訪問鏈路如下:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service-New-Bad](故障灰度)

頁面顯示如下:

圖4-19 灰度請求頁面

進入收藏頁面,點選【刪除】,程式報錯,顯示呼叫異常。

圖4-20 灰度服務刪除報錯

通過鏈路追蹤,也可以檢視到服務異常。

圖4-21 應用呼叫拓撲

圖4-22 灰度服務刪除鏈路錯誤

當故障錯誤大於10次,favorite-service-new 灰度例項分組被熔斷,灰度路由進行低優先順序目標選擇,流量回源至基線例項分組favorite-service,此時測試刪除功能正常,因為此時我們的訪問鏈路重新變為:

[雲原生閘道器] --> [Dubbo-Gateway] --> [Favorite-Service](基線正常)

頁面顯示如下,服務呼叫已回源:

圖4-22 灰度請求頁面(已回源)

5.  總結

在灰度釋出實施前,需要按照如下三方面,對整體流程進行計劃:

● 目標:在開始灰度釋出之前,需要了解發布的目標是什麼,比如說是測試新版本的效能,還是功能相容性等,以便在灰度時進行對應的測試和觀測。

● 灰度策略:有很多種灰度策略可供選擇,例如按使用者特徵來灰度、按流量來灰度、按地域來灰度等。通過系統使用者的特點,選擇最合適的灰度策略。

● 灰度範圍:在灰度釋出過程中,應當能隨時控制哪些使用者可以訪問新版本,當出現問題時,能將請求快速回滾到舊版本上。

灰度釋出過程中,確認流量是否已經按計劃切換到灰度例項分組,通過監控和日誌,檢查各服務是否正常執行,是否符合預期。

確定本次釋出成功後,可以依次對老版本分組的例項進行滾動升級,多次升級完成灰度釋出,一旦出現錯誤執行回退,有序控制釋出節奏。最後,根據實際應用情況,刪除或保留閘道器和治理中心的動態路由規則。

騰訊雲TSE提供了完整的全鏈路灰度釋出解決方案,適用各種釋出流程,無需侵入程式碼,通過視覺化配置灰度規則,有效地解決了微服務全鏈路灰度釋出難實現的問題,讓灰度釋出更便捷、更順利。