1、背景(灰度部署)
在咱们零碎公布生产环境时,有时为了确保新的服务逻辑没有问题,会让一小部分特定的用户来应用新的版本(比方客户端的内测版本
),而其余的用户应用旧的版本,那么这个在 Spring Cloud 中该如何来实现呢?
负载平衡组件应用:Spring Cloud LoadBalancer
2、需要
3、实现思路
通过翻阅 Spring Cloud 的官网文档,咱们晓得,大略能够通过 2
种形式来达到咱们的目标。
- 实现
ReactiveLoadBalancer
接口,重写负载平衡算法。 - 实现
ServiceInstanceListSupplier
接口,重写get
办法,返回自定义的服务列表
。
ServiceInstanceListSupplier
:能够实现如下性能,比方咱们的 user-service
在注册核心上存在 5 个,此处我能够只返回 3 个。
4、Spring Cloud 中是否有我上方相似需要的例子
查阅 Spring Cloud 官网文档,发现org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier
类能够实现相似的性能。
那可能有人会说,既然 Spring Cloud
曾经提供了这个性能,为什么你还要重写一个
?此处只是为了一个记录,因为工作中的需要可能各种各样,万一前期有相似的需要,此处记录了,前期晓得怎么实现。
5、外围代码实现
5.1 灰度外围代码
5.1.1 灰度服务实例选择器实现
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;
/**
* 自定义 依据服务名 获取服务实例 列表
* <p>
* 需要:用户通过申请拜访 网关 <br />
* 1、如果申请头中的 version 值和 上游服务元数据的 version 值统一,则抉择该 服务。<br />
* 2、如果申请头中的 version 值和 上游服务元数据的 version 值不统一,且 不存在 version 的值 为 default 则间接报错。<br />
* 3、如果申请头中的 version 值和 上游服务元数据的 version 值不统一,且 存在 version 的值 为 default,则抉择该服务。<br />
* <p>
* 参考: {@link org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier} 实现
*
* @author huan.fu
* @date 2023/6/19 - 21:14
*/
@Slf4j
public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
/**
* 申请头的名字,通过这个 version 字段和 服务中的元数据来 version 字段进行比拟,* 失去最终的实例数据
*/
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("获取到须要申请服务 [{}] 的 version:[{}]", getServiceId(), version);
return version;
}
/**
* 从申请中获取 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、获取 申请头中的 version 和 ServiceInstance 中 元数据中 version 统一的服务
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("返回申请服务:[{}]为 version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size());
return selectServiceInstances;
}
// 2、返回 version=default 的实例
selectServiceInstances = instances.stream()
.filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default"))
.collect(Collectors.toList());
log.info("返回申请服务:[{}]为 version:[{}]的有:[{}]个", getServiceId(), "default", selectServiceInstances.size());
return selectServiceInstances;
}
}
5.1.2 灰度 feign 申请头传递拦截器
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 申请头通过 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 申请头的值为:[{}]", version);
requestTemplate
.header("version", version);
}
}
留神:
此处全局配置了,配置了一个 feign 的全局拦截器,进行申请头 version
的传递。
5.1.3 灰度服务实例选择器配置
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 网关外围代码
5.2.1 网关配置文件
spring:
application:
name: lobalancer-gateway-8001
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
server-addr: localhost:8848
group: DEFAULT_GROUP
config:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
server:
port: 8001
logging:
level:
root: info
5.3 服务提供者外围代码
5.3.1 向外提供一个办法
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;
/**
* 获取服务信息
*
* @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 的服务地址
server-addr: localhost:8848
# 配置元数据
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 的服务地址
server-addr: localhost:8848
# 配置元数据
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 的服务地址
server-addr: localhost:8848
# 配置元数据
metadata:
version: default
config:
server-addr: localhost:8848
server:
port: 8007
留神 metadata 中 version 的值
5.4 服务消费者代码
5.4.1 通过 feign 调用提供者办法
/**
* @author huan.fu
* @date 2023/6/19 - 22:21
*/
@FeignClient(value = "provider")
public interface FeignProvider {
/**
* 获取服务信息
*
* @return ip:port
*/
@GetMapping("serverInfo")
String fetchServerInfo();}
5.4.2 向外提供一个办法
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;
/**
* 消费者控制器
*
* @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;
}
}
消费者端口 8002 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
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 的值
消费者端口 8003 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
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 的值
消费者端口 8004 配置信息
spring:
application:
name: consumer
cloud:
nacos:
discovery:
# 配置 nacos 的服务地址
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、测试
6.1 申请头中携带 version=v1
从上图中能够看到,当 version=v1
时,服务消费者为 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"}%
➜ ~
能够看到,消费者返回的端口是 8002
,提供者返回的端口是8005|8006
是合乎预期的。
6.2 不传递 version
从上图中能够看到,当不携带时,服务消费者为 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"}%
➜ ~
能够看到,消费者返回的端口是 8004
,提供者返回的端口是8007
是合乎预期的。
7、残缺代码
https://gitee.com/huan1993/spring-cloud-alibaba-parent/tree/master/loadbalancer-supply-service-instance
8、参考文档
1、https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer