A simple failover library for Java.
用于构建高性能的客户端(主调方)自适应负载均衡和自动重试能力。
- jdk1.8+
// 添加多个被调用资源,这里的被调用资源是指目标服务器(有多个服务器提供相同的服务),下面例子展示了3个,每个权重100
String server1 = "192.168.1.1";
String server1 = "192.168.1.2";
String server1 = "192.168.1.3";
SimpleFailover<String> failover = PriorityFailover.newBuilder()
.addResource(server1, 100.0)
.addResource(server2, 100.0)
.addResource(server3, 100.0)
.build();
String server = failover.getOneAvailable();
// 获取一个资源执行操作,上面添加了3个,并且初始权重相同,所以刚开始的时候每个资源被选中的概率是1/3
boolean success = doSomethingWithServer(server);
if(success) {
// 成功会增加权重,最大不会超过上面指定的100
failover.success(server);
} else {
// 失败扣减权重,降低下次被选择的概率,最小扣到0
failover.fail(server);
// 如果需要重试1次
server = failover.getOneAvailableExclude(Collections.singleton(server));
doSomethingWithServer(server);
// ...
}
本类库设计用来支撑百万TPS(单主调方)级别的RPC client的构建,因此它本身要达到千万TPS级别才能保证不成为瓶颈。
- 当被调资源全部完全健康(当前权重=最大权重)且最大权重相同时,使用round robin算法,时间复杂度O(1)
- 当被调资源数>10,最大权重不同,且全部完全健康时,使用AliasMethod算法,时间复杂度O(1)
- 其它情况下时间复杂度为O(N)。当开启了优先级分组以后,N为最高优先级的资源数,这个数字通常较小,从而大幅提升了性能。
核心类PriotiryFailover是线程安全的,无锁实现保证高性能。 PriorityFailoverManager/PriorityGroupManager的变更方法(通常资源上下线才使用)都不是线程安全的,变更时请自行加锁,PriorityFailoverBuilder类一般来说是使用完就丢弃的,也不是线程安全的。
单元测试代码中BenchmarkMain类可以用来做性能测试,文末有测试结果表,不同的工况下有不同的性能数值,整个表格中只有单线程N较大的一个场景下没有达标,大部分场景都可以达到数千万TPS。
默认情况下,每次调用失败将当前权重减半,例如: 初始权重100.0,第一次失败后(调用fail方法后)权重为50.0,第二次失败后25.0,第三次失败后12.5。
每次成功后将增加最大权重的1%,例如: 最大权重100,当前权重12.5,成功后(调用success方法后),权重为13.5。
所以这个默认算法是等比递减、等差递增。等比递减可以保证被调用服务器不稳定的情况下权重迅速下降(尽快摘除)。如果需要调整参数:
PriorityFailoverBuilder<String> buidler = PriorityFailover.newBuilder();
// 每次失败后只保留当前权重的0.1,成功后增加最大权重的0.01
buidler.weightFunction(new RatioWeightFunction(
/*failKeepRateOfCurrentWeight*/0.1, /*successIncreaseRateOfMaxWeight*/0.01));
如果想等差扣减权重:
// 每次失败后扣酱最大权重的0.05,成功后增加最大权重的0.01
buidler.weightFunction(new SimpleWeightFunction(
/*failDecreaseRate*/0.05, /*successIncreaseRate*/0.01));
不健康的资源,通过后台线程运行主动健康检查恢复权重,需要设置检查器和检查间隔参数:
buidler.checker(server -> pingServer(server));
builder.checkDuration(Duration.ofSeconds(60));
默认情况下,权重变成0算作不健康,健康检查成功1次就立刻恢复权重。这个行为可以通过WeightFunction定制:
buidler.weightFunction(new RatioWeightFunction(/*failKeepRateOfCurrentWeight*/0.5,
/*successIncreaseRateOfMaxWeight*/0.01, /*recoverThreshold*/2, /*downThreshold*/0.1));
以上第一个参数指定每次调用失败后权重减半,第二个参数指定调用成功后增加最大权重的0.01,第三个参数指定调用主动健康检查连续成功两次才可以增加权重(增加的数值由第二个参数指定),第四个参数指定当前权重小于0.1以后直接变成为0(否则等比递减很难减少到0,此时不会有主动健康检查)。
如果希望完全健康的时候也执行健康检查也可以通过weightFunction覆盖needCheck方法来定制。 另外检查任务是懒启动的,需要有一次fail或者down的调用才会启动,如果不想这样(例如完全健康的节点也想定期检查),可以在builder上设置startCheckTaskImmediately。
一般来说最小权重就是0,一个资源连续调用失败,它的权重应该被扣减到0,以免新的请求发到它那里去。
但有的时候我们不想这样,比如,我们总共只有两个集群,希望无论如何都选择一个相对健康的集群发送请求,可以通过以下代码指定最小权重是0.1:
SimpleFailover<String> failover = PriorityFailover.newBuilder()
.addResource(server1, /*maxWeight*/100.0, /*minWeight*/0.1)
...
主调方已经启动的情况下,被调资源有变动,这在分布式rpc场景是很常见的,可以通过
Object server1 = "s1";
Object server2 = "s2";
Object server3 = "s3";
PriorityFailoverManager<Object> manager = PriorityFailover.newBuilder()
.addResource(server1, 100)
.addResource(server2, 100)
.buildManager();
Object obj = manager.getFailover().getOneAvailable();
// 总是通过manager拿到PriorityFailover来用
// 当资源需要变更的时候
HashMap<Object, ResConfig> map = new HashMap<>();
map.put(server2, new ResConfig(/*maxWeight*/100));
map.put(server3, new ResConfig(/*maxWeight*/100));
manager.updateAll(map); //还有个update方法可以做增量更新
// 之后manager.getFailover()取出来的是个新的failover,删除了server1,添加了server3
变更后原有资源(上面的例子是server2)的权重会从之前的failover继承下来。updateAll的时候最大权重等参数可以改。
以上代码展示了全量变更,通过update方法可以实现增量变更。
上面的代码中,server3是个新加的资源,为了防止新上线的实例被打爆,我们可以把它的初始权重设置为5。
map.put(server3, new ResConfig(/*maxWeight*/100, /*minWeight*/0, /*priority*/0, /*initWeight*/5));
这样,新上的server3只有5%的流量,随着调用的成功,它的权重会慢慢增加到100。
多个被调资源的性能能力可能是不一样的,可能是因为硬件性能不同,也可能是某个资源上还运行了其它服务等原因。 虽然我们可以在addResource的时候为它们设置不同的最大权重,但是复杂场景下我们很难估计好不同资源之间的最大权重比,何况资源的能力可能在运行时变化。 假设现在有个被调资源在有性能压力,刚开始不会失败,如果持续高流量进入,会导致这个对这个资源的调用开始出错。所以我们希望,当某个资源有性能问题的苗头时,就减少对它的流量。
这个性能问题可以通过响应时间反应出来,在我们的代码中也就是调用getOneAvailable和调用success/fail的时间差。所以我们引入并发度的概念:
builder.concurrencyControl(true);
通过以上代码激活并发度控制,并发度高的资源,流量会减少。 并发度初始值是0,getOneAvailable会将并发度加1,success/fail/down会将并发度减1,内部选择资源的时候,当前权重会除以(并发度+1),也就是说如果并发度为1,有效权重就会减半(除以2),并发度是2时,有效权重就会变为1/3。
有个要求是getOneAvailable取出来的资源用完后,必须通过success/fail/down放回去,否则并发度计算会错误。
被调资源很多的情况下(比如有1000台服务器),我们不想把请求均匀的发到这些服务器上,因为这会导致:
- 导致底下的网络长连接、连接池不能很好的复用
- 如果主调方的访问的数据有局部性,可能导致被调方缓存命中率降低
- 被调方和主调方可能位置相关的特性,比如机房、可用区,希望调用时本地优先
构建的时候:
.addResource(server1, /*maxWeight*/100, /*minWeight*/0, /*priority*/0, /*initWeight*/100))
.addResource(server2, /*maxWeight*/100, /*minWeight*/0, /*priority*/0, /*initWeight*/100))
.addResource(server3, /*maxWeight*/100, /*minWeight*/0, /*priority*/1, /*initWeight*/100))
.addResource(server4, /*maxWeight*/100, /*minWeight*/0, /*priority*/1, /*initWeight*/100))
以上4个服务器分为2个group,当优先级为0的group变的不健康以后,流量会逐渐溢出到优先级为1的group。具体可看PriorityFailover设计。
注意流量是逐渐溢出的,具体算法与envoy类似,默认因子也是1.4,如果想要第一个group里面的资源死光光以后再溢出,可以在builder上设置:
builder.priorityFactor(Double.MAX_VALUE)
比如,机房里有300个RPC被调资源,想分为2组,第一组5个,剩下的归为第二组:
builder.enableAutoPriority(5);
如果希望分3组,第一个组5个,第二组20个,剩下的归为第三组,可以这样:
builder.enableAutoPriority(5, 20)
PriorityFailoverManager也能够支持自动优先级管理。
使用自动优先级管理,资源变更时,新增的资源和已有资源一样有均等的机会进入高优先级组; 同时已有的资源本来会在高优先级组的仍然会优先,以尽量保持调用的粘性,减少变更的影响,详见PriorityGroupManager类。
可以在builder上设置weightListener:
public interface WeightListener<T> {
void onSuccess(double maxWeight, double minWeight, double currentNewWeight, T resource);
void onFail(double maxWeight, double minWeight, double currentNewWeight, T resource);
}
需要注意这个回调仅在资源权重变更的时候触发(完全健康状态下再次成功是没有onSuccess回调的,onFail也类似)。
- 测试环境,笔记本,I7-9750H,6核12线程,2.6G~4.5G
- Score数值为千TPS,即要乘以1000。
- concurrencyCtrl代表是否打开并发度控制,totalSize代表资源数,coreSize代表分2组优先级情况下,priority=0的资源数,剩下的priority=1。
- getOneSuccess代表getOneAvailable获取资源以后,接下来调用failover.success(res)方法,模拟资源调用成功。
- getOneFail代表getOneAvailable获取资源以后,模拟1/1000的资源调用失败。资源调用失败会造成性能failover劣化,时间复杂度会退化到O(N),同时多线程场景下对volatile变量的写也会导致性能降低。
1线程:
Benchmark (concurrencyCtrl) (coreSize) (totalSize) Mode Cnt Score Error Units
Group1PriorityFailover.getOneFail N/A N/A 5 thrpt 2 66408.751 ops/s
Group1PriorityFailover.getOneFail N/A N/A 20 thrpt 2 68001.154 ops/s
Group1PriorityFailover.getOneFail N/A N/A 100 thrpt 2 47727.860 ops/s
Group1PriorityFailover.getOneFail N/A N/A 200 thrpt 2 25812.804 ops/s
Group1PriorityFailover.getOneFail N/A N/A 1000 thrpt 2 2433.041 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 5 thrpt 2 95741.988 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 20 thrpt 2 92530.019 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 100 thrpt 2 85027.270 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 200 thrpt 2 84072.456 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 1000 thrpt 2 56069.687 ops/s
Group2PriorityFailover.getOneFail true 5 1000 thrpt 2 17725.844 ops/s
Group2PriorityFailover.getOneFail true 20 1000 thrpt 2 9911.852 ops/s
Group2PriorityFailover.getOneFail false 5 1000 thrpt 2 66638.774 ops/s
Group2PriorityFailover.getOneFail false 20 1000 thrpt 2 65462.617 ops/s
Group2PriorityFailover.getOneSuccess true 5 1000 thrpt 2 17721.040 ops/s
Group2PriorityFailover.getOneSuccess true 20 1000 thrpt 2 10192.638 ops/s
Group2PriorityFailover.getOneSuccess false 5 1000 thrpt 2 84482.401 ops/s
Group2PriorityFailover.getOneSuccess false 20 1000 thrpt 2 87764.225 ops/s
5线程:
Benchmark (concurrencyCtrl) (coreSize) (totalSize) Mode Cnt Score Error Units
Group1PriorityFailover.getOneFail N/A N/A 5 thrpt 2 35019.174 ops/s
Group1PriorityFailover.getOneFail N/A N/A 20 thrpt 2 29588.433 ops/s
Group1PriorityFailover.getOneFail N/A N/A 100 thrpt 2 24206.241 ops/s
Group1PriorityFailover.getOneFail N/A N/A 200 thrpt 2 26622.312 ops/s
Group1PriorityFailover.getOneFail N/A N/A 1000 thrpt 2 13794.910 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 5 thrpt 2 39493.298 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 20 thrpt 2 37636.730 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 100 thrpt 2 39736.684 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 200 thrpt 2 40909.686 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 1000 thrpt 2 44379.175 ops/s
Group2PriorityFailover.getOneFail true 5 1000 thrpt 2 13332.955 ops/s
Group2PriorityFailover.getOneFail true 20 1000 thrpt 2 12736.631 ops/s
Group2PriorityFailover.getOneFail false 5 1000 thrpt 2 30005.073 ops/s
Group2PriorityFailover.getOneFail false 20 1000 thrpt 2 29428.004 ops/s
Group2PriorityFailover.getOneSuccess true 5 1000 thrpt 2 15578.987 ops/s
Group2PriorityFailover.getOneSuccess true 20 1000 thrpt 2 20515.372 ops/s
Group2PriorityFailover.getOneSuccess false 5 1000 thrpt 2 38486.222 ops/s
Group2PriorityFailover.getOneSuccess false 20 1000 thrpt 2 37179.500 ops/s
200线程
Benchmark (concurrencyCtrl) (coreSize) (totalSize) Mode Cnt Score Error Units
Group1PriorityFailover.getOneFail N/A N/A 5 thrpt 2 45083.860 ops/s
Group1PriorityFailover.getOneFail N/A N/A 20 thrpt 2 43295.450 ops/s
Group1PriorityFailover.getOneFail N/A N/A 100 thrpt 2 39304.540 ops/s
Group1PriorityFailover.getOneFail N/A N/A 200 thrpt 2 39092.222 ops/s
Group1PriorityFailover.getOneFail N/A N/A 1000 thrpt 2 26619.782 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 5 thrpt 2 60798.031 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 20 thrpt 2 59382.891 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 100 thrpt 2 60453.622 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 200 thrpt 2 58952.071 ops/s
Group1PriorityFailover.getOneSuccess N/A N/A 1000 thrpt 2 55245.930 ops/s
Group2PriorityFailover.getOneFail true 5 1000 thrpt 2 17456.668 ops/s
Group2PriorityFailover.getOneFail true 20 1000 thrpt 2 19482.938 ops/s
Group2PriorityFailover.getOneFail false 5 1000 thrpt 2 41700.014 ops/s
Group2PriorityFailover.getOneFail false 20 1000 thrpt 2 40878.123 ops/s
Group2PriorityFailover.getOneSuccess true 5 1000 thrpt 2 23196.162 ops/s
Group2PriorityFailover.getOneSuccess true 20 1000 thrpt 2 31493.098 ops/s
Group2PriorityFailover.getOneSuccess false 5 1000 thrpt 2 54561.672 ops/s
Group2PriorityFailover.getOneSuccess false 20 1000 thrpt 2 53995.894 ops/s