SOFA 源码分析 — 预热权重
前言
SOFA-RPC 支持根据权重对服务进行预热功能,具体地址:预热权重.
引用官方文档:
预热权重功能让客户端机器能够根据服务端的相应权重进行流量的分发。该功能也常被用于集群内少数机器的启动场景。利用流量权重功能在短时间内对服务端机器进行预热,然后再接收正常的流量比重。 运行机制如下:
1.服务端服务在启动时会将自身的预热时间,预热期内权重,预热完成后的正常权重推送给服务注册中心。如上图 ServiceB 指向 Service Registry 。
2.客户端在引用服务的时候会获得每个服务实例的预热权重信息。如上图 Service Registry 指向 client 。
3.客户端在进行调用的时候会根据服务所在地址的预热时期所对应的权重进行流量分发。如上图 client 指向 ServiceA 和 ServiceB 。 ServiceA 预热完毕,权重默认 100 , ServiceB 处于预热期,权重为 10,因此所承受流量分别为 100%110 和 10%110 。
如何使用
该功能使用方式如下。
ProviderConfig<HelloWordService> providerConfig = new ProviderConfig<HelloWordService>()
.setWeight(100)
.setParameter(ProviderInfoAttrs.ATTR_WARMUP_WEIGHT,"10")
.setParameter(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME,"12000");
如上,该服务的预热期为12s,在预热期内权重为10,预热期结束后的正常权重为100。如果该服务一共发布在两个机器A,B上,A机器正处于预热期内,并使用上述配置,B已经完成预热,正常权重为200。那么客户端在调用的时候,此时流量分发的比重为10:200,A机器预热结束后,流量分发比重为100:200。 在SOFABoot中,如下配置预热时间,预热期间权重和预热完后的权重即可。
<sofa:reference id="sampleRestFacadeReferenceBolt" interface="com.alipay.sofa.endpoint.facade.SampleFacade">
<sofa:binding.bolt>
<sofa:global-attrs weight="100" warm-up-time="10000" warm-up-weight="1000"/>
</sofa:binding.bolt>
</sofa:reference>
再来看看源码实现。
源码分析
从 demo 中看,SOFA 需要在 ProviderConfig 中配置属性,而这些属性都是保存在一个 Map 中。
代码:
public S setParameter(String key, String value) {
if (parameters == null) {
parameters = new ConcurrentHashMap<String, String>();
}
if (value == null) {
parameters.remove(key);
} else {
parameters.put(key, value);
}
return castThis();
}
当发布服务的时候,这个 Map 会被发布到注册中心。具体代码如下:
protected void doRegister(String appName, String serviceName, ProviderInfo providerInfo) {
if (LOGGER.isInfoEnabled(appName)) {
LOGGER.infoWithApp(appName, LogCodes.getLog(LogCodes.INFO_ROUTE_REGISTRY_PUB, serviceName));
}
//{service : [provider...]}
ProviderGroup oldGroup = memoryCache.get(serviceName);
if (oldGroup != null) { // 存在老的key
oldGroup.add(providerInfo);
} else { // 没有老的key,第一次加入
List<ProviderInfo> news = new ArrayList<ProviderInfo>();
news.add(providerInfo);
memoryCache.put(serviceName, new ProviderGroup(news));
}
// 备份到文件 改为定时写
needBackup = true;
doWriteFile();
if (subscribe) {
notifyConsumerListeners(serviceName, memoryCache.get(serviceName));
}
}
上面的代码中,提供者会将 providerInfo 的信息写到本地文件(注册中心)中。
而消费者则会从注册中心订阅服务列表的信息。具体代码如下:
@Override
public List<ProviderGroup> subscribe(ConsumerConfig config) {
String key = LocalRegistryHelper.buildListDataId(config, config.getProtocol());
List<ConsumerConfig> listeners = notifyListeners.get(key);
if (listeners == null) {
listeners = new ArrayList<ConsumerConfig>();
notifyListeners.put(key, listeners);
}
listeners.add(config);
// 返回已经加载到内存的列表(可能不是最新的)
ProviderGroup group = memoryCache.get(key);
if (group == null) {
group = new ProviderGroup();
memoryCache.put(key, group);
}
return Collections.singletonList(group);
}
上面这段代码会被 DefaultConsumerBootstrap 调用,根据消费者的配置信息,生成一个 key,然后将消费者添加到通知列表中(当数据变化时,通知消费者,由定时任务执行)。
然后,从内存中取出key 对应的服务分组,并返回集合(就是提供者注册的信息)。
这段代码会在 AbstractCluster 的 init 方法中调用—— List<ProviderGroup> all = consumerBootstrap.subscribe();
。
服务分组的数据结构是 ProviderInfo,是一个抽象的服务提供列表,其中包含服务的信息,比如地址,协议类型,主机地址,端口,路径,版本,动态参数,静态参数,服务状态等等,其中就包括权重。
获取权重的方法如下:
public int getWeight() {
ProviderStatus status = getStatus();
if (status == ProviderStatus.WARMING_UP) {
try {
// 还处于预热时间中
Integer warmUpWeight = (Integer) getDynamicAttr(ProviderInfoAttrs.ATTR_WARMUP_WEIGHT);
if (warmUpWeight != null) {
return warmUpWeight;
}
} catch (Exception e) {
return weight;
}
}
return weight;
}
注意 getStatus 方法:
public ProviderStatus getStatus() {
if (status == ProviderStatus.WARMING_UP) {
if (System.currentTimeMillis() > (Long) getDynamicAttr(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME)) {
// 如果已经过了预热时间,恢复为正常
status = ProviderStatus.AVAILABLE;
setDynamicAttr(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME, null);
}
}
return status;
}
逻辑如下:
获取服务状态,如果是预热状态,则获取预热状态的权重值,反之,如果不是,反之正常值(默认 100)。
获取状态的方法则是判断时间,如果当前时间大于预热时间,则修改状态为可用。并删除动态参数列表中的“预热时间”。
那么,什么时候会获取权重呢?
如果看过之前文章的同学肯定知道,在负载均衡的时候,会调用。
我们看看默认的随机均衡算法。还记得当时,楼主有个地方不是很明白,我们要根据权重随机,当时看来,并没有什么用处,今天明白了。再上一遍代码吧:
@ AbstractLoadBalancer.java
protected int getWeight(ProviderInfo providerInfo) {
// 从provider中或得到相关权重,默认值100
return providerInfo.getWeight() < 0 ? 0 : providerInfo.getWeight();
}
获取权重,默认 100.
再看随机算法的 doSelect 方法。
@ RandomLoadBalancer.java
@Override
public ProviderInfo doSelect(SofaRequest invocation, List<ProviderInfo> providerInfos) {
ProviderInfo providerInfo = null;
int size = providerInfos.size(); // 总个数
int totalWeight = 0; // 总权重
boolean isWeightSame = true; // 权重是否都一样
for (int i = 0; i < size; i++) {
int weight = getWeight(providerInfos.get(i));
totalWeight += weight; // 累计总权重
if (isWeightSame && i > 0 && weight != getWeight(providerInfos.get(i - 1))) {
isWeightSame = false; // 计算所有权重是否一样
}
}
if (totalWeight > 0 && !isWeightSame) {
// 如果权重不相同且权重大于0则按总权重数随机
int offset = random.nextInt(totalWeight);
// 并确定随机值落在哪个片断上
for (int i = 0; i < size; i++) {
offset -= getWeight(providerInfos.get(i));
if (offset < 0) {
providerInfo = providerInfos.get(i);
break;
}
}
} else {
// 如果权重相同或权重为0则均等随机
providerInfo = providerInfos.get(random.nextInt(size));
}
return providerInfo;
}
首先判断各个服务的权重是否相同,如果不同,进入第二个 if。
关键点来了,如果权重不同,那么从总的权重中,随机一个数,一次从服务列表的权重递减。知道该值小于0,那么就使用该服务。
这样就能大致保证权重小的被击中的几率较小。具体取决于 Java 的随机算法,但是我们还是比较相信 Java 的。
我们来推倒一下这个算法。
假设有 A, B, C, 3 个服务,每个服务默认权重 100,其中 C 现在处于预热阶段,则 C 的权重等于 10.
那么总权重 210。
随机一个数,
假设是 199,那么当这个算法运行结束,C,永远不会被选择到;
假设这个随机数是 201,那么当这个算法运行结束,极有可能会击中 C(C 在最后一位,即集合的排序是倒序)。但我好像没有从代码中看到倒序排序。
假设是倒叙的,那么,C 被击中的概率为 10/210。符合 SOFA 文档的介绍。
总结
现在看来,预热权重还是挺简单的,但最好要保证最小的权重放到最后,这样能够更加完美,保证公平。我已经在 SOFA 上提了这个 issue。
今天就到这里,bye!!!
重要更新
先说结论:SOFA 的权重算法没有漏洞。
关于之前说的:应该把最小权重放到最后的说法,是错误的。
重新推导:
假设有 A, B, C, 3 个服务,每个服务默认权重 100,其中 C 现在处于预热阶段,则 C 的权重等于 10.
那么总权重 210。
如果C落在第一位,那么一定会选中C的情况是权重落在0-9之间;
如果C落在第二位,那么一定会选中C的情况是权重落在100-109之间;
如果C是在第三位,那么一定会选中C的情况是权重落在200-209;
无论如何,都能保证权重是 10/210。