基於騰訊雲微服務引擎(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提供了完整的全鏈路灰度發佈解決方案,適用各種發佈流程,無需侵入代碼,通過可視化配置灰度規則,有效地解決了微服務全鏈路灰度發佈難實現的問題,讓灰度發佈更便捷、更順利。