其他
基于开源Tars的动态负载均衡实践
作者:vivo互联网服务器团队-Yang Minshan
一、背景
官方的描述是:TARS是一个支持多语言、内嵌服务治理功能,与Devops能很好协同的微服务框架。我们在开源的基础上做了很多适配内部系统的事情,比如与CICD构建发布系统、单点登录系统的打通,但不是这次我们要介绍的重点。这里想着重介绍一下我们在现有的负载均衡算法之外实现的动态负载均衡算法。
二、什么是负载均衡
这段话很好理解,本质上是一种解决分布式服务应对大量并发请求时流量分配问题的方法。
三、TARS 支持哪些负载均衡算法
基于轮询的负载均衡算法实现很简单,原理就是将所有提供服务的可用 ip 形成一个调用列表。当有请求到来时将请求按时间顺序逐个分配给请求列表中的每个机器,如果分配到了最后列表中的最后一个节点则再从列表第一个节点重新开始循环。这样就达到了流量分散的目的,尽可能的平衡每一台机器的负载,提高机器的使用效率。这个算法基本上能满足大量的分布式场景了,这也是TARS默认的负载均衡算法。
但是如果每个节点的处理能力不一样呢?虽然流量是均分的,但是由于中间有处理能力较弱的节点,这些节点仍然存在过载的可能性。于是我们就有了下面这种负载均衡算法。
权重分配顾名思义就是给每个节点赋值一个固定的权重,这个权重表示每个节点可以分配到流量的概率。举个例子,有5个节点,配置的权重分别是4,1,1,1,3,如果有100个请求过来,则对应分配到的流量也分别是40,10,10,10,30。这样就实现了按配置的权重来分配客户端的请求了。这里有个细节需要注意一下,在实现加权轮询的时候一定要是平滑的。也就是说假如有10个请求,不能前4次都落在第1个节点上。
业界已经有了很多平滑加权轮询的算法,感兴趣的读者可以自行搜索了解。
很多时候在一些存在缓存的业务场景中,我们除了对流量平均分配有需求,同时也对同一个客户端请求应该尽可能落在同一个节点上有需求。
假设有这样一种场景,某业务有1000万用户,每个用户有一个标识id和一组用户信息。用户标识id和用户信息是一一对应的关系,这个映射关系存在于DB中,并且其它所有模块需要去查询这个映射关系并从中获取一些必要的用户字段信息。在大并发的场景下,直接请求DB系统肯定是抗不住的,于是我们自然就想到用缓存的方案去解决。是每个节点都需要去存储全量的用户信息么?虽然可以,但不是最佳方案,万一用户规模从1000万上升到1亿呢?很显然这种解决方案随着用户规模的上升,变得捉襟见肘,很快就会出现瓶颈甚至无法满足需求。于是就需要一致性hash算法来解决这个问题。一致性hash算法提供了相同输入下请求尽可能落在同一个节点的保证。
为什么说是尽可能?因为节点会出现故障下线,也有可能因为扩容而新增,一致性hash算法是能够在这种变化的情况下做到尽量减少缓存重建的。TARS使用的hash算法有两种,一种是对key求md5值后,取地址偏移做异或操作,另一种是ketama hash。
四、为什么需要动态负载均衡?
那么如果仍然采用上述三种负载均衡算法的情况下,就有问题了,被影响的机器仍然会按指定的规则分配到流量。也许有人会想,基于权重的轮询负载均衡算法不是可以配置有问题的节点为低权重然后分配到很少的流量么?确实可以,但是这种方法往往处理不及时,如果是发生在半夜呢?并且在故障解除后需要再手动配置回去,增加了运维成本。因此我们需要一种动态调整的负载均衡算法来自动调整流量的分配,尽可能的保证这种异常情况下的服务质量。
从这里我们也不难看出,要实现动态负载均衡功能的核心其实只需要根据服务的负载动态的调整不同节点的权重就可以了。这其实也是业界常用的一些做法,都是通过周期性地获取服务器状态信息,动态地计算出当前每台服务器应具有的权值。
五、动态负载均衡策略
整体功能图如下:
rpc调用时,EndpointManager定期获得可用节点集合。节点附带权重信息。业务在发起调用时根据业务方指定的负载均衡算法选择对应的节点;
RegistrServer定期从db/监控中习获取超时率和平均耗时等信息。从其它平台(比如CMDB)获得机器负载类信息,比如cpu/内存等。所有计算过程线程异步执行缓存在本地;
EndpointManager根据获得的权重执行选择策略。下图为节点权重变化对请求流量分配的影响:
节点所有性能数据每60秒更新一次,使用线程定时更新;
计算所有节点权重值和取值范围,存入内存缓存;
主调获取到节点权重信息后执行当前静态权重负载均衡算法选择节点;
兜底策略:如果所有节点要重都一样或者异常则默认采用轮询的方式选择节点;
1、按每台机器在总耗时的占比反比例分配权重:权重 = 初始权重 *(耗时总和 - 单台机器平均耗时)/ 耗时总和(不足之处在于并不完全是按耗时比分配流量);
2、超时率权重:超时率权重 = 初始权重 - 超时率 * 初始权重 * 90%,折算90%是因为100%超时时也可能是因为流量过大导致的,保留小流量试探请求;
对应代码实现如下:
void LoadBalanceThread::calculateWeight(LoadCache &loadCache)
{
for (auto &loadPair : loadCache)
{
ostringstream log;
const auto ITEM_SIZE(static_cast<int>(loadPair.second.vtBalanceItem.size()));
int aveTime(loadPair.second.aveTimeSum / ITEM_SIZE);
log << "aveTime: " << aveTime << "|"
<< "vtBalanceItem size: " << ITEM_SIZE << "|";
for (auto &loadInfo : loadPair.second.vtBalanceItem)
{
// 按每台机器在总耗时的占比反比例分配权重:权重 = 初始权重 *(耗时总和 - 单台机器平均耗时)/ 耗时总和
TLOGDEBUG("loadPair.second.aveTimeSum: " << loadPair.second.aveTimeSum << endl);
int aveTimeWeight(loadPair.second.aveTimeSum ? (DEFAULT_WEIGHT * ITEM_SIZE * (loadPair.second.aveTimeSum - loadInfo.aveTime) / loadPair.second.aveTimeSum) : 0);
aveTimeWeight = aveTimeWeight <= 0 ? MIN_WEIGHT : aveTimeWeight;
// 超时率权重:超时率权重 = 初始权重 - 超时率 * 初始权重 * 90%,折算90%是因为100%超时时也可能是因为流量过大导致的,保留小流量试探请求
int timeoutRateWeight(loadInfo.succCount ? (DEFAULT_WEIGHT - static_cast<int>(loadInfo.timeoutCount * TIMEOUT_WEIGHT_FACTOR / (loadInfo.succCount
+ loadInfo.timeoutCount))) : (loadInfo.timeoutCount ? MIN_WEIGHT : DEFAULT_WEIGHT));
// 各类权重乘对应比例后相加求和
loadInfo.weight = aveTimeWeight * getProportion(TIME_CONSUMING_WEIGHT_PROPORTION) / WEIGHT_PERCENT_UNIT
+ timeoutRateWeight * getProportion(TIMEOUT_WEIGHT_PROPORTION) / WEIGHT_PERCENT_UNIT ;
log << "aveTimeWeight: " << aveTimeWeight << ", "
<< "timeoutRateWeight: " << timeoutRateWeight << ", "
<< "loadInfo.weight: " << loadInfo.weight << "; ";
}
TLOGDEBUG(log.str() << "|" << endl);
}
}
相关代码实现在RegistryServer,代码文件如下图:
核心实现是LoadBalanceThread类,欢迎大家指正。
如下图:
2) 注意:需要全部节点启用才生效,否则rpc框架处发现不同节点采用不同的负载均衡算法则强制将所有节点调整为轮询方式。
六、动态负载均衡适用的场景
七、下一步计划
最后也欢迎大家与我们讨论交流,一起为TARS开源做贡献。
END
猜你喜欢