前言
Spring Cloud系列文章大多是有联系的,最好是从第一篇开始看起
Spring Cloud系列: 点击查看Spring Cloud系列文章
SpringCloud Ribbon
SpringCloud Ribbon 是一个基于HTTP 和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松的,面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。
客户端负载均衡即当客户端向后台发出请求的时候,由负载均衡工具根据某种负载均衡策略选择出向哪个服务器发送请求。
负载均衡
负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
负载均衡构建在原有网络结构之上,它提供了一种透明且廉价有效的方法扩展服务器和网络设备的带宽、加强网络数据处理能力、增加吞吐量、提高网络的可用性和灵活性。
ribbon主要模块
ribbon-eureka:基于 Eureka 封装的模块,能够快速、方便地集成 Eureka。包括基于Eureka客户端的负载平衡器实现,主要用于服务注册和发现
ribbon-httpclient:基于 Apache HttpClient 封装的 REST 客户端,集成了负载均衡模块,可以直接在项目中使用来调用接口。
ribbon-core:一些比较核心且具有通用性的代码,包括负载均衡器和客户端接口定义,通用负载均衡器实现,客户端与负载均衡器和客户端工厂的集成。
Ribbon 的负载均衡策略
Ribbon 作为一款客户端负载均衡框架,默认的负载策略是轮询,同时也提供了很多其他的策略,能够让用户根据自身的业务需求进行选择。
以下为Ribbon 的负载均衡策略
1)BestAvailabl
选择一个最小的并发请求的 Server,逐个考察 Server,如果 Server 被标记为错误,则跳过,然后再选择 ActiveRequestCount 中最小的 Server。注:ServerStats有个属性activeRequestCount,这个属性记录的就是server的并发量。
2)AvailabilityFilteringRule
过滤掉那些一直连接失败的且被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来包含过滤 Server 的逻辑。其实就是检查 Status 里记录的各个 Server 的运行状态。
3)ZoneAvoidanceRule
使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 Server,前一个判断判定一个 Zone 的运行性能是否可用,剔除不可用的 Zone(的所有 Server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。
4)RandomRule
随机选择一个 Server。
5)RoundRobinRule
轮询选择,轮询 index,选择 index 对应位置的 Server。
6)RetryRule
对选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 Server。
7)ResponseTimeWeightedRule
作用同 WeightedResponseTimeRule,ResponseTime-Weighted Rule 后来改名为 WeightedResponseTimeRule。
8)WeightedResponseTimeRule
根据响应时间分配一个 Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。
自定义负载均衡策略
除了以上自带的策略,我们还可以自定义负载均衡策略。自定义负载均衡很简单,只需要实现IRule接口或者继承AbstractLoadBalancerRule。
AbstractLoadBalancerRule负载均衡策略抽象类 负责获得负载均衡器 保存在内部 通过负载均衡器维护的信息 作为分配的依据
AbstractLoadBalancerRule替我们提供了设置和获取负载平衡器的默认实现,我们只需要实现choose和initWithNiwsConfig方法即可
以下为AbstractLoadBalancerRule 的源码,AbstractLoadBalancerRule 实现了IRule接口
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
private ILoadBalancer lb;
@Override
public void setLoadBalancer(ILoadBalancer lb){
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer(){
return lb;
}
}
以下是轮询策略的源码实现
//原子类实例
private AtomicInteger nextServerCyclicCounter;
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. 使当前线程由执行状态,变成为就绪状态,让出cpu时间,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。*/
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;
}
private int incrementAndGetModulo(int modulo) {
//死循环
for (;;) {
//cas AtomicInteger 类 保证++的原子性
int current = nextServerCyclicCounter.get();
//线性轮训算法
int next = (current + 1) % modulo;
//compareAndSet的作用是防止多线程下还没执行到这一句 current被修改 如果被修改返回false 重新开始
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
Ribbon配置
超时时间
Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:
#请求连接的超时时间
ribbon.ConnectTimeout=2000
#请求处理的超时时间
ribbon.ReadTimeout=5000
也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
#以Ribbon客户端名称为开头
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
并发参数
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500
重试机制
在集群环境中,用多个节点来提供服务,难免会有某个节点出现故障。用 Nginx 做负载均衡的时候,如果你的应用是无状态的、可以滚动发布的,也就是需要一台台去重启应用,这样对用户的影响其实是比较小的,因为 Nginx 在转发请求失败后会重新将该请求转发到别的实例上去。
由于 Eureka 是基于 AP 原则构建的,牺牲了数据的一致性,每个 Eureka 服务都会保存注册的服务信息,当注册的客户端与 Eureka 的心跳无法保持时,有可能是网络原因,也有可能是服务挂掉了。
在这种情况下,Eureka 中还会在一段时间内保存注册信息。这个时候客户端就有可能拿到已经挂掉了的服务信息,故 Ribbon 就有可能拿到已经失效了的服务信息,这样就会导致发生失败的请求。
这种问题我们可以利用重试机制来避免。重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。
1. RetryRule 重试
解决上述问题,最简单的方法就是利用 Ribbon 自带的重试策略进行重试,此时只需要指定某个服务的负载策略为重试策略即可:
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
2. Spring Retry 重试 除了使用 Ribbon 自带的重试策略,我们还可以通过集成 Spring Retry 来进行重试操作。
在 pom.xml 中添加 Spring Retry 的依赖,代码如下所示。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
配置重试次数等信息:
# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true