第五届阿里中间件大赛参赛总结#复赛15名

在初秋写下这个标题,献给2019年的夏天。

当时是六月初,正好结束实习回学校,毕业季一系列零零碎碎的事,觉得干点别的都不太合适,就决定把零碎的时间用来打第五届中间件的比赛,不知不觉三个月就过去了,中途掺杂了一系列想退赛的想法,最终还是坚持到了复赛结束,最终拿到了复赛15名,作为内部排名第二名受邀答辩,在决赛答辩中获得季军。我的队名是“你写代码像cxk”,这个队名是夹在一群大佬中间瑟瑟发抖的我的自嘲,代码地址:https://github.com/airfan1994/aliMiddlewareRace2019

通过这篇文章写下赛题的情况和我的解决方案,以及比赛中的一些心路历程,欢迎大家拍砖指导,可以随时在博客、github留言交流。

初赛赛题

首先看初赛的赛题:https://tianchi.aliyun.com/competition/entrance/231714/information

赛题背景

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

要求

修改题目提供的扩展接口(UserLoadBalance),实现一套自适应负载均衡机制。要求能够具备以下能力:

1、Gateway(Consumer) 端能够自动根据服务处理能力变化动态最优化分配请求保证较低响应时间,较高吞吐量;

2、Provider 端能自动进行服务容量评估,当请求数量超过服务能力时,允许拒绝部分请求,以保证服务不过载;

3、当请求速率高于所有的 Provider 服务能力之和时,允许 Gateway( Consumer ) 拒绝服务新到请求。

赛题解析

初赛是一个负载均衡的题目,需要基于dubbo框架实现负载均衡算法来更好地分配流量给后端的三个invoker。具体到比赛评测就是会启动一个benchmarker向gateway在90s内不间断地发送请求,gateway执行选手所设计的负载均衡和限流算法将请求分发给后端的三个invoker,执行过程中出现异常的请求会重发,以最终完成的有效请求数作为排名依据。

在评测过程中,invoker侧会维护一个信号量代表允许进入线程池的请求个数,线程池自己还会有自己的线程池最大连接数(信号量大小与线程池最大连接数的大小没有关系),比赛限制不能直接获取信号量的大小,需要根据invoker服务器的状态动态调整流量分配。在90s的评测过程中,会多次变动信号量的大小,invoker线程池的状态也会随之变化。

因为三个invoker的服务能力其实是不一样的,如果一个invoker负载很大了,你还在给他拼命发,那么争抢信号量的时间就会变长,相同时间内完成的请求数就会变少,每个就会影响整体的吞吐量。如果当前信号量大于invoker的线程池最大连接数,那么就会出现线程池耗尽的异常,这样就需要重发这个请求,造成能力的浪费。而选手需要是:1)根据后端服务器的状态快速对流量分配作调整,防止出现一台机器已经超负载,另外一台机器还在空转的情况;2)在流量分配合理的基础上,对invoker进行合理限流,防止出现负载过大造成信号量争抢时间长,甚至压垮线程池的现象。

初赛思路

负载均衡算法是计算机学科的一个经典的问题,下面是一些经典的的负载均衡算法:

加权轮询:按照一定的权重轮流地将流量分配到后端三台机器,在比赛中这个是大家最容易想到的思路,也是大家作为比较的baseline;

最小连接数:选取实时连接数最少的机器转发流量;

一致性哈希算法:也是一种很常用的负载均衡算法,基于这种算法可以很方便的实现会话保持。

对于赛题分析部分提出的两个问题,我的负载均衡算法采用的是加权最小连接,这是个经典的负载均衡算法,做加权是因为三台机器的能力是不一样的。确定了思路之后,剩下的就是工程问题了,熟悉dubbo框架,实现负载均衡算法,不过这个地方有两个需要注意的点:

1)比赛的代码在进行远程调用(invoke函数)的时候用了异步执行的逻辑:

@Activate(group = Constants.CONSUMER)

public class TestClientFilter implements Filter {

    @Override

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        try{

            Result result = invoker.invoke(invocation);

            return result;

        }catch (Exception e){

            throw e;

        }

    }


    @Override

    public Result onResponse(Result result, Invoker<?> invoker, Invocation invocation) {

        URL url = invoker.getUrl();

        RpcStatus.endCount(url, invocation.getMethodName(), 0, true);

        return result;
    }
}

在调用invoker.invoke之后会立刻给你返回个future,而不会等待拿到执行结果,所以需要像上面的代码那样,把一个请求调用结束的状态更新放到onResponse,才能去真实地统计到当前连接数和调用的RT;

