灰度發(fā)布又名金絲雀發(fā)布,在微服務(wù)中的表現(xiàn)為同一服務(wù)同時上線不同版本,讓一部分用戶使用新版本來驗證新特性,如果驗證沒有問題,則將所有用戶都遷移到新版本上。
在微服務(wù)架構(gòu)中,網(wǎng)關(guān)負責(zé)請求的統(tǒng)一入口,主要功能之一是請求路由。而灰度發(fā)布實質(zhì)就是讓指定用戶路由到指定版本的服務(wù)上。所以該功能可以在網(wǎng)關(guān)這一層實現(xiàn)。
今天就分享下Spring Cloud Gateway如何實現(xiàn)灰度發(fā)布。
1 Spring Cloud Gateway的路由邏輯
既然要讓指定用戶路由到指定服務(wù)版本,我們需要先了解Spring Cloud Gateway的路由邏輯。
Spring Cloud Gateway通過Predicate來匹配路由。
- id: user-route
uri: lb://user-login
predicates:
- Path=/user/**
上述路由規(guī)則表示只要請求URL符合/user/**則都會匹配到user-route這條路由規(guī)則中。(根據(jù)Predicate尋找路由匹配規(guī)則的源碼在RoutePredicateHandlerMapping#lookupRoute方法中)。
那么要實現(xiàn)灰度發(fā)布該怎么做?我們這里可以自己寫一個Predicate,來實現(xiàn)指定用戶匹配到指定的路由規(guī)則當(dāng)中。假設(shè)我們自己寫的Predicate叫HeaderUserNameRoutePredicateFactory(相應(yīng)源碼在文后),相應(yīng)的配置如下:
- id: user-route-gray
uri: lb://user-login
predicates:
- Path=/user/**
- HeaderUsername=Jack
上述路由規(guī)則表示請求URL符合/user/**并且請求的HTTP Header中的Username屬性值為Jack則會匹配到user-route-gray這條路由規(guī)則中。
實現(xiàn)了指定用戶匹配到指定規(guī)則只是第一步,下一步要實現(xiàn)的是如何讓指定用戶路由到指定版本的服務(wù)中,想要實現(xiàn)這一點,就需要先了解Spring Cloud Gateway的負載均衡邏輯,也就是Spring Cloud Gateway是如何選取要調(diào)用的服務(wù)的。
2 Spring Cloud Gateway的負載均衡邏輯
負載均衡的邏輯如下:
1、 從注冊中心獲取服務(wù)實例列表(實際實現(xiàn)中服務(wù)實例列表是后臺定時刷新緩存在內(nèi)存中的);
2、根據(jù)負載均衡算法從實例列表中選取服務(wù)。
在Spring Cloud Gateway中,相應(yīng)的代碼在ReactiveLoadBalancerClientFilter#choose方法中。
默認情況下,Spring Cloud Gateway負載均衡策略會從注冊中心所有服務(wù)實例中輪詢選擇一個服務(wù)實例。由此可以看出,默認實現(xiàn)無法滿足我們的需求,因為我們想要特定用戶路由到特定的服務(wù)版本上。
那么該如何解決呢?答案是重寫負載均衡算法,來實現(xiàn)選擇特定版本的服務(wù)實例功能。
3 版本號如何指定
灰度發(fā)布的目的是實現(xiàn)指定用戶訪問指定版本,用戶信息可以在HTTP Header中帶過來,那么版本號如何指定?
這里有兩種方案。
第一種方案也是通過請求的HTTP Header帶過來,缺點是需要客戶端修改;
第二種方案是在網(wǎng)關(guān)層修改請求,動態(tài)為請求加上版本號信息,此方案較好,對客戶端透明。
4 灰度發(fā)布的實現(xiàn)
看到這里,整個灰度發(fā)布的實現(xiàn)思路應(yīng)該比較清晰了。
1、首先編寫自己的Predicate,實現(xiàn)指定用戶匹配到指定的路由規(guī)則中;
2、動態(tài)修改請求,添加版本號信息,版本號信息可以放在HTTP Header中(此處可以通過原生AddRequestHeaderGatewayFilterFactory來實現(xiàn),無需自己寫代碼);
3、重寫負載均衡算法,根據(jù)版本號信息從注冊中心的服務(wù)實例上選擇相應(yīng)的服務(wù)版本進行請求的轉(zhuǎn)發(fā)。
思路如上,下面附上關(guān)鍵代碼:
自定義HeaderUsernameRoutePredicateFactory源碼如下:
@Component
public class HeaderUsernameRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderUsernameRoutePredicateFactory.Config> {
public static final String USERNAME = "Username";
public HeaderUsernameRoutePredicateFactory() {
super(Config.class);
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("username");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
List<String> usernames = config.getUsername();
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String username = serverWebExchange.getRequest().getHeaders().getFirst(USERNAME);
if (!StringUtils.isEmpty(username)) {
return usernames.contains(username);
}
return false;
}
@Override
public String toString() {
return String.format("Header: Username=%s", config.getUsername());
}
};
}
@NoArgsConstructor
@Getter
@Setter
@ToString
public static class Config {
private List<String> username;
}
}
自定義負載均衡算法GrayRoundRobinLoadBalancer如下:
@Slf4j
public class GrayRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final position = new AtomicInteger(new Random().nextInt(1000));
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final AtomicInteger position;
public GrayRoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(list -> getInstanceResponse(list, headers));
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
List<ServiceInstance> serviceInstances = instances.stream()
.filter(instance -> {
//根據(jù)請求頭中的版本號信息,選取注冊中心中的相應(yīng)服務(wù)實例
String version = headers.getFirst("Version");
if (version != null) {
return version.equals(instance.getMetadata().get("version"));
} else {
return true;
}
}).collect(Collectors.toList());
if (serviceInstances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = serviceInstances.get(pos % serviceInstances.size());
return new DefaultResponse(instance);
}
}
自定義GrayReactiveLoadBalancerClientFilter,調(diào)用自定義的負責(zé)均衡算法:
@Slf4j
@Component
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = (String) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
}
return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
throw NotFoundException.create(true, "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance) response.getServer(), overrideScheme);
URI requestUrl = this.reconstructURI(serviceInstance, uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
}).then(chain.filter(exchange));
} else {
return chain.filter(exchange);
}
}
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
GrayRoundRobinLoadBalancer loadBalancer = new GrayRoundRobinLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
return loadBalancer.choose(this.createRequest(exchange));
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
return new DefaultRequest<>(headers);
}
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
}
最后的路由規(guī)則配置如下,表示用戶Jack走V2版本,其他用戶走V1版本:
- id: user-route-gray
uri: grayLb://user-login
predicates:
- Path=/user/**
- HeaderUsername=Jack
filters:
- AddRequestHeader=Version,v2
- id: user-route
uri: grayLb://user-login
predicates:
- Path=/user/**
filters:
- AddRequestHeader=Version,v1
寫在最后
微服務(wù)中的灰度發(fā)布功能如上所述,相比實現(xiàn),思路是大家更需要關(guān)注的地方。思路清晰了,即使換個網(wǎng)關(guān)實現(xiàn),換個注冊中心實現(xiàn),都是一樣的。
灰度發(fā)布實質(zhì)是讓指定用戶訪問指定版本的服務(wù)。
所以首先需要指定用戶匹配到指定的路由規(guī)則。
其次,服務(wù)的版本號信息可以通過HTTP請求頭字段來指定。
最后,負載均衡算法需要能夠根據(jù)版本號信息來做服務(wù)實例的選擇。
轉(zhuǎn)自:公眾號-WU雙文章來源:http://www.zghlxwxcb.cn/news/detail-832905.html
鏈接:https://blog.csdn.net/sslulu520/article/details/118528193文章來源地址http://www.zghlxwxcb.cn/news/detail-832905.html
到了這里,關(guān)于Spring Cloud Gateway實現(xiàn)灰度發(fā)布功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!