Spring Cloud Gateway實(shí)現(xiàn)灰度發(fā)布方案
灰度發(fā)布又名金絲雀發(fā)布,在微服務(wù)中的表現(xiàn)為同一服務(wù)同時(shí)上線不同版本,讓一部分用戶使用新版本來驗(yàn)證新特性,如果驗(yàn)證沒有問題,則將所有用戶都遷移到新版本上。
在微服務(wù)架構(gòu)中,網(wǎng)關(guān)負(fù)責(zé)請求的統(tǒng)一入口,主要功能之一是請求路由。而灰度發(fā)布實(shí)質(zhì)就是讓指定用戶路由到指定版本的服務(wù)上。所以該功能可以在網(wǎng)關(guān)這一層實(shí)現(xiàn)。
今天就分享下Spring Cloud Gateway如何實(shí)現(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方法中)。
那么要實(shí)現(xiàn)灰度發(fā)布該怎么做?我們這里可以自己寫一個(gè)Predicate,來實(shí)現(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ī)則中。
實(shí)現(xiàn)了指定用戶匹配到指定規(guī)則只是第一步,下一步要實(shí)現(xiàn)的是如何讓指定用戶路由到指定版本的服務(wù)中,想要實(shí)現(xiàn)這一點(diǎn),就需要先了解Spring Cloud Gateway的負(fù)載均衡邏輯,也就是Spring Cloud Gateway是如何選取要調(diào)用的服務(wù)的。
2 Spring Cloud Gateway的負(fù)載均衡邏輯
負(fù)載均衡的邏輯如下:
1、 從注冊中心獲取服務(wù)實(shí)例列表(實(shí)際實(shí)現(xiàn)中服務(wù)實(shí)例列表是后臺定時(shí)刷新緩存在內(nèi)存中的);
2、根據(jù)負(fù)載均衡算法從實(shí)例列表中選取服務(wù)。
在Spring Cloud Gateway中,相應(yīng)的代碼在ReactiveLoadBalancerClientFilter#choose方法中。
默認(rèn)情況下,Spring Cloud Gateway負(fù)載均衡策略會從注冊中心所有服務(wù)實(shí)例中輪詢選擇一個(gè)服務(wù)實(shí)例。由此可以看出,默認(rèn)實(shí)現(xiàn)無法滿足我們的需求,因?yàn)槲覀兿胍囟ㄓ脩袈酚傻教囟ǖ姆?wù)版本上。
那么該如何解決呢?答案是重寫負(fù)載均衡算法,來實(shí)現(xiàn)選擇特定版本的服務(wù)實(shí)例功能。
3 版本號如何指定
灰度發(fā)布的目的是實(shí)現(xiàn)指定用戶訪問指定版本,用戶信息可以在HTTP Header中帶過來,那么版本號如何指定?
這里有兩種方案。
第一種方案也是通過請求的HTTP Header帶過來,缺點(diǎn)是需要客戶端修改;
第二種方案是在網(wǎng)關(guān)層修改請求,動(dòng)態(tài)為請求加上版本號信息,此方案較好,對客戶端透明。
4 灰度發(fā)布的實(shí)現(xiàn)
看到這里,整個(gè)灰度發(fā)布的實(shí)現(xiàn)思路應(yīng)該比較清晰了。
1、首先編寫自己的Predicate,實(shí)現(xiàn)指定用戶匹配到指定的路由規(guī)則中;
2、動(dòng)態(tài)修改請求,添加版本號信息,版本號信息可以放在HTTP Header中(此處可以通過原生AddRequestHeaderGatewayFilterFactory來實(shí)現(xiàn),無需自己寫代碼);
3、重寫負(fù)載均衡算法,根據(jù)版本號信息從注冊中心的服務(wù)實(shí)例上選擇相應(yīng)的服務(wù)版本進(jìn)行請求的轉(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; } }
自定義負(fù)載均衡算法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ù)實(shí)例 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)用自定義的負(fù)責(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ā)布功能如上所述,相比實(shí)現(xiàn),思路是大家更需要關(guān)注的地方。思路清晰了,即使換個(gè)網(wǎng)關(guān)實(shí)現(xiàn),換個(gè)注冊中心實(shí)現(xiàn),都是一樣的。
灰度發(fā)布實(shí)質(zhì)是讓指定用戶訪問指定版本的服務(wù)。
所以首先需要指定用戶匹配到指定的路由規(guī)則。
其次,服務(wù)的版本號信息可以通過HTTP請求頭字段來指定。
最后,負(fù)載均衡算法需要能夠根據(jù)版本號信息來做服務(wù)實(shí)例的選擇。
到此這篇關(guān)于Spring Cloud Gateway實(shí)現(xiàn)灰度發(fā)布方案的文章就介紹到這了,更多相關(guān)Spring Cloud Gateway灰度發(fā)布內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Cloud 優(yōu)雅下線以及灰度發(fā)布實(shí)現(xiàn)
- SpringCloud實(shí)現(xiàn)灰度發(fā)布的方法步驟
- springcloud+nacos實(shí)現(xiàn)灰度發(fā)布示例詳解
- 關(guān)于SpringCloud灰度發(fā)布的實(shí)現(xiàn)
- SpringCloud灰度發(fā)布的設(shè)計(jì)與實(shí)現(xiàn)詳解
- SpringCloud的全鏈路灰度發(fā)布方案詳解
- Spring?Cloud實(shí)現(xiàn)灰度發(fā)布的示例代碼
- SpringCloud實(shí)現(xiàn)全鏈路灰度發(fā)布的示例詳解
相關(guān)文章
SpringBoot如何讀取配置文件中的數(shù)據(jù)到map和list
這篇文章主要介紹了SpringBoot如何讀取配置文件中的數(shù)據(jù)到map和list,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02一分鐘掌握J(rèn)ava?ElasticJob分布式定時(shí)任務(wù)
ElasticJob?是面向互聯(lián)網(wǎng)生態(tài)和海量任務(wù)的分布式調(diào)度解決方案,本文主要通過簡單的示例帶大家深入了解ElasticJob分布式定時(shí)任務(wù)的相關(guān)知識,需要的可以參考一下2023-05-05Java網(wǎng)絡(luò)通信基礎(chǔ)編程(必看篇)
下面小編就為大家?guī)硪黄狫ava網(wǎng)絡(luò)通信基礎(chǔ)編程(必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05Spring Boot實(shí)現(xiàn)熱部署的實(shí)例方法
在本篇文章里小編給大家整理的是關(guān)于Spring Boot實(shí)現(xiàn)熱部署的實(shí)例方法和實(shí)例,需要的朋友們可以參考下。2020-02-02Javafx利用fxml變換場景的實(shí)現(xiàn)示例
本文主要介紹了Javafx利用fxml變換場景的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07JMeter參數(shù)化4種實(shí)現(xiàn)方式(小結(jié))
參數(shù)化是自動(dòng)化測試腳本的一種常用技巧,可將腳本中的某些輸入使用參數(shù)來代替,JMeter提供了多種參數(shù)化方式,下面就其中常用的4種展開闡述,感興趣的可以來了解一下2021-12-12