2)我们需要意识到负载均衡策略所依赖的连接数更新相对于真实连接数的变化是迟缓的,比如你在某一时刻更新了新的连接数状态,在下一个时刻gateway同时会进来3个请求,这三个请求都会异步调用UesrLoadBalance函数的函数,会同时全部选择某个invoker;而这个过程我们理想的调用情况应该是第一个请求进来,更新连接数状态量,然后第二个请求进来再进行一次更新,做第二次选择,可能选择的invoker和第一个还不太一样,然后计算第三个...可能对于连接数比较少的情况这种滞后影响不是很大,但是对于比赛这种并发量非常高的场景,可能这种迟滞的时间窗口内,几百个请求就一下子打到了同一个invoker去了。

然后是限流,我在限流部分的算法思路是:在每300ms的时间窗统计平均响应时间,发现平均响应时间有明显变动之后,将限流值从线程池最大连接数作为最高点逐步下调,直至调到某个点之后平均响应时间会比之前的调节点会明显上升,说明接近了真实能力,再小幅度上调。

其实限流更多的是一个工程性问题,如何基于dubbo框架合理的更新状态,调节参数使变化感知更敏感,调整更到位。

初赛总结

初赛其实是个很有意思的题目,比赛的难点在于如何根据响应时间等指标探测出invoker真实的服务能力(信号量的大小),并据此调整流量分配策略。大家的想法也都很奇妙,排名靠前的小伙伴很多也真的是在设计反压策略。至于限流的部分则是考验工程能力和学习能力,需要大家去学习dubbo的框架,基于dubbo框架去设计策略,了解dubbo的异步响应机制。

复赛赛题

下面再来分析复赛的赛题:https://tianchi.aliyun.com/competition/entrance/231714/information

赛题背景

Apache RocketMQ作为的一款分布式的消息中间件,历年双十一承载了万亿级的消息流转,为业务方提供高性能低延迟的稳定可靠的消息服务。随着业务的逐步发展和云上的输出,各种依赖消息作为输入输出的流计算场景层出不穷,这些都给RocketMQ带来了新的挑战。请实现一个进程内消息持久化存储引擎,可支持简单的聚合计算,如指定时间窗口的内对于消息属性某些字段的求和、求平均等。

要求

实现一个进程内消息持久化存储引擎,要求包含以下功能:

  • 发送消息功能
  • 根据一定的条件做查询或聚合计算,包括

A. 查询一定时间窗口内的消息
B. 对一定时间窗口内的消息属性某个字段求平均,以及求和

例子:t表示时间,时间窗口(1000, 1002)表示: t>1000 & t<1002

消息内容简化成两个字段,一个是业务字段a(整数),一个是时间戳(long)。实际存储格式用户自己定义,只要能实现对应的读写接口就好。

 评测指标和规模

评测程序分为3个阶段: 发送阶段、查询聚合消息阶段、查询聚合结果阶段:

 * 发送阶段:假设发送消息条数为N1,所有消息发送完毕的时间为T1;发送线程多个,消息属性为: a(随机整数), t(输入时间戳,基本是升序的).消息总大小为50字节,消息条数在20亿条左右,总数据在100G左右

  * 查询聚合消息阶段:有多次查询,消息总数为N2,所有查询时间为T2; 返回以ta为条件的消息, 返回消息按照t升序排列

  * 查询聚合结果阶段: 有多次查询,消息总数为N3,所有查询时间为T3; 返回以ta为条件对a求平均的值

若查询结果都正确,则最终成绩为N1/T1 + N2/T2 + N3/T3

赛题解析

复赛的场景其实与TSDB有点像,时间序列数据库的很多技术比如时间戳压缩、value压缩、预聚合是可以借鉴的,但是由于查询形态比较单一,又要拼成绩,所以TSDB的结构和索引等方面也不能直接应用于比赛。

100G的数据如果评测数据随机性足够的话是无法放到内存的,所以需要按合理方式构建数据索引,为了提高成绩,索引也最好要全部放到4G的内存里。一个最简单的做法是每个线程基于t做个btree索引,然后查询数据的时候做个多路归并,应该很快就可以写出来。

复赛思路

各阶段分工:

首先说一下我的方案里的各阶段分工与大体逻辑:

在评测的put阶段,存储引擎会启动3个负责落盘的write thread, 评测程序会启动12个put线程调用put函数,12个put线程会把数据发送给12个ConcurrentLinkedQueue,由4个负责落盘的write thread去取ConcurrentLinkedQueue中的数据,采取一种类似于流水线的方式,这样设计的目的在于最大程度的分开IO和计算,尽量去打满IO。4个负载落盘的线程对应的将12个线程的数据写入各自的文件中。

