灰度發布(Gray Release,也稱為灰度發布或金絲雀發布)是指在軟件或服務發布過程中,將新版本的功能或服務以較小的比例引入到生產環境中,僅向部分用戶或節點提供新功能的一種發布策略。
在傳統的全量發布中,新版本的功能會一次性全部部署到所有的用戶或節點上。然而,這種方式潛在的風險是,如果新版本存在缺陷或問題,可能會對所有用戶或節點產生嚴重的影響,導致系統崩潰或服務不可用。
相比之下,灰度發布采用較小的規模,并逐步將新版本的功能引入到生產環境中,僅向一小部分用戶或節點提供新功能。通過持續監測和評估,可以在發現問題時及時回滾或修復。這種逐步引入新版本的方式可以降低風險,并提高系統的穩定性和可靠性。
1.實現思路
灰色發布的常見實現思路有以下幾種:
- 根據用戶劃分:根據用戶標識或用戶組進行劃分,在整個用戶群體中只選擇一小部分用戶獲得新功能。
- 根據地域劃分:在不同地區或不同節點上進行劃分,在其中的一小部分地區或節點進行新功能的發布。
- 根據流量劃分:根據流量的百分比或請求次數進行劃分,只將一部分請求流量引導到新功能上。
而在生產環境中,比較常用的是根據用戶標識來實現灰色發布,也就是說先讓一小部分用戶體驗新功能,以發現新服務中可能存在的某種缺陷或不足。
2.具體實現
Spring Cloud 全鏈路灰色發布的關鍵實現思路如下圖所示:
圖片
灰度發布的具體實現步驟如下:
- 前端程序在灰度測試的用戶 Header 頭中打上標簽,例如在 Header 中添加“grap-tag: true”,其表示要進行灰常測試(訪問灰度服務),而其他則為訪問正式服務。
- 在負載均衡器 Spring Cloud LoadBalancer 中,拿到 Header 中的“grap-tag”進行判斷,如果此標簽不為空,并等于“true”的話,表示要訪問灰度發布的服務,否則只訪問正式的服務。
- 在網關 Spring Cloud Gateway 中,將 Header 標簽“grap-tag: true”繼續往下一個調用服務中傳遞。
- 在后續的調用服務中,需要實現以下兩個關鍵功能:
- 在負載均衡器 Spring Cloud LoadBalancer 中,判斷灰度發布標簽,將請求分發到對應服務。
- 將灰度發布標簽(如果存在),繼續傳遞給下一個調用的服務。
經過第四步的反復傳遞之后,整個 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 全鏈路灰度發布功能了。