Spring?Cloud灰度部署實(shí)現(xiàn)過(guò)程詳解
1、背景(灰度部署)
在我們系統(tǒng)發(fā)布生產(chǎn)環(huán)境時(shí),有時(shí)為了確保新的服務(wù)邏輯沒(méi)有問(wèn)題,會(huì)讓一小部分特定的用戶來(lái)使用新的版本(比如客戶端的內(nèi)測(cè)版本),而其余的用戶使用舊的版本,那么這個(gè)在Spring Cloud中該如何來(lái)實(shí)現(xiàn)呢?
負(fù)載均衡組件使用:Spring Cloud LoadBalancer
2、需求

3、實(shí)現(xiàn)思路

通過(guò)翻閱Spring Cloud的官方文檔,我們知道,大概可以通過(guò)2種方式來(lái)達(dá)到我們的目的。
- 實(shí)現(xiàn) ReactiveLoadBalancer接口,重寫(xiě)負(fù)載均衡算法。
- 實(shí)現(xiàn)ServiceInstanceListSupplier接口,重寫(xiě)get方法,返回自定義的服務(wù)列表。
ServiceInstanceListSupplier: 可以實(shí)現(xiàn)如下功能,比如我們的 user-service在注冊(cè)中心上存在5個(gè),此處我可以只返回3個(gè)。
4、Spring Cloud中是否有我上方類似需求的例子
查閱Spring Cloud官方文檔,發(fā)現(xiàn)org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier 類可以實(shí)現(xiàn)類似的功能。
那可能有人會(huì)說(shuō),既然Spring Cloud已經(jīng)提供了這個(gè)功能,為什么你還要重寫(xiě)一個(gè)? 此處只是為了一個(gè)記錄,因?yàn)楣ぷ髦械男枨罂赡芨鞣N各樣,萬(wàn)一后期有類似的需求,此處記錄了,后期知道怎么實(shí)現(xiàn)。
5、核心代碼實(shí)現(xiàn)
5.1 灰度核心代碼
5.1.1 灰度服務(wù)實(shí)例選擇器實(shí)現(xiàn)
package com.huan.loadbalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 自定義 根據(jù)服務(wù)名 獲取服務(wù)實(shí)例 列表
* <p>
* 需求: 用戶通過(guò)請(qǐng)求訪問(wèn) 網(wǎng)關(guān)<br />
* 1、如果請(qǐng)求頭中的 version 值和 下游服務(wù)元數(shù)據(jù)的 version 值一致,則選擇該 服務(wù)。<br />
* 2、如果請(qǐng)求頭中的 version 值和 下游服務(wù)元數(shù)據(jù)的 version 值不一致,且 不存在 version 的值 為 default 則直接報(bào)錯(cuò)。<br />
* 3、如果請(qǐng)求頭中的 version 值和 下游服務(wù)元數(shù)據(jù)的 version 值不一致,且 存在 version 的值 為 default,則選擇該服務(wù)。<br />
* <p>
* 參考: {@link org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier} 實(shí)現(xiàn)
*
* @author huan.fu
* @date 2023/6/19 - 21:14
*/
@Slf4j
public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
/**
* 請(qǐng)求頭的名字, 通過(guò)這個(gè) version 字段和 服務(wù)中的元數(shù)據(jù)來(lái)version字段進(jìn)行比較,
* 得到最終的實(shí)例數(shù)據(jù)
*/
private static final String VERSION_HEADER_NAME = "version";
public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
super(delegate);
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get();
}
@Override
public Flux<List<ServiceInstance>> get(Request request) {
return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext())));
}
private String getVersion(Object requestContext) {
if (requestContext == null) {
return null;
}
String version = null;
if (requestContext instanceof RequestDataContext) {
version = getVersionFromHeader((RequestDataContext) requestContext);
}
log.info("獲取到需要請(qǐng)求服務(wù)[{}]的version:[{}]", getServiceId(), version);
return version;
}
/**
* 從請(qǐng)求中獲取version
*/
private String getVersionFromHeader(RequestDataContext context) {
if (context.getClientRequest() != null) {
HttpHeaders headers = context.getClientRequest().getHeaders();
if (headers != null) {
return headers.getFirst(VERSION_HEADER_NAME);
}
}
return null;
}
private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {
// 1、獲取 請(qǐng)求頭中的 version 和 ServiceInstance 中 元數(shù)據(jù)中 version 一致的服務(wù)
List<ServiceInstance> selectServiceInstances = instances.stream()
.filter(instance -> instance.getMetadata().get(VERSION_HEADER_NAME) != null
&& Objects.equals(version, instance.getMetadata().get(VERSION_HEADER_NAME)))
.collect(Collectors.toList());
if (!selectServiceInstances.isEmpty()) {
log.info("返回請(qǐng)求服務(wù):[{}]為version:[{}]的有:[{}]個(gè)", getServiceId(), version, selectServiceInstances.size());
return selectServiceInstances;
}
// 2、返回 version=default 的實(shí)例
selectServiceInstances = instances.stream()
.filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default"))
.collect(Collectors.toList());
log.info("返回請(qǐng)求服務(wù):[{}]為version:[{}]的有:[{}]個(gè)", getServiceId(), "default", selectServiceInstances.size());
return selectServiceInstances;
}
}5.1.2 灰度f(wàn)eign請(qǐng)求頭傳遞攔截器
package com.huan.loadbalancer;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 將version請(qǐng)求頭通過(guò)feign傳遞到下游
*
* @author huan.fu
* @date 2023/6/20 - 08:27
*/
@Component
@Slf4j
public class VersionRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String version = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
.getHeader("version");
log.info("feign 中傳遞的 version 請(qǐng)求頭的值為:[{}]", version);
requestTemplate
.header("version", version);
}
}注意: 此處全局配置了,配置了一個(gè)feign的全局?jǐn)r截器,進(jìn)行請(qǐng)求頭version的傳遞。
5.1.3 灰度服務(wù)實(shí)例選擇器配置
package com.huan.loadbalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 此處選擇全局配置
*
* @author huan.fu
* @date 2023/6/19 - 22:16
*/
@Configuration
@Slf4j
@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)
public class VersionServiceInstanceListSupplierConfiguration {
@Bean
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV1(
ConfigurableApplicationContext context) {
log.error("===========> versionServiceInstanceListSupplierV1");
ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withCaching()
.build(context);
return new VersionServiceInstanceListSupplier(delegate);
}
@Bean
@ConditionalOnClass(name = "org.springframework.web.reactive.DispatcherHandler")
public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV2(
ConfigurableApplicationContext context) {
log.error("===========> versionServiceInstanceListSupplierV2");
ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.build(context);
return new VersionServiceInstanceListSupplier(delegate);
}
}此處偷懶全局配置了
`@Configuration
@Slf4j
@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)
`
5.2 網(wǎng)關(guān)核心代碼
5.2.1 網(wǎng)關(guān)配置文件
spring:
application:
name: lobalancer-gateway-8001
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
group: DEFAULT_GROUP
config:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
server:
port: 8001
logging:
level:
root: info5.3 服務(wù)提供者核心代碼
5.3.1 向外提供一個(gè)方法
package com.huan.loadbalancer.controller;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 提供者控制器
*
* @author huan.fu
* @date 2023/3/6 - 21:58
*/
@RestController
public class ProviderController {
@Resource
private NacosDiscoveryProperties nacosDiscoveryProperties;
/**
* 獲取服務(wù)信息
*
* @return ip:port
*/
@GetMapping("serverInfo")
public String serverInfo() {
return nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort();
}
}5.3.2 提供者端口8005配置信息
spring:
application:
name: provider
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
# 配置元數(shù)據(jù)
metadata:
version: v1
config:
server-addr: localhost:8848
server:
port: 8005注意 metadata中version的值
5.3.2 提供者端口8006配置信息
spring:
application:
name: provider
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
# 配置元數(shù)據(jù)
metadata:
version: v1
config:
server-addr: localhost:8848
server:
port: 8006注意 metadata中version的值
5.3.3 提供者端口8007配置信息
spring:
application:
name: provider
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
# 配置元數(shù)據(jù)
metadata:
version: default
config:
server-addr: localhost:8848
server:
port: 8007注意 metadata中version的值
5.4 服務(wù)消費(fèi)者代碼
5.4.1 通過(guò) feign 調(diào)用提供者方法
/**
* @author huan.fu
* @date 2023/6/19 - 22:21
*/
@FeignClient(value = "provider")
public interface FeignProvider {
/**
* 獲取服務(wù)信息
*
* @return ip:port
*/
@GetMapping("serverInfo")
String fetchServerInfo();
}5.4.2 向外提供一個(gè)方法
package com.huan.loadbalancer.controller;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.huan.loadbalancer.feign.FeignProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 消費(fèi)者控制器
*
* @author huan.fu
* @date 2023/6/19 - 22:21
*/
@RestController
public class ConsumerController {
@Resource
private FeignProvider feignProvider;
@Resource
private NacosDiscoveryProperties nacosDiscoveryProperties;
@GetMapping("fetchProviderServerInfo")
public Map<String, String> fetchProviderServerInfo() {
Map<String, String> ret = new HashMap<>(4);
ret.put("consumer信息", nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort());
ret.put("provider信息", feignProvider.fetchServerInfo());
return ret;
}
}消費(fèi)者端口 8002 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: v1
config:
server-addr: localhost:8848
server:
port: 8002注意 metadata中version的值
消費(fèi)者端口 8003 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: v2
config:
server-addr: localhost:8848
server:
port: 8003注意 metadata中version的值
消費(fèi)者端口 8004 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服務(wù)地址
server-addr: localhost:8848
register-enabled: true
service: nacos-feign-consumer
group: DEFAULT_GROUP
metadata:
version: default
config:
server-addr: localhost:8848
server:
port: 8003注意 metadata中version的值
6、測(cè)試