在评测的查询聚合消息阶段,存储引擎会启动12个读取线程,和一个merge线程。12个读取会不断读取第一阶段落盘的文件,并对应发到ConcurrentLinkedQueue上,由merge线程对12个局部有序的ConcurrentLinkedQueue以t为依据做全局排序,并将全局排序的数据按照a的值做分区到不同的分区文件中。

在查询聚合结果阶段,12个存储引擎会并发读取第2阶段落盘的Partition文件,计算平均值。

关于IO:

感觉对这种性能型比赛,IO方案用的最多的也就是池化DirectBuffer的FileChannel,或者不进行flush的mappedbytebuffer,第一种实现大家可以参考我这次的代码,第二种实现大家可以参考我github上第三次中间件大赛的代码。

这两种IO方式其实效果相差不大,在这两种的选择上,我的想法是mmap方案垃圾回收不太可控,我担心突发的gc可能会影响我第三阶段的效果,就全部用了filechannel。

关于java常见IO方式的比较大家可以参考这篇文章:https://juejin.im/post/5c471b066fb9a049d2366332

关于mmap和filechannel的比较大家可以参考这篇文章: https://www.jianshu.com/p/d0b4ac90dbcb

关于索引:

索引。。。最开始的时候开始想在索引上花些心思的,弄点什么LSM,Rtree之类的,感觉都有可借鉴的地方,后来又有没时间+大家都在压数据也就没心思去搞了。

我第二阶段对12个t局部有序的文件做了一个全局t排序,然后在每个全局t有序的分区内,每256个t记录最大值,将这total/256个最大值按顺序存储在一个long类型数组中,这样数组下标就与这256条数据在文件中的位置产生了联系。就产生了一个每256个t对应文件中位置的映射,就这样做了个简单的索引方案。做消息查询的时候,先根据tMin和tMax在数组中做二分查找到对应的两个文件pos,然后把这两个文件pos中间的数据全部加载到内存再按a的范围进行过滤。

关于压缩:

压缩。。。。。。在这点上我是服气的,在这次比赛中见识到很多选手奇技淫巧的压缩。我关于压缩一直考虑的不多(其实是大家在研究压缩的那段时间我在百阿。。。)。不过对于t压缩还是投入了一定的心血的,其实对于时间戳的压缩一直是这种时序数据库开发者发力研究的点,大家可以看看阿里云TSDB(https://help.aliyun.com/document_detail/56235.html?spm=a2c4g.11186623.6.546.5a7671e01qyS9N),有一个很重要的优势就是相对与开源版本能够提高好几倍的压缩。因为在真实的IOT场景,这种时序的数据,对于时间戳数据是比比赛的数据要密集的多的,可能很多条数据都是连续的时间戳,所以一个良好的压缩算法是能够对这种时序产生非常高的压缩比的,比如faebook的delta-of-delta压缩算法等等,大家可以了解一下。但是由于这次比赛的a相对于真是的时序数据的t还是不够集中,所以导致这些经典的压缩算法效果并不好,主要体现在消耗的CPU资源与节省的IO资源相比是不合算的。

最终我用的压缩方式也偏向于保守,由于t一直是递增的,所以对于每两个相邻的t,差距在128以内的就用一个byte存,大于一个byte的就用自己实现的一个hashmap存,之所以用自己实习的hashmap是为了减少对象头的空间消耗,以及防止不受控制的垃圾回收和rehash行为。

第三阶段优化:

第三阶段是一个超级有意思的阶段,也是本届比赛的亮点,在那段没有时间做的日子里就看到群里的大佬们花式刷成绩,最后主办方不得不作出对随机范围的优化,来限制基于数据分布的一些优化行为。由于分数是按照总任务数除以总时间的方式来计算,所以优化本就耗时不多的第三阶段效果会明显的多。

我的方案包括:1)将a和body分开存储,减少每次查询需要的读入的数据总量;2)对t进行全局排序,这样就防止至少要读入12个局部有序的文件;3)按a的取值进行分区,每次只读入amin,amx之间的分区,而自然地回避掉其他的分区;4)尽可能地减少聚合消息的计算,能不做的判断就不做,能用位运算就不用乘除法。

一些面向比赛的优化点:

CPU资源还是很紧张的,所以尽量避免不必要的线程空转等待,比如write thread和read thread在无数据读入的时候,视情况Thread.sleep(0,1)让出时间片;比如ByteBuffer尽量池化减少gc压力。再比如尽量用原子量不用锁,减少无意义的锁争抢和时间片流失。具体大家可以看我的代码。

复赛总结:

