Spring Cloud 升级之路 - 2020.0.x - 6. 使用 Spring Cloud LoadBalancer (1)
本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford
我们使用 Spring Cloud 官方推荐的 Spring Cloud LoadBalancer 作为我们的客户端负载均衡器。
Spring Cloud LoadBalancer背景
Spring Cloud LoadBalancer是一个客户端负载均衡器,类似于Ribbon,但是由于Ribbon已经进入维护模式,并且Ribbon 2并不与Ribbon 1相互兼容,所以Spring Cloud全家桶在Spring Cloud Commons项目中,添加了Spring cloud Loadbalancer作为新的负载均衡器,并且做了向前兼容,就算你的项目中继续用 Spring Cloud Netflix 套装(包括Ribbon,Eureka,Zuul,Hystrix等等)让你的项目中有这些依赖,你也可以通过简单的配置,把ribbon替换成Spring Cloud LoadBalancer。
负载均衡器在哪里使用?
Spring Cloud 中内部微服务调用默认是 http 请求,主要通过下面三种 API:
- RestTemplate:同步 http API
- WebClient:异步响应式 http API
- 三方客户端封装,例如 openfeign
如果项目中加入了 spring-cloud-loadbalancer 的依赖并且配置启用了,那么会自动在相关的 Bean 中加入负载均衡器的特性。
- 对于 RestTemplate,会自动对所有
@LoadBalanced注解修饰的 RestTemplate Bean 增加 Interceptor 从而加上了负载均衡器的特性。 - 对于 WebClient,会自动创建
ReactorLoadBalancerExchangeFilterFunction,我们可以通过加入ReactorLoadBalancerExchangeFilterFunction会加入负载均衡器的特性。 - 对于三方客户端,一般不需要我们额外配置什么。
这些使用的示例,会在我们系列升级完最后的测试部分看到。
Spring Cloud LoadBalancer 结构简介
上一节我们提到了 NamedContextFactory,Spring Cloud LoadBalancer 这里也是使用了这个机制实现了不同微服务使用不同的 Spring Cloud LoadBalancer 配置。相关核心实现是 @LoadBalancerClient 和 @LoadBalancerClients 这两个注解,以及 NamedContextFactory.Specification 的实现 LoadBalancerClientSpecification,NamedContextFactory 的实现 LoadBalancerClientFactory。
经过上一节的详细分析,我们知道可以通过 LoadBalancerClientFactory 知道默认配置类为 LoadBalancerClientConfiguration. 并且获取微服务名称可以通过 environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
public static final String NAMESPACE = "loadbalancer";
public static final String PROPERTY_NAME = NAMESPACE + ".client.name";
public LoadBalancerClientFactory() {
super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
}
查看配置类 LoadBalancerClientConfiguration,我们可以发现这个类主要定义两种 Bean,分别是 ReactorLoadBalancer<ServiceInstance> 和 ServiceInstanceListSupplier。
ReactorLoadBalancer 是负载均衡器,主要提供根据服务名称获取服务实例列表并从从中选择的功能。
Mono<Response<T>> choose(Request request);
在默认配置中的实现是:
LoadBalancerClientConfiguration
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
//获取微服务名称
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
//创建 RoundRobinLoadBalancer
//注意这里注入的是 LazyProvider,这主要因为在注册这个 Bean 的时候相关的 Bean 可能还没有被加载注册,利用 LazyProvider 而不是直接注入所需的 Bean 防止报找不到 Bean 注入的错误。
return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
可以看出,默认配置的 ReactorLoadBalancer 实现是 RoundRobinLoadBalancer。这个负载均衡器实现很简单,有一个原子类型的 AtomicInteger position,从 ServiceInstanceListSupplier 中读取所有的服务实例列表,然后对于 position 原子加1,对列表大小取模,返回列表中这个位置的服务实例 ServiceInstance。
public Mono<Response<ServiceInstance>> choose(Request request) {
//注入的时候注入的是 Lazy Provider,这里取出真正的 Bean,也就是 ServiceInstanceListSupplier
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
//获取实例列表
return supplier.get(request)
.next()
//从列表中选择一个实例
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
// 如果 ServiceInstanceListSupplier 也实现了 SelectedInstanceCallback,则执行下面的逻辑进行回调。SelectedInstanceCallback 就是每次负载均衡器选择实例之后进行的回调
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
}
//postion 原子 +1 并取绝对值
int pos = Math.abs(this.position.incrementAndGet());
//返回对应下标的实例
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
ServiceInstanceListSupplier 是服务列表提供者接口:
public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> {
String getServiceId();
default Flux<List<ServiceInstance>> get(Request request) {
return get();
}
static ServiceInstanceListSupplierBuilder builder() {
return new ServiceInstanceListSupplierBuilder();
}
}
spring-cloud-loadbalancer 中有很多 ServiceInstanceListSupplier 的实现,在默认配置中是通过属性配置指定实现的,这个配置项是spring.cloud.loadbalancer.configurations。例如:
LoadBalancerClientConfiguration
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
//spring.cloud.loadbalancer.configurations 未指定或者为 default
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default",
matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
//通过 DiscoveryClient 提供实例
.withDiscoveryClient()
//开启缓存
.withCaching()
.build(context);
}
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
//如果 spring.cloud.loadbalancer.configurations 指定为 zone-preference
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "zone-preference")
public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
//通过 DiscoveryClient 提供实例
.withDiscoveryClient()
//启用更倾向于同一个 zone 下实例的特性
.withZonePreference()
//开启缓存
.withCaching()
.build(context);
}
可以看到,可以通过 ServiceInstanceListSupplier.builder() 生成官方封装好各种特性的 ServiceInstanceListSupplier。其实从底层实现可以看出,所有的 ServiceInstanceListSupplier 实现都是代理模式,例如对于默认配置,底层代码近似于:
return //开启服务实例缓存
new CachingServiceInstanceListSupplier(
//启用通过 discoveryClient 的服务发现
new DiscoveryClientServiceInstanceListSupplier(
discoveryClient, env
)
, cacheManagerProvider.getIfAvailable()
);
除了默认配置 LoadBalancerClientConfiguration,用户配置自定义配置则是通过 @LoadBalancerClients 和 @LoadBalancerClient.这个原理是通过 LoadBalancerClientConfigurationRegistrar 实现的。首先,我们来看一下 LoadBalancerClientFactory 这个 NamedContextFactory 是如何创建的:
[LoadBalancerAutoConfiguration]
private final ObjectProvider<List<LoadBalancerClientSpecification>> configurations;
public LoadBalancerAutoConfiguration(ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
//注入 LoadBalancerClientSpecification List 的 provider
//在 Bean 创建的时候,进行载入,而不是注册的时候
this.configurations = configurations;
}
@ConditionalOnMissingBean
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory() {
//创建 LoadBalancerClientFactory
LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory();
//读取所有的 LoadBalancerClientSpecification,设置为 LoadBalancerClientFactory 的配置
clientFactory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList));
return clientFactory;
}
那么,LoadBalancerClientSpecification 这些 Bean 是怎么创建的呢?在 @LoadBalancerClients 和 @LoadBalancerClient 注解中,都包含 @Import(LoadBalancerClientConfigurationRegistrar.class)。这个 @Import 加载一个 ImportBeanDefinitionRegistrar,这里是 LoadBalancerClientConfigurationRegistrar. ImportBeanDefinitionRegistrar里面的方法参数包含注解元数据,以及注册 Bean 的 BeanDefinitionRegistry。一般通过注解元数据,动态通过 BeanDefinitionRegistry 注册 Bean,在这里的实现是:
[LoadBalancerClients]
@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(LoadBalancerClientConfigurationRegistrar.class)
public @interface LoadBalancerClients {
//可以指定多个 LoadBalancerClient
LoadBalancerClient[] value() default {};
//指定所有的负载均衡配置的默认配置
Class<?>[] defaultConfiguration() default {};
}
[LoadBalancerClient]
@Configuration(proxyBeanMethods = false)
@Import(LoadBalancerClientConfigurationRegistrar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoadBalancerClient {
//name 和 value 都是微服务名称
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
//这个微服务的配置
Class<?>[] configuration() default {};
}
[LoadBalancerClientConfigurationRegistrar]
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//获取 LoadBalancerClients 注解的元数据
Map<String, Object> attrs = metadata.getAnnotationAttributes(LoadBalancerClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
//对于 value 属性,其实就是一个 LoadBalancerClient 列表,对于每个生成一个特定微服务名字的 LoadBalancerClientSpecification
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client), client.get("configuration"));
}
}
//如果指定了 defaultConfiguration,则注册为 default 的配置
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name, attrs.get("defaultConfiguration"));
}
//获取 LoadBalancerClient 注解的元数据
Map<String, Object> client = metadata.getAnnotationAttributes(LoadBalancerClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
//初始化 LoadBalancerClientSpecification 的 BeanDefinition,用于注册一个 LoadBalancerClientSpecification Bean
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(LoadBalancerClientSpecification.class);
//构造器参数
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
//注册 Bean
registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification", builder.getBeanDefinition());
}
从代码中我们可以看出,通过使用 @LoadBalancerClients 和 @LoadBalancerClient 注解可以自动生成对应的 LoadBalancerClientSpecification 进而实现公共负载均衡配置或者特定某个微服务的负载均衡配置。
微信搜索“我的编程喵”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer:
Spring Cloud 升级之路 - 2020.0.x - 6. 使用 Spring Cloud LoadBalancer (1)的更多相关文章
- Spring Cloud 升级之路 - 2020.0.x - 7. 使用 Spring Cloud LoadBalancer (2)
本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...
- Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...
- SpringCloud升级之路2020.0.x版-21.Spring Cloud LoadBalancer简介
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...
- Spring Cloud 升级之路 - 2020.0.x - 4. 使用 Eureka 作为注册中心
Eureka 目前的状态:Eureka 目前 1.x 版本还在更新,但是应该不会更新新的功能了,只是对现有功能进行维护,升级并兼容所需的依赖. Eureka 2.x 已经胎死腹中了.但是,这也不代表 ...
- Spring Cloud 升级之路 - 2020.0.x - 3. Undertow 的 accesslog 配置
上一节我们讲述了如何使用 Undertow 作为我们的 Web 服务容器,本小节我们来分析使用 Undertow 的另一个问题,也就是如何配置 accesslog,以及 accesslog 的各种占位 ...
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...
- Spring Cloud 升级之路 - 2020.0.x - 2. 使用 Undertow 作为我们的 Web 服务容器
本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我们的项目中,我 ...
- Spring Cloud 升级之路 - 2020.0.x - 5. 理解 NamedContextFactory
spring-cloud-commons 中参考了 spring-cloud-netflix 的设计,引入了 NamedContextFactory 机制,一般用于对于不同微服务的客户端模块使用不同的 ...
- SpringCloud升级之路2020.0.x版-22.Spring Cloud LoadBalancer核心源码
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 经过上一节的详细分 ...
随机推荐
- Fiddler使用 断点 模拟返回 AutoResponder Mock 模拟数据 相关学习记录
断点 测试中有时需要改变发出去的请求信息,需要用到打断点的方法.断点包含两种方式: before response:在request请求的时候,未到达服务器之前,一般用来修改请求参数 after re ...
- Box UVA - 1587
Ivan works at a factory that produces heavy machinery. He has a simple job - he knocks up wooden box ...
- UI自动化测试框架:关键字驱动+数据驱动
1. 关键字驱动框架简介 2. 工程结构说明 3. 工程代码实现 action 包 page_action.py business_process 包 case_process.py data_so ...
- 使用Docker及k8s启动logstash服务
前提:搭建好elasticsearch和kibana服务 下载镜像,需要下载与elasticsearch和kibana相同版本镜像 docker pull docker.elastic.co/logs ...
- 浅谈Java的反射机制和作用
浅谈Java的反射机制和作用 作者:Java大师 欢迎转载,转载请注明出处 很多刚学Java反射的同学可能对反射技术一头雾水,为什么要学习反射,学习反射有什么作用,不用反射,通过new也能创建用户对象 ...
- 08- Tomcat入门与环境搭建部署
环境搭建:网站文件(开发人员提供),相关软件(web服务器,应用服务器,数据库软件),硬件(服务器设备上),网络环境. 开发人员提供:部署文档说明书(操作系统版本,硬件配置,服务器软件及相关版本,部署 ...
- hdu 1814 字典序最小的2sat(暴力深搜)
题意: 题意就是最基础的2sat,关系只有矛盾关系,然后二选一,关键是这个题目是输出字典序最小的那组解. 思路: 输出字典序最小,用强连通那个实现不了(起码没看到有人实现),其实我 ...
- 路由器逆向分析------MIPS系统网络的配置(QEMU)
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/69378333 MIPS系统网络的配置 使用QEMU 模拟正在运行的MIPS系统并 ...
- POJ2431贪心(最少加油次数)
题意: 给一个终点,然后给你一个卡车距离终点的距离,还有其他个加油站距离终点的距离,然后每走一个单位距离要花费一个单位油,卡车的邮箱是无限大的,而每个加油站的油量是有限的,整个路径是一个 ...
- Django中图形验证码(django-simple-captcha)
django-simple-captcha 在网站开发的登录页面中,经常会需要使用到图形验证码来验证.在Django中,django-simple-captcha库包提供了图形验证码的使用. 下面我们 ...
