1. LoadBalancer 是什么?
Spring Cloud LoadBalancer 是 Spring Cloud 官方自己提供的客户端负载均衡器,用来替代 Ribbon。
官方文档:Spring Cloud LoadBalancer :: Spring Cloud Commons
2. LoadBalancer 作用
从注册中心拉去服务列表,例如拉取库存服务列表,让订单根据负载均衡算法(例如轮询)调用某个库存服务。
3. 负载均衡器如何使用
3.1. 订单服务 pom.xml 中添加 LoadBalancer 的依赖
<!-- loadbalancer 负载均衡器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
3.2. 订单服务的 application.yml 中添加配置,启用 nacos 本地集群优先的负载均衡策略
spring:
application:
name: icoolkj-mall-user
cloud:
nacos:
discovery:
server-addr: icoolkj-mall-nacos-server:8848
namespace: dev # 指定 dev 开发环境命名空间
#group: group1 # 指定 group1 分组
cluster-name: BJ # 指定集群名称 北京机房
username: nacos
password: nacos
loadbalancer:
nacos:
enabled: true # 官方建议开启 nacos 负载均衡策略(NacosLoadBalancer)
#enabled: false # 默认使用轮询负载策略(RoundRobinLoadBalancer)
3.3. RestTemplate 通过添加 @LoadBalanced 注解接入 LoadBalancer
@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3.4. 订单服务调用逻辑
RestTemplate 远程调用
String url = "http://localhost:8560/api/order/getOrderByUserId?userId=" + userId
可以改为:
String url = "http://icoolkj-mall-order01/api/order/getOrderByUserId?userId=" + userId
使用微服务名称 icoolkj-mall-order01 代替 localhost:8560(localhost:8561)
4. 负载均衡器策略配置
4.1. 内置的负载均衡器策略
4.1.1. 轮询(默认)
4.1.2. 随机
4.1.3. NacosLoadBalancer
本地集群优先的原则,会先根据 cluster 配置优先选择本地集群,再根据权重选择具体的实例。
注意:如果配置了 spring.cloud.loadbalancer.nacos.enabled=true, 才会选择 NacosLoabalancer 负载均衡算法。
4.1.4. 测试
启动两个及其以上的订单服务,会员服务发起调用,查看负载均衡结果。
4.1.4.1. 将 spring.cloud.loadbalancer.nacos.enabled=false,测试是否按照轮询效果
4.1.4.2. 将 spring.cloud.loadbalancer.nacos.enabled=true,配置 cluster,测试本地集群优先是否生效。
4.2. 如何修改负载均衡策略为随机策略
注意,需要先将 spring.cloud.loadbalancer.nacos.enabled=false,或者去除该项配置。
4.2.1. 通过 @Bean 的方式配置随机的负载均衡策略
可以参考轮询策略的 @Bean 实现,定义 LoadBalancerConfig 配置类但是不要加 @Configuration 注解
// 注意,不需要加 @Configuration
public class LoadBalancerConfig {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory
) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class),
name);
}
}
4.2.2. 配置随机策略生效方式
方式1:全局生效,所有调用的接口都生效(配置再服务启动类上)
// 设置全局的负载均衡策略
@LoadBalancerClients(defaultConfiguration = LoadBalancerConfig.class)
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
方式2:局部生效,只对调用的接口生效(配置在 openFeign 的接口上)
@FeignClient(name = "icoolkj-mall-account", path = "/api/account")
// 设置局部的负载均衡策略
@LoadBalancerClient(name = "icoolkj-mall-account", configuration = LoadBalancerConfig.class)
public interface AccountServiceFeignClient {
@PostMapping("/reduce-balance")
Result<?> reduceBalance(@RequestBody AccountRequest accountRequest);
}
同上,同样的方式也可以配置其他的内置负载均衡器。
4.3. 自定义负载均衡器
需求:实现基于 ip hash 的负载均衡器
4.3.1. 实现 ip hash 策略的负载均衡器 MyCustomLoadBalancer,可以参考 RandomLoadBalancer 实现
// 定制的负载均衡策略
public class MyCustomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
private final String serviceId;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
*/
public MyCustomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@SuppressWarnings("rawtypes")
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
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);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// 自定义负载均衡策略
//获取 Request 对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ipAddress = request.getRemoteAddr();
log.info("用户 IP:" + ipAddress);
int hash = ipAddress.hashCode();
// IP 哈希负载均衡算法
int index = hash % instances.size();
// 得到服务实例的方法
ServiceInstance instance = instances.get(index);
return new DefaultResponse(instance);
}
}
4.3.2. 通过 @Bean 的方式配置 ip hash 的负载均衡器策略
// 注意,不需要加 @Configuration
public class LoadBalancerConfig {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory
) {
return new MyCustomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class),
name);
}
}
4.3.3. 配置 ip hash 负载均衡器生效
// 设置全局的负载均衡策略
@LoadBalancerClients(defaultConfiguration = LoadBalancerConfig.class)
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
测试,会员服务调用多个订单服务,是否走自定义的 ip hash 策略。
5. RestTempalte + @LoadBalanced 注解使用不当导致的 bug 及其解决方案
问题重现:某些特殊场景,微服务启动期间需要从其他微服务获取一些初始化数据,可能会选择在 Bean 的初始化方法中去调用其他微服务获取数据。
注意:在微服务架构中,通常推荐在服务启动后的业务逻辑中调用其他微服务,而不是在 Bean 的初始化中进行。这是因为 Bean 的初始化阶段可能发生在服务完全就绪之前,此时调用其他微服务可能会遇到各种问题,例如服务尚未注册、网络未就绪等。
演示:在会员服务的 Controller 的初始化方法调用订单服务,会出现什么问题?
@RestController
@RequestMapping("/api/user")
public class UserController implements InitializingBean{
@Autowired
private RestTemplate restTemplate;
@Override
public void afterPropertiesSet() throws Exception {
String url = "http://icoolkj-mall-order01/api/order/getOrderByUserId?userId=1";
Result<List<OrderResponse>> result = restTemplate.getForObject(url, Result.class);
log.info("result =>> {}", result.getData().stream().findFirst().toString());
}
}
启动会员服务,会启动失败,找不到要调用的微服务名:
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://icoolkj-mall-order01/api/order/getOrderByUserId": icoolkj-mall-order01
原因分析:业务 Bean 初始化期间使用 @LoadBalanced 修饰的 RestTemplate,还没有负载均衡能力,简单理解:此时负载均衡器功能还没生效,不能去调用其他微服务。
问题关键:@LoadBalanced 修饰的 RestTemplate 是什么时候具有负载均衡能力的
解决思路:
思路1:在服务启动后的业务逻辑中调用其他微服务,而不是在 Bean 的初始化方法中进行。
思路2:如果不使用 @LoadBalanced 注解,也可以通过添加 LoadBalancerInterceptor 拦截器让 RestTemplate 起到负载均衡的作用。
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(LoadBalancerInterceptor loadBalancerInterceptor) {
RestTemplate restTemplate = new RestTemplate();
//注入loadBalancerInterceptor拦截器(具有负载均衡的能力)
restTemplate.setInterceptors(Arrays.asList(loadBalancerInterceptor));
return restTemplate;
}
}
6. 小结
负载均衡的作用,负载均衡的策略及配置。