复赛大概一个半月左右的时间,对选手们的身心挑战还是很大的,特别是为了程序的鲁棒性,主办方也在同步优化评测程序,大家都需要频繁改动代码以适应改动的评测和群里流行的技术趋势。但是还是觉得很有意思的,与高手过招其乐无穷,而可能一点点的成绩差距的背后是太多需要努力学习和弥补的点。

心路历程

三个月的赛程,本来到最后都有点身心俱疲,但是结束的那天下午竟还有些舍不得。

其实这次参赛在心态上是比较放松的,放松是因为早就做好了到了最后没有精力而退赛的心理准备,但是就经历来说还是比较跌宕起伏的。当时看到今年的题目的时候,心里的感觉就是十分想做复赛,以及万分不想做初赛。网络。。。。我最不擅长的就是这个,netty,dubbo这些听着就头疼,第三届中间件大赛就是网络给我留下了心理阴影。初赛也果然没让我失望,6月半多个月都投入到了比赛上了,结果天天在淘汰线上挣扎,而名次是一天天哗哗的掉,好像离初赛截止前一周半的时候我还在250多名晃荡,经常是我这个250花了250分的力气写了一版,结果名次还是250。就是那种入不得门的感觉,完全不知道怎么改进,每次觉得肯定能上分结果调到最后怎么也上不了分,走的路没觉得有理由不通但就是怎么也不通。挣挣扎扎,寻寻觅觅,冷冷清清,最后一周才低分爬进了复赛。

刚才提到了,复赛我是真想做,真喜欢这个题目,当时的想法就是IO,索引,实现一个时序数据库,好酷,最近看的这么多数据库的东西终于要排上用场了。事实证明我果然还是太乃义务了,开始以为实现的会是一个TSDB(https://help.aliyun.com/document_detail/56235.html?spm=a2c4g.11186623.6.546.5a7671e01qyS9N),结果最终差点实现了一个redis。。。。复赛开端的时候还不错,因为当时七月快入职了,觉得自己可能精力不够就提前写了一版,接触题目比较早,开始正式赛的时候名次还比较靠前。另一方面,入职之后的生活比我想象中的累得多,七月基本完全没写,完全靠7月前的底子撑着。

时间飞逝,七月前期还好,到了后期大家的思路逐渐受到了当时第一名的影响,开始把所有的at数据缓存到到了内存,我满脸懵逼,说好的时序存储场景你告诉我value可以放内存?快到八月初的时候,我基本处于半弃赛的状态。不过这个比赛不断延期的优良传统拯救了我,时间后延到了我的百阿,百阿晚上的时间是真的无聊,百阿睡不着的我于是又开始奋力做了两周,终于从六七十名追到了前十。百阿之后又恢复常态,上班+周末干活没时间,印象最深的就是每天晚上打车会在车上后排会抓紧时间赶紧做一做。其实每天打车回家后排的时间真的是不够的,但是比赛会自动延期呀,我就能偶尔抓紧某个延期出来的周末做一版,就这样苟延残喘的苟到了最后。

还是要交个朋友

除了一个人是真的累之外,还是就是不同人之间的思想碰撞真的是挺有用的,有的圈圈你靠你自己不停地试就真的很难去走出来。举个栗子:复赛的时候,官方给了个实现的demo,上面两个重要的函数加了synchronized,然后选手参赛的时候又需要实现这几个函数体,不能删掉给的函数,于是我就惯性思维地把这个synchronized一直带到了很晚,直到有一天丽丽看了眼我的代码,嘲笑我什么加了这么多synchronized,还直接加到了函数上,我才猛然意识到我不能删函数,不能改参数,但是synchronized还是可以去掉的。这个问题我发现的时候已经很晚了,如果靠我自己意识到,不知道还得再过多久。一个人的心力有限,在面对复杂程序、各种边界条件打扰时很容易忽略某个重要的点,多些小伙伴多份思路有时候会碰撞出意想不到的火花。即使不行还可以作为一个理由约顿火锅不是。

面对现实

不知道大家有没有这中经历,我经常会明明这个方案效果不好,但就是不敢相信,试图去多提交几次,再多提交几次。通过这次比赛有一个深刻的体会就是:越到了职业生涯的后期爆发期,基本上所有的差距都是有原因的,都不是靠小聪明和随机性可以弥补的,要对差距保持敬畏之心,踏踏实实的学习努力,而不是盯着别人的缺点,试图发挥自己的小聪明。

好好学习

群里看到很多大佬,有的坚持了好几届,有的很辛苦的白天上班晚上做,真的很佩服他们,一句最朴素的好好学习用以自勉,年轻的时间很短暂,多学习,多做有意义的事。

最后再次感谢主办方提供这么好的机会,也感谢丽丽。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值