关于Tars:基于开源Tars的动态负载均衡实践

42次阅读

共计 4724 个字符,预计需要花费 12 分钟才能阅读完成。

一、背景

vivo 互联网畛域的局部业务在微服务的实际过程当中基于很多综合因素的思考抉择了 TARS 微服务框架。

官网的形容是:TARS 是一个反对多语言、内嵌服务治理性能,与 Devops 能很好协同的微服务框架。咱们在开源的根底上做了很多适配外部零碎的事件,比方与 CICD 构建公布零碎、单点登录零碎的买通,但不是这次咱们要介绍的重点。这里想着重介绍一下咱们在现有的负载平衡算法之外实现的动静负载平衡算法。

二、什么是负载平衡

维基百科的定义如下:负载平衡(Load balancing)是一种电子计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其余资源中调配负载,以达到优化资源应用、最大化吞吐率、最小化响应工夫、同时防止过载的目标。应用带有负载平衡的多个服务器组件,取代繁多的组件,能够通过冗余进步可靠性。负载平衡服务通常是由专用软件和硬件来实现。次要作用是将大量作业正当地摊派到多个操作单元上进行执行,用于解决互联网架构中的高并发和高可用的问题。

这段话很好了解,实质上是一种解决分布式服务应答大量并发申请时流量调配问题的办法。

三、TARS 反对哪些负载平衡算法

TARS 反对三种负载平衡算法,基于轮询的负载平衡算法、基于权重调配的轮询负载平衡算法、一致性 hash 负载平衡算法。函数入口是 selectAdapterProxy,代码在 TarsCpp 文件里,感兴趣的能够从这个函数开始深刻理解。

3.1 基于轮询的负载平衡算法

基于轮询的负载平衡算法实现很简略,原理就是将所有提供服务的可用 ip 造成一个调用列表。当有申请到来时将申请按工夫程序一一调配给申请列表中的每个机器,如果调配到了最初列表中的最初一个节点则再从列表第一个节点从新开始循环。这样就达到了流量扩散的目标,尽可能的均衡每一台机器的负载,进步机器的应用效率。这个算法基本上能满足大量的分布式场景了,这也是 TARS 默认的负载平衡算法。

然而如果每个节点的解决能力不一样呢?尽管流量是均分的,然而因为两头有解决能力较弱的节点,这些节点依然存在过载的可能性。于是咱们就有了上面这种负载平衡算法。

3.2 基于权重调配的轮询负载平衡算法

权重调配顾名思义就是给每个节点赋值一个固定的权重,这个权重示意每个节点能够调配到流量的概率。举个例子,有 5 个节点,配置的权重别离是 4,1,1,1,3,如果有 100 个申请过去,则对应调配到的流量也别离是 40,10,10,10,30。这样就实现了按配置的权重来调配客户端的申请了。这里有个细节须要留神一下,在实现加权轮询的时候肯定要是平滑的。也就是说如果有 10 个申请,不能前 4 次都落在第 1 个节点上。

业界曾经有了很多平滑加权轮询的算法,感兴趣的读者能够自行搜寻理解。

3.3 一致性 Hash

很多时候在一些存在缓存的业务场景中,咱们除了对流量平均分配有需要,同时也对同一个客户端申请应该尽可能落在同一个节点上有需要。

假如有这样一种场景,某业务有 1000 万用户,每个用户有一个标识 id 和一组用户信息。用户标识 id 和用户信息是一一对应的关系,这个映射关系存在于 DB 中,并且其它所有模块须要去查问这个映射关系并从中获取一些必要的用户字段信息。在大并发的场景下,间接申请 DB 零碎必定是抗不住的,于是咱们天然就想到用缓存的计划去解决。是每个节点都须要去存储全量的用户信息么?尽管能够,但不是最佳计划,万一用户规模从 1000 万回升到 1 亿呢?很显然这种解决方案随着用户规模的回升,变得顾此失彼,很快就会呈现瓶颈甚至无奈满足需要。于是就须要一致性 hash 算法来解决这个问题。一致性 hash 算法提供了雷同输出下申请尽可能落在同一个节点的保障。

为什么说是尽可能?因为节点会呈现故障下线,也有可能因为扩容而新增,一致性 hash 算法是可能在这种变动的状况下做到尽量减少缓存重建的。TARS 应用的 hash 算法有两种,一种是对 key 求 md5 值后,取地址偏移做异或操作,另一种是 ketama hash。

