阿里天池比赛启发- 流量负载算法

3,252 阅读8分钟

前言


之前写过一篇关于流量负载相关文章:聊聊负载均衡算法,dubbo3.0采用更加合理的自适应负载算法,然后我关注到19年阿里天池中间件比赛也有相关的课题,最近废寝忘食一直在做研究,直到最晚搞点11点,然后也请教了百度搞rpc的大佬讨论相关思想,下面开始梳理一下启发。

自适应概念


我们从负载算法来看,随机算法会让某些节点一时间请求量都打到它上面,轮询算法会平均的打到各个节点,但是每台机器的性能都不一样,比如服务器节点配置、网络带宽等等,所以有了权重负载,但是权重的话是一开始研发同学根据服务节点的配置打上对应的权重,随着请求两的增加有些性能好的节点也会扛不住,这样就引出了自适应负载概念。

自适应有哪些指标

一般有cpu指标、RT、并发数、异常类型等指标

cpu指标怎么实现?

我们有请gpt3同学来回答。

image.png

dubbo本身不自带节点监控的,需要外接接口,那dubbo本身有哪些现成数据呢?

RT、并发数、异常类型这个就是我们暂定的核心指标,为我们后面研究奠定一些基础理论。

阿里天池中间件比赛


image.png

地址:tianchi.aliyun.com/competition…

这是19年的活动了,大概是我关注比较晚,也是最近在搞流量负载才开始关注,但是它相关思想还是可以给我们启发的。

赛题背景

负载均衡是大规模计算机系统中的一个基础问题。灵活的负载均衡算法可以将请求合理地分配到负载较少的服务器上。理想状态下,一个负载均衡算法应该能够最小化服务响应时间(RTT),使系统吞吐量最高,保持高性能服务能力。自适应负载均衡是指无论处在空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,更好的进行流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。

要求
修改题目提供的扩展接口(UserLoadBalance),实现一套自适应负载均衡机制。要求能够具备以下能力:
1、Gateway(Consumer) 端能够自动根据服务处理能力变化动态最优化分配请求保证较低响应时间,较高吞吐量;
2、Provider 端能自动进行服务容量评估,当请求数量超过服务能力时,允许拒绝部分请求,以保证服务不过载;
3、当请求速率高于所有的 Provider 服务能力之和时,允许 Gateway( Consumer ) 拒绝服务新到请求。

评测

enter image description here
1、PTS 作为压测请求客户端向 Gateway(Consumer) 发起 HTTP 请求,Gateway(Consumer) 加载用户实现的负载均衡算法选择一个 Provider,Provider 处理请求,返回结果。
2、每个 Provider 的服务能力(处理请求的速率)都会动态变化:

  • 三个 Provider 的总处理能力会分别在小于/约等于/大于请求量三个状态变动;
  • 三个 Provider 任意一个的处理能力都小于总请求量。

3、评测分为预热和正式评测两部分,预热部分不计算成绩,正式评测部分计算成绩。
4、正式评测阶段,PTS 以固定连接数(1024) 向 Gateway 发送请求,1分钟后停止;
5、以 PTS 统计的成功请求数和最大 TPS 作为排名依据。成功请求数越大,排名越靠前。成功数相同的情况下,按照最大 TPS 排名。

实践


平均请求时间维度

首先是根据RT维度来负载均衡,主要改的是UserLoadBalance类,因为这个类是为了select出对应的节点,然后丢给下一个类去invoke代理请求。

image.png

image.png

里面的实现思路是这样,就是每次请求都会叠加,然后总请求时间除以总请求次数=平均请求时间,十分粗暴

缺点:

  1. 总时间总会到非常大的值,需要定期去按比例缩小
  2. 这样统计出来的时候是比较平均的,所以指标无法跟近期变化而变化的,比如A节点之前很牛逼,最近5分钟网络一直有问题,然后我们有把所有请求对过去,那就炸了。

窗口计算RT维度

针对上面的平均RT算法,对服务节点的性能是不够敏感的,那么我们通过窗口算法来改进。这个再交给gpt同学来解答~

image.png

其实就是一个数组来储存数据,一个size来记录当前储存多少值,然后通过轮询统计然后再除以请求次数,得到这个窗口的RT平均请求时间。

实际开发

