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

1、背景

vivo 互联网领域的部分业务在微服务的实践过程中基于不少综合因素的考虑选择了TARS微服务框架。java

官方的描述是:TARS是一个支持多语言、内嵌服务治理功能,与Devops能很好协同的微服务框架。咱们在开源的基础上作了不少适配内部系统的事情,好比与CICD构建发布系统、单点登陆系统的打通,但不是此次咱们要介绍的重点。这里想着重介绍一下咱们在现有的负载均衡算法以外实现的动态负载均衡算法。算法

2、什么是负载均衡

维基百科的定义以下:负载平衡(Load balancing)是一种电子计算机技术,用来在多个计算机(计算机集群)、网络链接、CPU、磁盘驱动器或其余资源中分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。使用带有负载平衡的多个服务器组件,取代单一的组件,能够经过冗余提升可靠性。负载平衡服务一般是由专用软件和硬件来完成。主要做用是将大量做业合理地分摊到多个操做单元上进行执行,用于解决互联网架构中的高并发和高可用的问题。docker

这段话很好理解,本质上是一种解决分布式服务应对大量并发请求时流量分配问题的方法。缓存

3、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。

4、为何须要动态负载均衡?

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

那么若是仍然采用上述三种负载均衡算法的状况下,就有问题了,被影响的机器仍然会按指定的规则分配到流量。也许有人会想,基于权重的轮询负载均衡算法不是能够配置有问题的节点为低权重而后分配到不多的流量么?确实能够,可是这种方法每每处理不及时,若是是发生在半夜呢?而且在故障解除后须要再手动配置回去,增长了运维成本。所以咱们须要一种动态调整的负载均衡算法来自动调整流量的分配,尽量的保证这种异常状况下的服务质量。

从这里咱们也不难看出,要实现动态负载均衡功能的核心其实只须要根据服务的负载动态的调整不一样节点的权重就能够了。这其实也是业界经常使用的一些作法,都是经过周期性地获取服务器状态信息,动态地计算出当前每台服务器应具备的权值。

5、动态负载均衡策略

在这里咱们采用的也是基于各类负载因子的方式对可用节点动态计算权重,将权重返回后复用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当前系统中能获取到的数据):

一、按每台机器在总耗时的占比反比例分配权重:权重 = 初始权重 *(耗时总和 - 单台机器平均耗时)/ 耗时总和(不足之处在于并不彻底是按耗时比分配流量);

二、超时率权重:超时率权重 = 初始权重 - 超时率 初始权重 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框架处发现不一样节点采用不一样的负载均衡算法则强制将全部节点调整为轮询方式。

6、动态负载均衡适用的场景

若是你的服务是跑在Docker容器上的,那可能不太须要动态负载均衡这个特性。直接使用Docker的调度能力进行服务的自动伸缩,或者在部署上直接将Docker分配的粒度拆小,让服务独占docker就不存在相互影响的问题了。若是服务是混合部署的,而且服务大几率会受到其它服务的影响,好比某个服务直接把cpu占满,那建议开启这个功能。

7、下一步计划

目前的实现中只考虑了平均耗时和超时率两个因子,这能在必定程度上反映服务能力提供状况,但不够彻底。所以,将来咱们还会考虑加入cpu使用状况这些能更好反映节点负载的指标。以及,在主调方根据返回码来调整权重的一些策略。

最后也欢迎你们与咱们讨论交流,一块儿为TARS开源作贡献。

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