6.1 請(qǐng)求頭中攜帶 version=v1
從上圖中可以看到,當(dāng)version=v1時(shí),服務(wù)消費(fèi)者為consumer-8002, 提供者為provider-8005和provider-8006
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \
--header 'version: v1'
{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
? ~
可以看到,消費(fèi)者返回的端口是8002,提供者返回的端口是8005|8006是符合預(yù)期的。
6.2 不傳遞version
從上圖中可以看到,當(dāng)不攜帶時(shí),服務(wù)消費(fèi)者為consumer-8004, 提供者為provider-8007和
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
? ~可以看到,消費(fèi)者返回的端口是8004,提供者返回的端口是8007是符合預(yù)期的。
完整代碼
參考文檔
1、https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer
以上就是Spring Cloud灰度部署實(shí)現(xiàn)過(guò)程詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Cloud灰度部署的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring中PathMatcher路徑匹配器的實(shí)現(xiàn)
Spring框架中的PathMatcher是一個(gè)接口,本文主要介紹了Spring中PathMatcher路徑匹配器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07
idea中同一SpringBoot項(xiàng)目多端口啟動(dòng)
本文主要介紹了idea中同一SpringBoot項(xiàng)目多端口啟動(dòng),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
使用Mybatis-plus實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)表的內(nèi)部字段進(jìn)行比較
這篇文章主要介紹了使用Mybatis-plus實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)表的內(nèi)部字段進(jìn)行比較方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
springboot-mongodb的多數(shù)據(jù)源配置的方法步驟
這篇文章主要介紹了springboot-mongodb的多數(shù)據(jù)源配置的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
spring boot下mybatis配置雙數(shù)據(jù)源的實(shí)例
這篇文章主要介紹了spring boot下mybatis配置雙數(shù)據(jù)源的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09

