一 简介
Ribbon作为spring cloud中常用作为负载均衡的组件。为了保证业务的高可用性,往往都会部署采用集群式的部署(横向扩展)来提供业务服务(一般都是无状态),为了将流量均衡的打到服务器上,或者根据机器的负载程度将流量分布到合适的机器上,往往需要引入负载均衡组件来管理。
负载均衡分为集中式负载均衡和进程内负载均衡:
集中式负载均衡即在服务的消费方和提供方之间使用独立的负载均衡设施,如nginx。通过负载均衡设施将访问请求通过某种负载均衡策略将流量转发至服务的提供方。
进程内负载均衡是将负载均衡集成到消费方,消费方从服务注册中心中获取到可用的地址,然后根据设定好的策略从可用的服务地址中选取一个服务器,Ribbon就属于进程内负载均衡。
二 Ribbon负载均衡规则
1.RoundRobinRule:轮询
2.RandomRule:随机
3.RetryRule:先按照轮询的策略获取服务,如果获取服务失败则在指定时间内进行重试
4.WeightResponseTimeRule:对轮询的扩展,响应速度越快的实例越容易被选中
5.BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
6.AvailabilityFilteringRule 先过滤故障实例,再选择并发较小的实例
7.ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器
三 RoundRobinRule轮询源码初探
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
while(true) {
if (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) {
// 这是轮询的核心,大体思路是:
// 维护一个定增的计数,通过递增的计数模数总服务器实例数,来轮询服务器
// 如 serverCount = 2
// 1 % 2 = 1; 2 % 2 = 0; 3 % 2 = 1; 4 % 2 = 0.....
int nextServerIndex = this.incrementAndGetModulo(serverCount);
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
// 获取到的实例为null,则试着让其他线程来获取
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
if (count >= 10) {
// 最多尝试10次
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
整个算法的逻辑还是很简单,我对其中的代码做了简单的注释,其中最核心的算法我认为是
int nextServerIndex = this.incrementAndGetModulo(serverCount);
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
获取nextServerIndex是通过incrementAndGetModulo方法得到的,考虑到在高并发的场景中,会有多个对象来争抢获取这个nextServerIndex,为了达到轮询的效果,通常我们会考虑对nextServerIndex进行加锁,不管是synchorized还是aqs,都太重了,会带来额外的锁相关的开销。开发人员很巧妙的利用了自旋锁的方式来获取nextServerIndex,
while(!this.nextServerCyclicCounter.compareAndSet(current, next)) 则采用了自旋锁和CAS来保证在高并发下nextServerIndex的逐步定增。个人觉得,能用无锁解决的问题尽量不要使用重量级的锁,为带来额外的开销
四 RandomRule随机源码初探
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
Server server = null;
while(server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int index = this.chooseRandomInt(serverCount);
server = (Server)upList.get(index);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}
server = null;
Thread.yield();
}
}
return server;
}
}
整体思路和RoundRobinRule类似,重点说明下随机算法获取服务器的思想,主要就是通过
ThreadLocalRandom.current().nextInt(serverCount)
来随机获取【0, serverCount)之间的随机数来获取服务实例。注意这里获取随机数的方式是使用了ThreadLocalRandom.current() 而不是Random, 这是因为在高并发下,使用Random存在性能问题,ThreadLocalRandom是ThreadLocal和Random类的组合,它与当前线程隔离。因此,它通过简单地避免对 Random对象的任何并发访问,在多线程环境中实现了更好的性能。调用ThreadLocalRandom.current()方法,它将返回当前线程的ThreadLocalRandom实例。