Ribbon负载均衡
1、什么是Ribbon
Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它是基于Netflix Ribbon实现的。它不像Spring Cloud服务注册中心、配置中心、API网关那样独立部署,但是它几乎存在于每个Spring Cloud微服务中,包括Feign提供的声明式服务调用也是基于该Ribbon实现的。
Ribbon默认提供多种负载均衡算法,例如:轮训、随机等等。也可以自定义负载均衡算法。
2、负载均衡不同方案的区别
目前业界主流的负载均衡方案可分成两类:
- 集中式负载均衡(服务器负载均衡),即在consumer和provider之间使用独立的负载均衡设施(可以是硬件,如F5,也可以说是软件,如nginx)由该设施负载把访问请求通过某种策略转发。
- 进程内负载均衡(客户端负载均衡),将负载均衡逻辑集成到consumer,consumer从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的provider。
3、Ribbon负载均衡策略
3.1、轮询策略(默认)
com.netflix.loadbalancer.RoundRobinRule
实现原理:接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务器重启后接口计数从1开始。
比如一共有5个provider,第一次取第一个,第二次取第二个,第三次取第三个,以此类推。
3.2、权重轮询策略
com.netflix.loadbalancer.WeightedResponseTimeRule
对RoundRobinRule的扩张,根据每个provider的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性就越低。
实现原理:一开始为轮询策略,并开启一个计时器,每30秒收集一次每个provider的平均响应时间,当信息足够时,给每个provider附上一个权重值,并按权重随机选择provider,高权重的provider会被高概率选中。
3.3、随机策略
com.netflix.loadbalancer.RandomRule
实现原理:随机从provider列表中选择一个。
3.4、最少并发数策略
com.netflix.loadbalancer.BestAvailableRule
实现原理:会先过滤掉由于多次访问故障而处于熔断的服务,然后选择一个并发最小的服务
3.5、重试策略
com.netflix.loadbalancer.RetryRule
实现原理:其实就是轮询策略的增强版,轮询策略服务不可用时不做处理,重试策略服务不可用时会重新尝试集群中的其他节点。
3.6、可用性敏感策略
com.netflix.loadbalancer.AvailabilityFilteringRule
实现原理:先过滤故障实例,再选择并发较小的实例
- 第一种:过滤掉在Eureka中一直处于连接失败的provider
- 第二种:过滤掉高并发(繁忙)的provider
3.7、区域敏感性策略
com.netflix.loadbalancer.ZoneAvoidanceRule
实现原理:
- 以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下的区域中选择可用的provider
- 如果这个ip区域内有一个或者多个实例不可达成或响应变慢,都会降低该ip区域内其他ip被选中的权重
4、Ribbon入门案例
Ribbon默认采用轮询的负载策略。
实例代码:https://gitee.com/junweihu/eureka-demo
private List<Product> selectProductListByLoadBalancerClient() {
StringBuffer sb = null;
// 根据服务名称获取服务
ServiceInstance providerInstances = loadBalancerClient.choose("SERVICE-PROVIDER");
if (providerInstances == null) {
return null;
}
sb = new StringBuffer();
String host = providerInstances.getHost();
int port = providerInstances.getPort();
sb.append("http://" + host + ":" + port + "/product/list");
log.info("call address:{}", sb.toString());
ResponseEntity<List<Product>> exchange = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {
});
return exchange.getBody();
}
5、Ribbon负载均衡策略设置
5.1、全局
在启动类或配置类中注入负载均衡策略对象。所有服务请求均使用该策略。
/**
* 随机负载均衡
* @return
*/
@Bean
public RandomRule randomRule() {
return new RandomRule();
}
多次访问结果如下
5.2、局部
修改配置文件,指定服务的负载均衡策略。格式:服务应用名.ribbon.NFLoadBalancerRuleClassName
# 负载均衡策略
# service-provider为调用的服务名称
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
6、Ribbon点对点直连
点对点直连是指绕过注册中心,直连服务提供者获取服务,一般用在测试。
6.1添加依赖
在pom中引入Ribbon的依赖,注意如果pom中有Eureka的依赖,需要剔除Eureka的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
6.2配置文件
配置文件中关闭Eureka,添加直连的服务地址。如果不设置负载均衡策略默认使用轮询策略。
# 负载均衡策略
# service-provider为调用的服务名称
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 指定具体的服务列表,多个用逗号间隔
listOfServers: http://localhost:7070,http://localhost:7071
# 关闭Eureka实现点对点
ribbon:
eureka:
enabled: false
6.3访问
关闭Eureka注册中心,服务提供者由于无法注册到注册中心所以连接异常。但是服务是可以正常访问的。
7、Ribbon负载均衡算法
Ribbon核心组件IRule:根据特定算法从服务列表中选取一个要访问的服务
7.1、源码
RoundRobinRule源码分析,负载均衡的核心在choose方法中体现
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
// 存活可触达的服务列表
List<Server> reachableServers = lb.getReachableServers();
// 所有服务列表,包含可触达和不可触达
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
//没有可用的服务
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 获取服务列表的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
// CAS自旋获取当前实例下标值
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
// 实际调用服务器位置下标=接口第几次请求数%服务器集群总数量
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
7.2、自定义
定义负载均衡接口
public interface ILoadBalancer {
ServiceInstance instance(List<ServiceInstance> serviceInstances);
}
接口实现类
@Component
@Slf4j
public class MyLB implements ILoadBalancer{
//记录当前请求次数
private AtomicInteger currentRequest = new AtomicInteger(0);
@Override
public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
ServiceInstance instance = serviceInstances.get(index);
return instance;
}
private final int getAndIncrement() {
int current, next;
do {
current = this.currentRequest.get();
next = current > Integer.MAX_VALUE ? 0 : current + 1;
} while (!this.currentRequest.compareAndSet(current, next));
log.info("当前第:{}次访问", next);
return next;
}
}
使用
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private ILoadBalancer loadBalancer;
public List<Product> selectProductListByMyLb() {
// 获取服务列表
List<String> services = discoveryClient.getServices();
if (CollectionUtils.isEmpty(services)) {
return null;
}
// 根据服务名称获取服务
List<ServiceInstance> providerInstances = discoveryClient.getInstances("service-provider");
if (CollectionUtils.isEmpty(providerInstances)) {
return null;
}
ServiceInstance instance = loadBalancer.instance(providerInstances);
String host = instance.getHost();
int port = instance.getPort();
StringBuffer sb = new StringBuffer("http://" + host + ":" + port + "/product/list");
log.info("call address:{}", sb.toString());
ResponseEntity<List<Product>> exchange = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {
});
return exchange.getBody();
}
效果