无论你多有天分,也不论你多么努力,有些事情就是需要时间。让九个女人同时怀孕,小孩也不可能在一个月内出生。
–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
在介绍完了IPing、ServerList、ServerListFilter、ServerListUpdater
之后,终于来到了LoadBalancer
五大核心组件中的最后一个组件:IRule
。它是Ribbon负载均衡器的核心中的核心,是五大组件中最为重要,也是最难理解的部分,本系列将此部分内容将分多篇进行叙述。
Ribbon最重要的能力是负载均衡(注意:Ribbon并不等于负载均衡,只是负载均衡器是它最出名也是最重要的模块),而负载均衡的核心是负载均衡算法,这些算法都将通过IRule的子类一一体现。
正文
IRule
一共提供7种规则算法:
RoundRobinRule
轮询规则:线性轮询WeightedResponseTimeRule
加权轮询规则:Server的rt响应时间作为权重参考,响应时间越短,权重越高,从而越容易被选中RandomRule
随机规则:使用ThreadLocalRandom.current().nextInt(serverCount)
随机选择一个ServerAvailabilityFilteringRule
可用性过滤规则:过滤掉已熔断/负载过高的Server们,然后剩下的Server们使用线性轮询ZoneAvoidanceRule
区域可用性规则:评估zone可用区的性能,然后从多个可用区中按权重选择一个zone,在从zone里面线性轮询出一台ServerRetryRule
重试规则:它可以包装一个IRule subRule
(默认是RoundRobinRule
),当一个周期内没找到Server时,进行重试BestAvailableRule
最小并发请求规则:选择一个最小并发数(也就是ServerStats.activeRequestsCount
最小)的Server
本文将介绍轮询和加权轮询规则。
IRule介绍
为LoadBalancer定义“规则”的接口,它是负载均衡算法的实现。负载均衡策略,根据特定算法中从服务列表中选取一个要访问的Server。
public interface IRule {
// 最为重要的一个方法:从lb.allServers/lb.upServers根据key找到一台Server
// 若没找到返回null
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
它的继承图谱如下(Spring Cloud下并无单独实现):
AbstractLoadBalancerRule
该抽象类仅事把IRule和ILoadBalancer
完成绑定,所以根据Rule选择的Server列表均来自ILoadBalancer
内
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;
}
}
其它所有实现类均基于它来实现。
RoundRobinRule 轮询
轮循算法实现,最广为人知和最基本的负载均衡策略(也叫线性轮询),算法也比较容易理解。
成员属性
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
}
nextServerCyclicCounter
:类似于index,用于控制线性轮询规则AVAILABLE_ONLY_SERVERS/ALL_SERVERS
:本来是用来控制是轮询所有还是只轮询up的机器的,但其实代码里并没有使用到它俩。(规则是轮询所有Server)
算法逻辑
轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
代码实现非常简单,就一小戳:
RoundRobinRule:
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
该算法逻辑在AbstractServerPredicate#incrementAndGetModulo()
也有一个一毛一样的方法,代表都是轮询算法喽。
choose()选择逻辑
依照算法,最终会选择出唯一一个Server供以使用。
RoundRobinRule:
// 注意:它仅是个public方法,并非接口方法
// 另外,它轮询的是lb里的allServers,而并非只是up的哦~~~~
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
// 一次choose中最多轮询10次,如果还没有找到可用的,那就放弃 避免你有100台机器,一直轮询下去
int count = 0;
while (server == null && count++ < 10) {
// 说明:up的机器仅仅只是做一个数量的判断,并不参与整整的轮询
// 真正轮询的是allServers,这点特别注意
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);
// 若选出的这个server是活的并且可以服务了,那就return
// 否则,继续下一轮的循环获取
// 所以,,,为毛不直接轮询upServers呢???这个后文会给出解释
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
server = null;
}
// 10次都还没找到一台可用的,那就不用再找了。返回null
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
轮询过程用一句话总结:把所有的Server使用线性轮询算法选出一台Server供以使用。有如下几点有必要提出:
- 轮询的是
allServers
,而非reachableServers
- 轮询最多只会尝试10次,如果10次还没找到可以提供服务的Server,也不继续往下走
- 其中你应该有个疑问:这里是拿出来再判断,那为毛不直接轮询
reachableServers
呢?这个在后文会给出答案
接口方法实现:
RoundRobinRule:
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
使用场景
轮询调度算法假设所有服务器的处理性能都相同(实际上大多数情况确实是这样的),不关心每台服务器的当前连接数和响应速度。当请求服务间隔时间变化比较大时,轮询调度算法容易导致服务器间的负载不平衡。
所以此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。另外因为这种算法具有人为基本可预测性,所以调试、定位问题方面相对容易点。
轮询策略是Ribbon的默认策略,也几乎是所有的负载均衡器的默认策略。基于它扩展出了带权重的轮询策略,它能满足机器性能不一样的case,这便是它的子类们。
WeightedResponseTimeRule 加权轮询
它继承自轮询算法,相较于它,增加了响应时间作为权重,为每个Server动态分配权重,然后按照权重轮询(加权循环)。
当没有为服务器收集足够的统计信息时,此规则将回退到RoundRobinRule
。所以他是依赖于LoadBalancerStats
统计数据的。
成员属性
public class WeightedResponseTimeRule extends RoundRobinRule {
public static final IClientConfigKey<Integer> WEIGHT_TASK_TIMER_INTERVAL_CONFIG_KEY = CommonClientConfigKey.valueOf("ServerWeightTaskTimerInterval");
public static final int DEFAULT_TIMER_INTERVAL = 30 * 1000;
private int serverWeightTaskTimerInterval = DEFAULT_TIMER_INTERVAL;
private volatile List<Double> accumulatedWeights = new ArrayList<Double>();
private final Random random = new Random();
protected Timer serverWeightTimer = null;
protected AtomicBoolean serverWeightAssignmentInProgress = new AtomicBoolean(false);
String name = "unknown";
}
serverWeightTaskTimerInterval
:多久去重新计算一次Server的权重。默认是30s定时去计算一次。当然你可以通过ServerWeightTaskTimerInterval
这个key去配置,如<clientName>.ribbon.ServerWeightTaskTimerInterval = xxx
accumulatedWeights
:保存从索引0到当前索引的累计权重,例如,index 2处的值表示拥有从0到2的服务器权重之和(每30会重新计算一次)random
:生成随机值。这样权重越大,随机落入的概率就越大serverWeightTimer
:Timer,用于定时调度serverWeightAssignmentInProgress
:标志位。标志是否正在进行server权重的分配name
:ILoadBalancer
的名称
算法逻辑
WeightedResponseTimeRule:
@Override
public Server choose(ILoadBalancer lb, Object key) {
Server server = null;
// 循环内找出一台Server
while (server == null) {
// 获取当前引用,以防它被其他线程更改
List<Double> currentWeights = accumulatedWeights;
// 轮询的也是所有的Server
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
// 列表中的最后一个是所有权值的和
double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);
// 没有服务器被击中,总重量没有初始化,那就回退到正常的轮询逻辑
if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
server = super.choose(getLoadBalancer(), key);
if(server == null) {
return server;
}
} else {
//============权重轮询逻辑=========
// 随机生成一个权重值(0-最大权重),然后选取一片
// 这个算法同ZoneAvoidanceRule.randomChooseZone()算法是一样的
double randomWeight = random.nextDouble() * maxTotalWeight;
int n = 0;
for (Double d : currentWeights) {
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
server = allList.get(serverIndex);
}
if (server.isAlive()) {
return (server);
}
// Next.
server = null;
}
return server;
}
按照收集到的权重值数组,若收集的到的数据不够,回退到线性轮询规则;若够了,就随机生成一个权重值(范围在0-最大权重值之间),然后从中取出一个index,这便是加权轮询的算法。
定时统计权重值
权重算法的数据依据是定时统计权重值。
WeightedResponseTimeRule:
serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval);
class DynamicServerWeightTask extends TimerTask {
@Override
public void run() {
new ServerWeight().maintainWeights();
}
}
WeightedResponseTimeRule.ServerWeight:
// 维护权重的方法。权重值的计算规则
public void maintainWeights() {
// 1、得到所有Server总的rt时间(使用的平均值)
double totalResponseTime = 0;
for (Server server : nlb.getAllServers()) {
totalResponseTime += ssstats.getSingleServerStat(server).getResponseTimeAvg();
}
// 每个服务器的权重为(所有服务器的响应时间总和- responseTime)
// 因此,响应时间越长,权重越小,被选择的可能性就越小
Double weightSoFar = 0.0;
...
setWeights(finalWeights);
}
这个策略整合了随机算法和加权算法(根据响应时间),会开启定时任务,每30秒计算一次所有Provider的响应时间,以响应时间作为权重,响应时间越短的服务器被选中的概率越大。
比如Node1:Node2:Node3的平均响应时间为100ms:200ms:300ms,那么nodes的权重值对应是300:500:600(逆序)。然后每次以600为基础 * 随机值,那么落在 0–300的概率为50%,300–500的概率33%,100–600的概率为17%,也就是平均响应时间越短的节点,被选中的概率越大。
使用场景
该算法更加的智能一些,在父类的基础上增加了弹性(比如对网络的容错性更强),但是在木有分区,其它环境全一样的情况下必要性不大。
实际在生产环境下,我个人建议使用该策略代替默认的线性轮询策略,因为它的效果更好。当然喽,它的坏处是因为随机所以对于单次请求是具有不可预测性的,调试起来稍微困难点
ResponseTimeWeightedRule
已标记为过期,请使用WeightedResponseTimeRule
代替。
代码示例
@Test
public void fun1() throws InterruptedException {
List<Server> serverList = new ArrayList<>();
serverList.add(createServer("华南", 1));
serverList.add(createServer("华东", 1));
serverList.add(createServer("华东", 2));
serverList.add(createServer("华北", 1));
serverList.add(createServer("华北", 2));
serverList.add(createServer("华北", 3));
serverList.add(createServer("华北", 4));
// 轮询策略:因为Servers均来自于lb,所以必须关联上一个lb实例哦
ILoadBalancer lb = new BaseLoadBalancer();
lb.addServers(serverList);
RoundRobinRule rule = new RoundRobinRule(lb);
while (true) {
System.out.println(rule.choose(null));
TimeUnit.SECONDS.sleep(2);
}
}
private Server createServer(String zone, int index) {
Server server = new Server("www.baidu" + zone + ".com", index);
server.setAlive(true);
server.setReadyToServe(true);
server.setZone(zone);
return server;
}
运行程序,控制台打印:
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
www.baidu华北.com:4
www.baidu华南.com:1
www.baidu华东.com:1
www.baidu华东.com:2
www.baidu华北.com:1
www.baidu华北.com:2
www.baidu华北.com:3
www.baidu华北.com:4
www.baidu华南.com:1
...
非常标准的线性轮询有木有。
总结
关于`Ribbon的LoadBalancer五大组件之:IRule的第一篇就先介绍到这,本文概述了IRule以及介绍了其7大负载均衡算法,且着重介绍了最为常用、最广为人知的轮询算法和加权轮询算法的实现,并且辅以示例来加强理解。
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
- [享学Netflix] 一、Apache Commons Configuration:你身边的配置管理专家
- [享学Netflix] 二、Apache Commons Configuration事件监听机制及使用ReloadingStrategy实现热更新
- [享学Netflix] 三、Apache Commons Configuration2.x全新的事件-监听机制
- [享学Netflix] 四、Apache Commons Configuration2.x文件定位系统FileLocator和FileHandler
- [享学Netflix] 五、Apache Commons Configuration2.x别样的Builder模式:ConfigurationBuilder
- [享学Netflix] 六、Apache Commons Configuration2.x快速构建工具Parameters和Configurations
- [享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?
- [享学Netflix] 八、Apache Commons Configuration2.x相较于1.x使用上带来哪些差异?
- [享学Netflix] 九、Archaius配置管理库:初体验及基础API详解
- [享学Netflix] 十、Archaius对Commons Configuration核心API Configuration的扩展实现
- [享学Netflix] 十一、Archaius配置管理器ConfigurationManager和动态属性支持DynamicPropertySupport
- [享学Netflix] 十二、Archaius动态属性DynamicProperty原理详解(重要)
- [享学Netflix] 十三、Archaius属性抽象Property和PropertyWrapper详解
- [享学Netflix] 十四、Archaius如何对多环境、多区域、多云部署提供配置支持?
- [享学Netflix] 十五、Archaius和Spring Cloud的集成:spring-cloud-starter-netflix-archaius
- [享学Netflix] 十六、Hystrix断路器:初体验及RxJava简介
- [享学Netflix] 十七、Hystrix属性抽象以及和Archaius整合实现配置外部化、动态化
- [享学Netflix] 十八、Hystrix配置之:全局配置和实例配置
- [享学Netflix] 十九、Hystrix插件机制:SPI接口介绍和HystrixPlugins详解
- [享学Netflix] 二十、Hystrix跨线程传递数据解决方案:HystrixRequestContext
- [享学Netflix] 二十一、Hystrix指标数据收集(预热):滑动窗口算法(附代码示例)
- [享学Netflix] 二十二、Hystrix事件源与事件流:HystrixEvent和HystrixEventStream
- [享学Netflix] 二十三、Hystrix桶计数器:BucketedCounterStream
- [享学Netflix] 二十四、Hystrix在滑动窗口内统计:BucketedRollingCounterStream、HealthCountsStream
- [享学Netflix] 二十五、Hystrix累计统计流、分发流、最大并发流、配置流、功能流(附代码示例)
- [享学Netflix] 二十六、Hystrix指标数据收集器:HystrixMetrics(HystrixDashboard的数据来源)
- [享学Netflix] 二十七、Hystrix何为断路器的半开状态?HystrixCircuitBreaker详解
- [享学Netflix] 二十八、Hystrix事件计数器EventCounts和执行结果ExecutionResult
- [享学Netflix] 二十九、Hystrix执行过程核心接口:HystrixExecutable、HystrixObservable和HystrixInvokableInfo
- [享学Netflix] 三十、Hystrix的fallback回退/降级逻辑源码解读:getFallbackOrThrowException
- [享学Netflix] 三十一、Hystrix触发fallback降级逻辑的5种情况及代码示例
- [享学Netflix] 三十二、Hystrix抛出HystrixBadRequestException异常为何不会触发熔断?
- [享学Netflix] 三十三、Hystrix执行目标方法时,如何调用线程池资源?
- [享学Netflix] 三十四、Hystrix目标方法执行逻辑源码解读:executeCommandAndObserve
- [享学Netflix] 三十五、Hystrix执行过程集大成者:AbstractCommand详解
- [享学Netflix] 三十六、Hystrix请求命令:HystrixCommand和HystrixObservableCommand
- [享学Netflix] 三十七、源生Ribbon介绍 — 客户端负载均衡器
- [享学Netflix] 三十八、Ribbon核心API源码解析:ribbon-core(一)IClient请求客户端
- [享学Netflix] 三十九、Ribbon核心API源码解析:ribbon-core(二)IClientConfig配置详解
- [享学Netflix] 四十、Ribbon核心API源码解析:ribbon-core(三)RetryHandler重试处理器
- [享学Netflix] 四十一、Ribbon核心API源码解析:ribbon-core(四)ClientException客户端异常
- [享学Netflix] 四十二、Ribbon的LoadBalancer五大组件之:IPing心跳检测
- [享学Netflix] 四十三、Ribbon的LoadBalancer五大组件之:ServerList服务列表
- [享学Netflix] 四十四、netflix-statistics详解,手把手教你写个超简版监控系统
- [享学Netflix] 四十五、Ribbon服务器状态:ServerStats及其断路器原理
- [享学Netflix] 四十六、Ribbon负载均衡策略服务器状态总控:LoadBalancerStats
- [享学Netflix] 四十七、Ribbon多区域选择:ZoneAvoidanceRule.getAvailableZones()获取可用区
- [享学Netflix] 四十八、Ribbon服务器过滤逻辑的基础组件:AbstractServerPredicate
- [享学Netflix] 四十九、Ribbon的LoadBalancer五大组件之:ServerListFilter服务列表过滤器
- [享学Netflix] 五十、Ribbon的LoadBalancer五大组件之:ServerListUpdater服务列表更新器