public <T> Invoker<T> windows(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    Map<Invoker, UserLoadBalance.RequestTimeWindow> map = UserClusterInvoker.map;

    if (map.isEmpty() || map.size() < invokers.size()) {
        invokers.forEach(it -> {
            if (!map.containsKey(it)) {
                map.putIfAbsent(it, new RequestTimeWindow());
            }
        });
        return invokers.get(ThreadLocalRandom.current().nextInt(invokers.size()));
    }
    
    List<Long> list = new ArrayList<>();

    Long allHeight = 0L;

    for (Map.Entry<Invoker, UserLoadBalance.RequestTimeWindow> entry : map.entrySet()) {
        UserLoadBalance.RequestTimeWindow requestInfo = entry.getValue();

        BigDecimal avgTime = new BigDecimal(Double.toString(requestInfo.getAverageRequestTime()));

        BigDecimal height = new BigDecimal(500).divide(avgTime, 2, RoundingMode.HALF_UP);

        while (height.compareTo(new BigDecimal(1)) < 0) {
            height = height.multiply(new BigDecimal(10));
        }

        allHeight += height.longValue();

        list.add(height.longValue());
    }

    long realHeight = ThreadLocalRandom.current().nextInt(allHeight.intValue());

    int i = 0;

    Invoker invoker = null;

    for (Map.Entry<Invoker, UserLoadBalance.RequestTimeWindow> it : map.entrySet()) {
        Long height = list.get(i);

        if (realHeight < height) {
            System.out.println("命中服务节点:" + it.getKey().getUrl().getPort());

            invoker = it.getKey();
            break;
        } else {
            realHeight -= height;
            i++;
        }
    }

    return invoker != null ? invoker : invokers.get(ThreadLocalRandom.current().nextInt(invokers.size()));
}

image.png

首先是拿到所有服务端节点的信息,信息里面有平均请求时间

image.png

里面有个比较重要的点,就是synchronized,这个信息作为一个共享的资源来操作,所以在高并发下会导致数据不安全,所以加上关键字。

image.png

算法:500/平均RT,因为一般请求rt都是500ms左右,然后分母越大,接口性能越差,这个权重就越低,符合我们的预期,然后下面乘以10,为了算法最后变成0.xxx,这样意义不大,把它整到大于0的权重。当然也存在,一个节点权重是1,然后另一个是0.2,那么0.2在这个操作之后比1的还大,算法有待优化。

image.png

这块按权重来负载,这里还有段故事,按权重来命中像不像我们抽奖活动里面每个商品不同百分比命中,然后我就按我之前的算法来搞,比如说A有20%,B有80%,我们取100个值,里面20个是A,80个是B,然后随机数看命中哪个商品。

后面发现在塞入100个值的时候性能比较差的,改为上面的算法快很多,就是A 20,B 80,随机数60,那么命中B,这样效率高很多。

压测

我们通过wrk来进行压测,4个线程1024连接来压dubbo

2bb6e8ac-d731-404d-b967-9817c70b7c53.jpeg

我发现比之前平均RT算法 Avg RT还慢,虽然说rt可以衡量节点的性能,但是会导致一瞬间大量的请求都怼到该节点,导致性能问题。所以需要加上qps参数来衡量

窗口计算RT+qps维度

由于rt维度会导致节点qps高,那么我们加上qps维度

image.png

算法:500/窗口RT * 100/qps,这样RT跟qps高的话都会导致权重下降,这样就可以解决rt维度的缺陷。

image.png

qps比较暴力,直接自增,然后调用的时候判断当前有多少并发数量

压测

90ed8034-4866-4048-915a-ff4bb79ff72b.jpeg

image.png

Avg RT比之前快了,然后90%请求数时间也比之前快,也有不足:就是超时的数量比之前多,我们对异常节点控制不到位。

窗口计算RT+qps+异常类型维度

最后我们定义到这三个维度来计算节点的权重相对比较合理的,也是跟百度大佬讨论了一波,他们自研的rpc也是根据这个算法来实现。

1b51d505fe94d4bd3231f37639a3a17.jpg

dubbo本身是有这个机制的,就是服务节点不可用会剔除可用节点列表,但是那些时间比较久的那些是没有做管理的,那么我们可以对这些情况进行监控,异常类型比如说超时,算中等异常级别,如果服务异常、代码异常,那这个算高级异常,权重直接按在底下。

基于Tps、qps以及调整对应的权重

后面我跟gpt在聊,性能指标里面有rt跟tps,对于不同场景下要求不一样的,比如说我们要处理更多请求,那么需要用tps,如果要用户体验好是用rt来衡量,对于高并发场景我换成了tps,发现压测超时的少了很多,然后再调整两个参数的比重。

比如说TPS占40%,qps占60%,如果TPS是个位数,qps又是一个很高的值,那么就容易失真对吧。

压测结果 image.png

所以在负载算法里面要合理的挑对应的因子去计算!