前言

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。

SOFA 源码分析 — 预热权重的更多相关文章

  1. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  2. SOFA 源码分析 — 自动故障剔除

    前言 集群中通常一个服务有多个服务提供者.其中部分服务提供者可能由于网络,配置,长时间 fullgc ,线程池满,硬件故障等导致长连接还存活但是程序已经无法正常响应.单机故障剔除功能会将这部分异常的服 ...

  3. SOFA 源码分析 — 负载均衡和一致性 Hash

    前言 SOFA 内置负载均衡,支持 5 种负载均衡算法,随机(默认算法),本地优先,轮询算法,一致性 hash,按权重负载轮询(不推荐,已被标注废弃). 一起看看他们的实现(重点还是一致性 hash) ...

  4. SOFA 源码分析 —— 服务发布过程

    前言 SOFA 包含了 RPC 框架,底层通信框架是 bolt ,基于 Netty 4,今天将通过 SOFA-RPC 源码中的例子,看看他是如何发布一个服务的. 示例代码 下面的代码在 com.ali ...

  5. SOFA 源码分析 — 调用方式

    前言 SOFARPC 提供了多种调用方式满足不同的场景. 例如,同步阻塞调用:异步 future 调用,Callback 回调调用,Oneway 调用. 每种调用模式都有对应的场景.类似于单进程中的调 ...

  6. SOFA 源码分析— 事件总线

    前言 大部分框架都是事件订阅功能,即观察者模式,或者叫事件机制.通过订阅某个事件,当触发事件时,回调某个方法.该功能非常的好用,而 SOFA 内部也设计了这个功能,并且内部大量使用了该功能.来看看是如 ...

  7. SOFA 源码分析 — 自定义线程池原理

    前言 在 SOFA-RPC 的官方介绍里,介绍了自定义线程池,可以为指定服务设置一个独立的业务线程池,和 SOFARPC 自身的业务线程池是隔离的.多个服务可以共用一个独立的线程池. API使用方式如 ...

  8. SOFA 源码分析 — 链路数据透传

    前言 SOFA-RPC 支持数据链路透传功能,官方解释: 链路数据透传功能支持应用向调用上下文中存放数据,达到整个链路上的应用都可以操作该数据. 使用方式如下,可分别向链路的 request 和 re ...

  9. SOFA 源码分析 —— 过滤器设计

    前言 通常 Web 服务器在处理请求时,都会使用过滤器模式,无论是 Tomcat ,还是 Netty,过滤器的好处是能够将处理的流程进行分离和解耦,比如一个 Http 请求进入服务器,可能需要解析 h ...

随机推荐

  1. 【一天一道LeetCode】#77. Combinations

    一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Given t ...

  2. Zookeeper实现master选举

    使用场景         有一个向外提供的服务,服务必须7*24小时提供服务,不能有单点故障.所以采用集群的方式,采用master.slave的结构.一台主机多台备机.主机向外提供服务,备机负责监听主 ...

  3. Get Form type using javascript in CRM 2011

    Get from type var type = Xrm.Page.ui.getFormType(); getFromType() function returns integer value for ...

  4. Linux进程实践(3) --进程终止与exec函数族

    进程的几种终止方式 (1)正常退出 从main函数返回[return] 调用exit 调用_exit/_Exit (2)异常退出 调用abort   产生SIGABOUT信号 由信号终止  Ctrl+ ...

  5. Linux System Programming --Chapter Seven

    文件和目录管理 一.文件与其元数据 我们首先看一下一个简单的文本文件是怎么保存的: 打开vim,编辑一段文本: [root@localhost ~]# vim hello.txt 编辑内容如下: op ...

  6. 网站开发进阶(二十四)HTML颜色代码表

    HTML颜色代码表 设置背景色:style='background-color:red' 设置字体颜色:style='color:red' 生活在于学习,知识在于积累.

  7. 在android C/C++ native编程(ndk)中使用logcat

    最近在研究Android 2.2 源代码的C/C++层,需要对代码进行一些调试,但是奇怪的是,直接添加LOGD("XXXXXXXX");,使用logcat却看不到任何输出,换成LO ...

  8. Unity3D学习笔记(三)Unity的C#基础

    在C#脚本中,必须显式的继承MonoBehaviour类需要注意的是,在创建C#脚本时,脚本名应尽量符合C#命名规则,以字母或下划线开头,因为类名的默认跟随脚本名.C#声明变量的方式和C++和Java ...

  9. android中的回调

    1.引子 android中的回调最经典的就是点击事件设置监听(一般通过switch(v.getId()))这里写个最基本的 btn_rigister.setOnClickListener(new Vi ...

  10. LeetCode之“树”:Binary Tree Preorder && Inorder && Postorder Traversal

    Binary Tree Preorder Traversal 题目链接 题目要求: Given a binary tree, return the preorder traversal of its ...