SpringCloud全鏈路灰色釋出具體實現!

2023-11-13 12:00:29

灰度釋出(Gray Release,也稱為灰度釋出或金絲雀釋出)是指在軟體或服務釋出過程中,將新版本的功能或服務以較小的比例引入到生產環境中,僅向部分使用者或節點提供新功能的一種釋出策略。

在傳統的全量釋出中,新版本的功能會一次性全部部署到所有的使用者或節點上。然而,這種方式潛在的風險是,如果新版本存在缺陷或問題,可能會對所有使用者或節點產生嚴重的影響,導致系統崩潰或服務不可用。

相比之下,灰度釋出採用較小的規模,並逐步將新版本的功能引入到生產環境中,僅向一小部分使用者或節點提供新功能。通過持續監測和評估,可以在發現問題時及時回滾或修復。這種逐步引入新版本的方式可以降低風險,並提高系統的穩定性和可靠性。

1.實現思路

灰色釋出的常見實現思路有以下幾種:

  • 根據使用者劃分:根據使用者標識或使用者組進行劃分,在整個使用者群體中只選擇一小部分使用者獲得新功能。
  • 根據地域劃分:在不同地區或不同節點上進行劃分,在其中的一小部分地區或節點進行新功能的釋出。
  • 根據流量劃分:根據流量的百分比或請求次數進行劃分,只將一部分請求流量引導到新功能上。

而在生產環境中,比較常用的是根據使用者標識來實現灰色釋出,也就是說先讓一小部分使用者體驗新功能,以發現新服務中可能存在的某種缺陷或不足。

2.具體實現

Spring Cloud 全鏈路灰色釋出的關鍵實現思路如下圖所示:

灰度釋出的具體實現步驟如下:

  1. 前端程式在灰度測試的使用者 Header 頭中打上標籤,例如在 Header 中新增「grap-tag: true」,其表示要進行灰常測試(存取灰度服務),而其他則為存取正式服務。
  2. 在負載均衡器 Spring Cloud LoadBalancer 中,拿到 Header 中的「grap-tag」進行判斷,如果此標籤不為空,並等於「true」的話,表示要存取灰度釋出的服務,否則只存取正式的服務。
  3. 在閘道器 Spring Cloud Gateway 中,將 Header 標籤「grap-tag: true」繼續往下一個呼叫服務中傳遞。
  4. 在後續的呼叫服務中,需要實現以下兩個關鍵功能:
    1. 在負載均衡器 Spring Cloud LoadBalancer 中,判斷灰度釋出標籤,將請求分發到對應服務。
    2. 將灰度釋出標籤(如果存在),繼續傳遞給下一個呼叫的服務。

經過第四步的反覆傳遞之後,整個 Spring Cloud 全鏈路的灰度釋出就完成了。

3.核心實現思路和程式碼

灰度釋出的關鍵實現技術和程式碼如下。

3.1 區分正式服務和灰度服務

在灰度釋出的執行流程中,有一個核心的問題,如果在 Spring Cloud LoadBalancer 進行服務呼叫時,區分正式服務和灰度服務呢?

這個問題的解決方案是:在灰度服務既註冊中心的 MetaData(後設資料)中標識自己為灰度服務即可,而後設資料中沒有標識(灰度服務)的則為正式服務,以 Nacos 為例,它的設定如下:

spring:
  application:
    name: canary-user-service
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: localhost:8848
        namespace: public
        register-enabled: true 
        metadata: { "grap-tag":"true" } # 標識自己為灰度服務

3.2 負載均衡呼叫灰度服務

Spring Cloud LoadBalancer 判斷並呼叫灰度服務的關鍵實現程式碼如下:

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,
                                                          Request request) {
        // 範例為空
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }
            return new EmptyResponse();
        } else { // 服務不為空
            RequestDataContext dataContext = (RequestDataContext) request.getContext();
            HttpHeaders headers = dataContext.getClientRequest().getHeaders();
            // 判斷是否為灰度釋出(請求)
            if (headers.get(GlobalVariables.GRAY_KEY) != null &&
                    headers.get(GlobalVariables.GRAY_KEY).get(0).equals("true")) {
                // 灰度釋出請求,得到新服務範例列表
                List<ServiceInstance> findInstances = instances.stream().
                        filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) != null &&
                                s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
                        .toList();
                if (findInstances.size() > 0) { // 存在灰度釋出節點
                    instances = findInstances;
                }
            } else { // 查詢非灰度釋出節點
                // 灰度釋出測試請求,得到新服務範例列表
                instances = instances.stream().
                        filter(s -> s.getMetadata().get(GlobalVariables.GRAY_KEY) == null ||
                                !s.getMetadata().get(GlobalVariables.GRAY_KEY).equals("true"))
                        .toList();
            }
            // 隨機正數值 ++i( & 去負數)
            int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
            // ++i 數值 % 範例數 取模 -> 輪詢演演算法
            int index = pos % instances.size();
            // 得到服務實體方法
            ServiceInstance instance = (ServiceInstance) instances.get(index);
            return new DefaultResponse(instance);
        }
    }

以上程式碼為自定義負載均衡器,並使用了輪詢演演算法。如果 Header 中有灰度標籤,則只查詢灰度服務的節點範例,否則則查詢出所有的正式節點範例(以供服務呼叫或服務轉發)。

3.3 閘道器傳遞灰度標識

要在閘道器 Spring Cloud Gateway 中傳遞灰度標識,只需要在 Gateway 的全域性自定義過濾器中設定 Response 的 Header 即可,具體實現程式碼如下:

package com.example.gateway.config;

import com.loadbalancer.canary.common.GlobalVariables;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class LoadBalancerFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 得到 request、response 物件
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        if (request.getQueryParams().getFirst(GlobalVariables.GRAY_KEY) != null) {
            // 設定金絲雀標識
            response.getHeaders().set(GlobalVariables.GRAY_KEY,
                    "true");
        }
        // 此步驟正常,執行下一步
        return chain.filter(exchange);
    }
}

3.4 Openfeign 傳遞灰度標籤

HTTP 呼叫工具 Openfeign 傳遞灰度標籤的實現程式碼如下:

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 從 RequestContextHolder 中獲取 HttpServletRequest
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        // 獲取 RequestContextHolder 中的資訊
        Map<String, String> headers = getHeaders(attributes.getRequest());
        // 放入 openfeign 的 RequestTemplate 中
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            template.header(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 獲取原請求頭
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        if (enumeration != null) {
            while (enumeration.hasMoreElements()) {
                String key = enumeration.nextElement();
                String value = request.getHeader(key);
                map.put(key, value);
            }
        }
        return map;
    }
}

小結

灰度釋出是微服務時代保證生產環境安全的必備措施,而其關鍵實現思路是:

1、註冊中心區分正常服務和灰度服務;

2、負載均衡正確轉發正常服務和灰度服務;

3、閘道器和 HTTP 工具傳遞灰度標籤。

這樣,我們就完整的實現 Spring Cloud 全鏈路灰度釋出功能了。

本文已收錄到我的面試小站 www.javacn.site,其中包含的內容有:Redis、JVM、並行、並行、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、訊息佇列等模組。