如何实现负载均衡
服务器端负载均衡
比如NGINX --> 通过NGINX负载均衡
客户端侧负载均衡
在课程微服务中获取到所有的用户微服务的所有实例 在课程微服务中提供的算法来使用其中一个用户微服务
其中课程微服务相对于用户微服务来说处于客户端测,因此称为客户端侧负载均衡
Ribbon & Spring Cloud Loadbalancer对比与选择
都是客户端侧负载均衡
Ribbon现状
- 成熟 流行
- 维护模式(由于之前维护不给力 不会提供新功能 只会维护)
Spring Cloud Loadbalancer
-
下一代客户端侧负载均衡器
-
过于简陋 目前只提供了轮询算法的负载均衡模式
-
暂时没有找到成功案例
-
目前默认的负载均衡器依然是Ribbon
手写一个客户端负载均衡器
随机的负载均衡器
修改课程微服务如下:
package com.cloud.msclass.service;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;
@Service
public class LessonService {
private static final Logger logger = LoggerFactory.getLogger(LessonService.class);
@Autowired
private LessonRepository lessonRepository;
@Autowired
private LessonUserRepository lessonUserRepository;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
public Lesson buyById(Integer id) {
// 1. 根据id查询lesson
Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
if (lessonUser != null) {
return lesson;
}
// TODO 登录实现后需重构
Integer userId = 1;
// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
List<ServiceInstance> instances = this.discoveryClient.getInstances("ms-user");
// List<ServiceInstance> njInstances = instances.stream().filter(instance -> {
// Map<String, String> metadata = instance.getMetadata();
// String jiFang = metadata.get("JIFANG");
// if ("NJ".contentEquals(jiFang)) {
// return true;
// }
// return false;
// }).collect(Collectors.toList());
int i = ThreadLocalRandom.current().nextInt(instances.size());
// TODO 需要改进
URI uri = instances.get(i).getUri();
logger.info("当前选择的用户微服务实例的地址是 = {}", uri);
UserDTO userDTO = restTemplate.getForObject(uri + "users/{userId}", UserDTO.class, userId);
BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
if (money.doubleValue() < 0) {
throw new IllegalArgumentException("余额不足");
}
// TODO 购买逻辑 ... 1. 调用用户微服务的扣减金额接口 2.向lesson_user表插入数据
return lesson;
}
}
启动多个用户微服务:
调用课程微服务:http://localhost:8010/lesssons/buy/1
使用Ribbon实现负载均衡
Netflix开源的客户端侧负载均衡器
添加注解
/**
* spring web提供的轻量级http client
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
修改代码
package com.cloud.msclass.service;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;
@Service
public class LessonService {
@Autowired
private LessonRepository lessonRepository;
@Autowired
private LessonUserRepository lessonUserRepository;
@Autowired
private RestTemplate restTemplate;
public Lesson buyById(Integer id) {
// 1. 根据id查询lesson
Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
if (lessonUser != null) {
return lesson;
}
// TODO 登录实现后需重构
// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
Integer userId = 1;
UserDTO userDTO = restTemplate.getForObject("http://ms-user/users/{userId}", UserDTO.class, userId);
BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
if (money.doubleValue() < 0) {
throw new IllegalArgumentException("余额不足");
}
// TODO 购买逻辑 ... 1. 调用用户微服务的扣减金额接口 2.向lesson_user表插入数据
return lesson;
}
}
Ribbon核心组件
Ribbon配置自定义-Java代码配置方式
将负载均衡规则改为随机,负载均衡可以细到微服务级别(针对特定的微服务采用特定的负载均衡算法)
package com.cloud.ribbonconfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}
package com.cloud.msclass.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
import com.cloud.ribbonconfiguration.RibbonConfiguration;
/**
* 设置用户微服务适用随机负载均衡策略
* @project ms-class
* @author guanglai.zhou
* @date 2020年2月16日
*/
@Configuration
@RibbonClient(name = "ms-user", configuration = RibbonConfiguration.class)
public class MsUserRibbonConfiguration {
}
需要为ribbon配置文件单独定义一个包
现在课程微服务有两个上下文,一个是应用的主上下文(applicationContext),一个是ribbon的上下文,这个是子上下文,父子上下文扫描的包一旦重叠,会导致各种奇葩的问题。在xml配置spring的时代,springframework上下文是父上下文,而springmvc上下文是子上下文,一旦扫描包重叠,就可能导致事务不生效。只需要让spring不扫描包含controller注解的类,而spring-mvc只扫描包含@Controller注解的包,避免重复扫描
参考博客:https://blog.csdn.net/qq_32588349/article/details/52097943
Ribbon配置自定义-配置属性配置方式
- 注释掉代码定义
package com.cloud.msclass.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
import com.cloud.ribbonconfiguration.RibbonConfiguration;
/**
* 设置用户微服务适用随机负载均衡策略
* @project ms-class
* @author guanglai.zhou
* @date 2020年2月16日
*/
//@Configuration
//@RibbonClient(name = "ms-user", configuration = RibbonConfiguration.class)
public class MsUserRibbonConfiguration {
}
- 在配置文件中添加如下配置
ms-user:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
细粒度配置最佳实践总结
代码配置:
基于代码,更加灵活
有小坑(父子上下文)、线上修改得重新打包、发布
属性配置
易上手 配置更加直观 线上修改无需重新打包、发布(配置consul可以实现自动刷新)、优先级更高
极端情况下没有代码配置方式灵活
-
尽量使用属性配置,属性方式实现不了再考虑使用代码配置
-
在同一个微服务内尽量保持单一性 比如统一使用属性配置 不建议两种方式混用 增加定位代码的复杂性
Ribbon全局配置
把ribbon配置类放到启动类所在的包或者子包,可以实现全局配置,但强烈不推荐使用,毕竟使父子上下文重叠是一种异常情况。事实上,很多时候都会导致应用无法启动。
首先注释掉之前的配置
#ms-user:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
package com.cloud.msclass.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
import com.cloud.ribbonconfiguration.RibbonConfiguration;
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class GlobalRibbonConfiguration {
}
目前ribbon的全局配置只能通过java代码的方式实现,不能通过配置方式来实现
Ribbon支持的配置项
package com.cloud.ribbonconfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.RandomRule;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
// @Bean
// public IPing ping() {
// 在同一台设备上 导致无法找到服务 ping不通 ?
// return new PingUrl();
// }
}
饥饿加载
当第一次访问http://localhost:8010/lesssons/buy/1时,速度比较慢,以后再次访问,速度就比较快了。
因为ribbon默认情况下是懒加载的,当以下代码在第一次调用的时候:
UserDTO userDTO = restTemplate.getForObject("http://ms-user/users/{userId}", UserDTO.class, userId);
才会创建一个名为ms-user的ribbonClient,而创建那些ribbon的类需要时间
可以通过饥饿加载来解决上面的问题
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: ms-user # 更细粒度 但是目前不支持通配符 如果不配置 则全部使用饥饿加载
扩展Ribbon-优先调用同机房实例
- 同机房优先调用
- 随机负载均衡算法
首先在课程微服务中添加配置
spring.cloud.consul.discovery.tags JIFANG=NJ
定义一个RibbonRule的实现类
package com.cloud.msclass.ribbon;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServer;
import org.springframework.cloud.consul.discovery.ConsulServerUtils;
import org.springframework.util.CollectionUtils;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
public class MyRibbonRule extends AbstractLoadBalancerRule {
@Autowired
private ConsulDiscoveryProperties consulDiscoveryProperties;
public static final String JIFANG = "JIFANG";
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// 读取配置
}
@Override
public Server choose(Object key) {
// 1. 获得想要调用微服务的实例列表
ILoadBalancer loadBalancer = this.getLoadBalancer();
List<Server> servers = loadBalancer.getReachableServers();
// 2. 筛选出机房相同的实例列表
// 可以拿到课程微服务所配置的tag
List<String> tags = consulDiscoveryProperties.getTags();
// 课程微服务所配置的元数据
Map<String, String> metadata = ConsulServerUtils.getMetadata(tags);
List<Server> jiFangMatchServers = servers.stream().filter(server -> {
ConsulServer consulServer = (ConsulServer) server;
Map<String, String> targetMetadata = consulServer.getMetadata();
return Objects.equals(metadata.get(JIFANG), targetMetadata.get(JIFANG));
}).collect(Collectors.toList());
// 3. 随机返回1个实例
if(CollectionUtils.isEmpty(jiFangMatchServers)) {
return this.randomChoose(servers);
}
return this.randomChoose(jiFangMatchServers);
}
private Server randomChoose(List<Server> servers) {
int i = ThreadLocalRandom.current().nextInt(servers.size());
return servers.get(i);
}
}
配置RibbonRule:
package com.cloud.ribbonconfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.cloud.msclass.ribbon.MyRibbonRule;
import com.netflix.loadbalancer.IRule;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new MyRibbonRule();
}
// @Bean
// public IPing ping() {
// return new PingUrl();
// }
}
对于以上实现存在一个漏洞,当在负载均衡时获取到一个服务器,但该服务器突然挂机(或者下线一段时间内),会导致请求失败
解决上面报错的解决思路(3种)
-
主要原因是ServerList的更新不及时。因此将PollingServerListUpdater更新ServerList的定时任务周期缩短
配置 .ribbon.ServerListRefreshInterval=30000 实际项目中还是需要谨慎使用的 主要是系统性能的问题 即使这个时间设置的再小 仍然存在时间间隔
-
配置IPing,将不可用的实例删选掉,这种方法也不能彻底解决问题,因为ribbon不是调用之前去进行ping的,而是定时ping的;另一个时机是当ribbon感知到配置发生变化的时候
-
使用Consul API直接从Consul查询 可以彻底解决问题
编码解决问题
package com.cloud.msclass.ribbon;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.model.HealthService;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAwareLoadBalancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServer;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
public class MyRibbonRuleV2 extends AbstractLoadBalancerRule {
@Autowired
private ConsulDiscoveryProperties consulDiscoveryProperties;
@Autowired
private ConsulClient consulClient;
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
@Override
public Server choose(Object key) {
// 1. 获得想要调用微服务的实例列表
ILoadBalancer lb = this.getLoadBalancer();
ZoneAwareLoadBalancer loadBalancer = (ZoneAwareLoadBalancer) lb;
// 想要调用的微服务的名称(ms-user)
String name = loadBalancer.getName();
// 本地配置的tag
List<String> tags = consulDiscoveryProperties.getTags();
// 筛选出JIFANG=NJ这条数据
String jiFangTag = tags.stream().filter(tag -> tag.startsWith("JIFANG")).findFirst().orElse(null);
// 2. 筛选出机房相同的实例列表
Response<List<HealthService>> serviceResponse = this.consulClient.getHealthServices(name, jiFangTag, true,
QueryParams.DEFAULT);
// 当前健康的用户微服务实例
List<HealthService> healthServices = serviceResponse.getValue();
if (CollectionUtils.isEmpty(healthServices)) {
Response<List<HealthService>> allHealthServiceResponse = this.consulClient.getHealthServices(name, null,
true, QueryParams.DEFAULT);
healthServices = allHealthServiceResponse.getValue();
}
List<ConsulServer> consulServers = healthServices.stream().map(ConsulServer::new).collect(Collectors.toList());
if (CollectionUtils.isEmpty(consulServers)) {
return null;
}
// 3. 随机返回1个实例
return this.randomChoose(consulServers);
}
private Server randomChoose(List<ConsulServer> servers) {
int i = ThreadLocalRandom.current().nextInt(servers.size());
return servers.get(i);
}
}
以上实现存在如下几个问题:
- 性能问题 持续请求consul服务器
- 不使用本地缓存 如果consul服务器崩溃 则课程微服务会失败 此时要保证Consul的高可用
- 最后使用以上方案仅适用于consul,不属于通用方案
因此没有完美的方案 根据项目要求进行权衡选择
本章总结
- 负载均衡的两种方式
- Ribbons是什么,如何使用
- Ribbon核心组件
- 配置自定义
- 如何扩展?