@[toc]
一、前言
在前面的Ribbon系列文章:
我们聊了以下问题:
- 为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?
- SpringCloud是如何集成Ribbon的?
- Ribbon如何作用到RestTemplate上的?
- 如何获取到Ribbon的ILoadBalancer?
- ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?
- ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?
- 如何根据负载均衡器
ILoadBalancer
从Eureka Client获取到的List<Server>
中选出一个Server?- Ribbon如何发送网络HTTP请求?
- Ribbon如何用IPing机制动态检查服务实例是否存活?
本篇文章我们继续看Ribbon内置了哪些负载均衡策略?RandomRule负载均衡策略的算法是如何实现的?
PS:Ribbon依赖Spring Cloud版本信息如下:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
二、Ribbon内置了哪些负载均衡算法?
- RandomRule --> 随机选择一个Server
- RoundRobinRule --> 轮询选择,轮询Index,选择index对应位置的Server,请求基本平摊到每个Server上。
- WeightedResponseTimeRule --> 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低。
- ZoneAvoidanceRule --> 综合判断Server所在Zone的性能和Server的可用性选择server,在没Zone的环境下,类似于轮询(RoundRobinRule)。默认策略
- BestAvailableRule --> 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。
- RetryRule --> 对选定的负载均衡策略上 重试机制,在一个配置时间段内选择Server不成功,就一直尝试使用subRule(默认是RoundRobinRule)的方式选择一个可用的Server。
AvailabilityFilteringRule --> 过滤掉一直连接失败的(被标记为circuit tripped的)的Server,并过滤掉那些高并发的后端Server 或者 使用一个AvailabilityPredicate来定义过滤Server的逻辑,本质上就是检查status里记录的各个Server的运行状态;其具体逻辑如下:
先用round robin算法,轮询依次选择一台server,如果判断这个server是否是存活的、可用的,如果这台server是不可以访问的,那么就用round robin算法再次选择下一台server,依次循环往复10次,还不行,就走RoundRobin选择。
三、随机算法 --> RandomRule
我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,而choose(Object key)方法中又会调用choose(ILoadBalancer lb, Object key)
方法,所以我们只需要看各个IRule实现类的choose(ILoadBalancer lb, Object key)
方法;
PS:allList和upList的一些疑问和解惑!
最近和一个大V聊了一下RandomRule中Server的选择,随机的下标是以allList的size为基数,而Server的选择则是拿到随机数以upList为准;当时我们考虑极端情况可能存在越界问题!
当天晚上博主又追了一下Ribbon的整个执行流程,结论如下:
- upList和allList是Ribbon维护在自己内存的,在服务启动时会从服务注册中心把服务实例信息拉到upList和allList;
- 后续无论是通过ping机制还是每30s从注册中心拉取全量服务实例列表,
但凡all list发生变更,都会触发一个事件,然后修改本地内存的up list。
- 另外默认ping机制并不会定时每10s执行,因为默认的IPing实现是DummyPing,而
BaseLoadBalancer#canSkipPing()
里会判断IPing实现是DummyPing则不启动Timer定时做Ping机制。Eureka和Ribbon整合之后,EurekaRibbonClientConfiguration(spring-cloud-netflix-eureka-client包下)类中新定义了一个IPing(NIWSDiscoveryPing),此时会启动Timer每10s做一次ping操作。
随机算法体现在RandomRule#chooseRandomInt()方法:
然而,chooseRandomInt()方法中居然使用的不是Random,而是ThreadLocalRandom
,并直接使用ThreadLocalRandom#nextInt(int)方法获取某个范围内的随机值,ThreadLocalRandom是个什么东东?
1、ThreadLocalRandom详解
ThreadLocalRandom
位于JUC(java.util.concurrent
)包下,继承自Random。
1)为什么不用Random?
从Java1.0开始,java.util.Random就已经存在,其是一个线程安全类,多线程环境下,科通通过它获取到线程之间互不相同的随机数,其线程安全性是通过原子类型AtomicLong的变量seed
+ CAS实现的。
尽管Random使用 CAS
操作来更新它原子类型AtomicLong的变量seed,并且在很多非阻塞式算法中使用了非阻塞式原语,但是CAS在资源高度竞争时的表现依然糟糕。
2)ThreadLocalRandom的诞生?
JAVA7在JUC包下增加了该类,意在将它和Random结合以克服Random中的CAS性能问题;
虽然可以使用ThreadLocal<Random>
来避免线程竞争,但是无法避免CAS
带来的开销;考虑到性能诞生了ThreadLocalRandom;ThreadLocalRandom不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。
ThreadLocalRandom的核心实现细节:
- 使用一个普通long类型的变量
SEED
替换Random
中的AtomicLong类型的seed
;- 不能同构构造函数创建ThreadLocalRandom实例,因为它的构造函数是私有的,要使用静态工厂
ThreadLocalRandom.current()
;- 它是CPU缓存感知式的,使用8个long虚拟域来填充64位L1高速缓存行
3)ThreadLocalRandom的错误使用场景
1> 代码示例:
package com.saint.random;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author Saint
*/
public class ThreadLocalRandomTest {
private static final ThreadLocalRandom RANDOM =
ThreadLocalRandom.current();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new SonThread().start();
}
}
private static class SonThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " obtain random value is : " + RANDOM.nextInt(100));
}
}
}
2> 运行结果:
- 居然每个线程获取到的随机值都是一样的!!!
3> 运行结果分析:
上述代码中之所以每个线程获取到的随机值都是一样,因为:
- ThreadLocalRandom 类维护了一个类单例字段,线程通过调用
ThreadLocalRandom#current()
方法来获取ThreadLocalRandom单例对象
;然后以线程维护的实例字段threadLocalRandomSeed
为种子生成下一个随机数和下一个种子值;- 线程在调用 current() 方法的时候,会根据用每个线程 thread 的一个实例字段
threadLocalRandomProbe
是否为 0 来判断当前线程实例是是第一次调用随机数生成方法,进而决定是否要给当前线程初始化一个随机的 threadLocalRandomSeed 种子值。- 所以,如果其他线程绕过 current() 方法直接调用随机数方法(比如nextInt()),那么它的种子值就是可预测的,即一样的。
4)ThreadLocalRandom的正确使用方式
每次要获取随机数时,调用ThreadLocalRandom的正确使用方式是ThreadLocalRandom.current().nextX(int)
:
public class ThreadLocalRandomTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new SonThread().start();
}
}
private static class SonThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " obtain random value is : " + ThreadLocalRandom.current().nextInt(100));
}
}
}
运行结果如下:
5)ThreadLocalRandom源码解析
1> nextInt(int bound)方法获取随机值
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 1. 使用当前种子值SEED获取新种子值,mix32()可以看到是一个扰动函数
int r = mix32(nextSeed());
int m = bound - 1;
// 2. 使用新种子值获取随机数
if ((bound & m) == 0) // power of two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}
当bound=100时,代码执行如下:
2> nextSeed()方法获取下一个种子值
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
//r = UNSAFE.getLong(t, SEED) 获取当前线程中对应的SEED值
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
nextSeed()方法中首先使用 基于主内存地址的Volatile读的方式获取老的SEED种子值,然后再使用 基于主内存地址的Volatile写的方式设置新的SEED种子值;
种子值相关常量:
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 种子值
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
3> 总述
- ThreadLocalRandom中直接基于主内存地址的Volatile读方式读取老SEED值。
- ThreadLocalRandom中直接基于主内存地址的Volatile写方式将老SEED值替换为新SEED值;因为这里的种子值都是线程级别的,所以不需要原子级别的变量,也不会出现多线程竞争修改种子值的情况。
谈到基于主内存地址的Volatile读写,ConCurrentHashMap中也有大量使用,参考博文:https://blog.csdn.net/Saintmm/article/details/122911586。