微服務中的灰度發布(又稱為金絲雀發布)是一種持續部署策略,它允許在正式環境的小部分用戶群體上先部署新版本的應用程序或服務,而不是一次性對所有用戶同時發布全新的版本。
這種方式有助于在生產環境中逐步驗證新版本的穩定性和兼容性,同時最小化潛在風險,不影響大部分用戶的正常使用。
在 Spring Cloud 微服務架構中,實現灰度發布通常涉及到以下幾個方面:
根據一定的策略(如用戶 ID、請求頭信息、IP 地址等)將流入的請求分配給不同版本的服務實例。
使用 Spring Cloud Gateway、Zuul 等 API 網關組件實現路由規則,將部分請求定向至新版本的服務節點。
新版本服務啟動時會注冊帶有特定版本標簽的服務實例到服務注冊中心(如 Eureka 或 Nacos)。
請求在路由時可以根據版本標簽選擇相應版本的服務實例。
監控與評估:
在灰度發布的階段,運維團隊會對新版本服務的性能、穩定性以及用戶體驗等方面進行實時監控和評估。
如果新版本表現良好,則可以逐漸擴大灰度范圍直至全面替換舊版本。
故障恢復與回滾:若新版本出現問題,可通過快速撤銷灰度發布策略,使所有流量恢復到舊版本服務,實現快速回滾,確保服務整體可用性。
通過 Spring Cloud 的擴展組件和自定義路由策略,開發人員可以輕松實現灰度發布功能,確保在微服務架構中安全、平滑地進行版本迭代升級。
灰色發布的常見實現思路有以下幾種:
而在生產環境中,比較常用的是根據用戶標識來實現灰色發布,也就是說先讓一小部分用戶體驗新功能,以發現新服務中可能存在的某種缺陷或不足。
Spring Cloud 全鏈路灰色發布的關鍵實現思路如下圖所示:
圖片
灰度發布的具體實現步驟如下:
在負載均衡器 Spring Cloud LoadBalancer 中,判斷灰度發布標簽,將請求分發到對應服務。
將灰度發布標簽(如果存在),繼續傳遞給下一個調用的服務。
經過第四步的反復傳遞之后,整個 Spring Cloud 全鏈路的灰度發布就完成了。
在灰度發布的執行流程中,有一個核心的問題,如果在 Spring Cloud LoadBalancer 進行服務調用時,區分正式服務和灰度服務呢?
這個問題的解決方案是:在灰度服務既注冊中心的 MetaData(元數據)中標識自己為灰度服務即可,而元數據中沒有標識(灰度服務)的則為正式服務,以 Nacos 為例,它的設置如下:
spring: application: name: gray-user-service cloud: nacos: discovery: username: nacos password: nacos server-addr: localhost:8848 namespace: public register-enabled: true metadata: { "gray-tag":"true" } # 標識自己為灰度服務
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 中有灰度標簽,則只查詢灰度服務的節點實例,否則則查詢出所有的正式節點實例(以供服務調用或服務轉發)。
要在網關 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;@Componentpublic 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); }}
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;@Componentpublic 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; }}
本文鏈接:http://www.tebozhan.com/showinfo-26-88919-0.html微服務如何灰度發布?你會嗎?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com