四、为什么须要动静负载平衡?

咱们目前的服务大部分还是跑在以虚拟机为主的机器上,因而混合部署(一个节点部署多个服务)是常见景象。在混合部署的状况下,如果一个服务代码有 bug 了占用大量的 CPU 或内存,那么必然跟他一起部署的服务都会受到影响。

那么如果依然采纳上述三种负载平衡算法的状况下,就有问题了,被影响的机器依然会按指定的规定调配到流量。兴许有人会想,基于权重的轮询负载平衡算法不是能够配置有问题的节点为低权重而后调配到很少的流量么?的确能够,然而这种办法往往解决不及时,如果是产生在中午呢?并且在故障解除后须要再手动配置回去,减少了运维老本。因而咱们须要一种动静调整的负载平衡算法来主动调整流量的调配,尽可能的保障这种异常情况下的服务质量。

从这里咱们也不难看出,要实现动静负载平衡性能的外围其实只须要依据服务的负载动静的调整不同节点的权重就能够了。这其实也是业界罕用的一些做法,都是通过周期性地获取服务器状态信息,动静地计算出以后每台服务器应具备的权值。

五、动静负载平衡策略

在这里咱们采纳的也是基于各种负载因子的形式对可用节点动静计算权重,将权重返回后复用 TARS 动态权重节点抉择算法。咱们抉择的负载因子有:接口 5 分钟均匀耗时 / 接口 5 分钟超时率 / 接口 5 分钟异样率 /CPU 负载 / 内存使用率 / 网卡负载。负载因子反对动静扩大。

整体性能图如下:

5.1 整体交互时序图

rpc 调用时,EndpointManager 定期取得可用节点汇合。节点附带权重信息。业务在发动调用时依据业务方指定的负载平衡算法抉择对应的节点;

RegistrServer 定期从 db/ 监控中习获取超时率和均匀耗时等信息。从其它平台(比方 CMDB)取得机器负载类信息,比方 cpu/ 内存等。所有计算过程线程异步执行缓存在本地;

EndpointManager 依据取得的权重执行抉择策略。下图为节点权重变动对申请流量调配的影响:

5.2 节点更新和负载平衡策略

节点所有性能数据每 60 秒更新一次,应用线程定时更新;

计算所有节点权重值和取值范畴,存入内存缓存;

主调获取到节点权重信息后执行以后动态权重负载平衡算法抉择节点;

兜底策略:如果所有节点要重都一样或者异样则默认采纳轮询的形式抉择节点;

5.3 负载的计算形式

负载计算形式:每个负载因子设定权重值和对应的重要水平(按百分比示意),依据具体的重要水平调整设置,最初会依据所有负载因子算出的权重值乘对应的百分比后算出总值。比方:耗时权重为 10,超时率权重为 20,对应的重要水平别离为 40% 和 60%,则总和为 10 0.4 + 20 0.6 = 16。对应每个负载因子计算的形式如下(以后咱们只应用了均匀耗时和超时率这两个负载因子,这也是最容易在 TARS 以后零碎中能获取到的数据):

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 类,欢送大家斧正。

5.4 应用形式

  1. 在 Servant 管理处配置 -w -v 参数即可反对动静负载平衡,不配置则不启用。

如下图:

  1. 留神:须要全副节点启用才失效,否则 rpc 框架处发现不同节点采纳不同的负载平衡算法则强制将所有节点调整为轮询形式。

六、动静负载平衡实用的场景

如果你的服务是跑在 Docker 容器上的,那可能不太须要动静负载平衡这个个性。间接应用 Docker 的调度能力进行服务的主动伸缩,或者在部署上间接将 Docker 调配的粒度拆小,让服务独占 docker 就不存在相互影响的问题了。如果服务是混合部署的,并且服务大概率会受到其它服务的影响,比方某个服务间接把 cpu 占满,那倡议开启这个性能。

七、下一步打算

目前的实现中只思考了均匀耗时和超时率两个因子,这能在肯定水平上反映服务能力提供状况,但不够齐全。因而,将来咱们还会思考退出 cpu 应用状况这些能更好反映节点负载的指标。以及,在主调方依据返回码来调整权重的一些策略。

最初也欢送大家与咱们探讨交换,一起为 TARS 开源做奉献。

作者:vivo 互联网服务器团队 -Yang Minshan

正文完
 0