第五届阿里云中间件天池大赛总结

云生不知处队伍

初赛

两个指标:

  1. 争取尽可能多的完成率。(三个 provider 的一共 1300+个线程,consumer 只有 1024 个线程,靠线程数分配即可做到 100%的完成率)
  2. 争取尽量多的完成数(运用两个指标,tps 和 cpu 使用率,因为测试环境通过 sleep 来模拟请求处理,所以 CPU 使用率不会上升,只能依靠 tps 来作为负载均衡的指标)

复赛

整体设计:

最大化平均值阶段的的分,争取做到写入、查询阶段的总得分最大化。

思路:

写入阶段每个线程顺序写入,这个阶段不做任何处理。

查询阶段,开启一个线程对写入数据进行处理,对 a 进行分桶,在每个桶内全局 t 有序,这样就能靠 t 锁定数据在那个桶内。

平均值阶段:依靠查询阶段建立的分桶,进行平均值查找。

运行流程

整体上我们三个阶段的工作流程如下:

  • 写入。各线程各自写入,t压缩存储在堆外内存中,并对t构建索引存储在内存中,a和body分别写入文件,每次filechannel写入大小32kb以上,这样不需要更多的buffer,即可最大化写入速度。写入过程中对a的分布进行采样,以保证分桶时各桶尽量平均。

  • 分桶。在查询阶段开始之前,我们先完成分桶操作。首先对采样的a进行排序,确定每个桶中a的范围;然后创建多个读线程,读取上一阶段各线程写入的a文件,并按照全局t升序写入到各个桶中;为了追求更高速度,写入的时候每个桶使用一个线程写入。分桶时,为了给平均值阶段留下更多的内存,我们选择在构建分桶的同时,将写入阶段存储在内存中的t写入磁盘,内存中仅留分桶后的t。分桶时,每个桶内都将部分a直接存储在内存中,并根据桶内a的范围进行一定压缩。

  • 查询。对每个线程写入的内容,分别找出tMin、tMax在文件中的位置,方法是根据t的索引进行二分确定文件位置,读取t文件并解压,得到准确的读盘区间。从硬盘读取该区间a文件的数据后逐个判断是否符合aMin和aMax区间,对于符合范围的读取body并加入到结果中。读取body时,尽量聚合临近的body一起读盘,可以提高一些分数。最后,将查询结果按t排序并返回。

    我们选择先分桶再查询,实际中可以选择并行来提高查询阶段分数,我们为了保证平均值阶段开始前分桶一定已经完成,没有选择并行进行,这样可能会牺牲查询阶段分数,但是可以保证平均值开始前一定完成分桶。

  • 平均值。先根据aMin、aMax确定相关的桶,对于每个桶,分别找出tMin、tMax在文件中的位置,方法跟查询阶段类似。读取后逐个判断是否符合查询条件,对于符合条件的计数并累加。

    查询和平均值阶段,读取a的时候,设置buffer大小为1M,一次最多读取1M数据,减少读取次数,经测试发现,基本上对于所有查询,每个桶内最多只需要1次读取。

    这里有一个优化空间,就是中间的桶,a是全部符合查询条件的,可以进行优化,我们没来得及搞。

原因和具体做法:

平均值阶段最容易追求高分数,这一阶段只与 a,t 有关,需要处理的数据量远少于前两个阶段,故我们选择追求该阶段的分数最优。所以必须在查询平均值阶段开始前完成 a 的分桶,减少读盘开销。

每次读盘要 20KB 以上才是合理的???(关于磁盘性能的评测标准)

川渝一家亲队伍

初赛

核心思路:

找到一个可以量化各个节点排队情况的数据指标,然后根据这个指标对请求权重进行调整:

指标需要满足的条件:

  1. 若各个节点都未出现过排队,则各个节点的指标值相同。
  2. 若某个节点过载 x%,则该指标会增大 x%

引入排队系数的概念:

排队系数 = 平均耗时/平均耗时差

复赛

大致思路:

按 T 对消息进行全局排序,并顺序分成多个大块,每个大块内又对 A 进行排序,顺序分成多个小块,查询或聚合时,先定位大块,再定位小块。

  1. 实时排序:使用一个大数组作为缓冲区,然后通过 T%缓冲区数组长度的方式计算出数组下标,将 message 写到对应的位置,每个位置上是一个消息链表,用于处理 T 字段相同的消息(这个地方排序有点意思)
  2. 当缓冲池中的消息达到一定量后,会从 minTime 对应的下标开始,取出一批排序好的消息,进行存储。
  3. 顺序的 A 字段和顺序的 T 字段都进行了压缩,A 字段的压缩逻辑是:先对 A 进行差值存储,若一个小块儿内的所有差值的前 N 为都是 0,则这一小块内的所有差值都只存储 8-N 位,同时将 N 记录在小块儿的索引对象里面,最终在大块儿为 2W 左右时,A 字段平均可以压缩到 6byte,压缩率为 75 左右;T 字段的压缩逻辑是:先对 T 进行差值存储,若当前差值和上一个差值一样,则在重复计数区进行计数,对于连续相等的差值,计数区每满 256 才会新开辟 1byte,最后 T 字段被压缩到 60M,压缩率约为 0.4%。
  4. 聚合统计
    在对两侧的大块儿进行处理时,先对加载的数据进行分场景预估,以此减少数据的加载量;对于中间的大块,由于 T 都是满足条件的,因此对于那些 A 也满足条件的小块,直接使用了预计算好的数据;对于 A 不完全满足查询条件的小块,则需要进行数据加载,在加载小块时,对于比较林静的小块,使用了合并加载的方式。

如何确定 IOPS 和 IO 速度之间的平衡:

  1. 由于中间的块性价比比较高,因此我们肯定希望中间的块越多越好,也就是块越小越好,也就是每次 IO 加载的数据越少越好。
  2. IOPS 为 1W,则一次 IO 至少耗时0.1ms,0.1ms 可以加在 20KB 的数据;也就是如果一次 IO 加载的数据小于 20Kb,则这次 IO 的性价比就不好。

cdeb 队伍

初赛

思路:

将服务能力以单位时间内成功请求数对每个 service 的服务能力进行量化,采用试探的方式评估每个 service 的服务能力是否已经达到最大。试探的准则核心是尝试增加或降低 service 的负载流量,根据改变负载后成功的请求数的变化,对 service 当前时刻的负载状态进行评估(已满,未满)。通过不断尝试或者降低该 service 的负载并发量,最终使得负载并发量收敛到实际值并发量。

复赛:

核心思路:

核心优化方向是尽可能使得每次查询的有效消息都分布的更紧密和调整方案的 IO 请求数与读文件总比例大小,使得 SSD 硬件性能IOPS 和读速度比例更接近。

索引设计:

使用二级索引,每 16384 条消息分为一个 block,每 512 条消息分为一个 cell,block 之间保证 t 升序,block 内部的 cell 之间保证 a 升序。每次查询时先根据查询范围 t 二分查找出有效的 block 范围,再对 block 取余内部根据查询范围 a 二分查找出有效的 cell 范围。每个 bolck 内查找定位的 cell 是连续的,有利于文件存取。

delta 压缩:

借鉴了时间戳压缩算法 delta of delta,对 t 和 a 进行了压缩。如下图所示,在原本 delta of delta 算法基础上,使用了 byte 对齐,每次压缩都以 8 的倍数 bit 为单位,解压时直接能对 byte 进行操作,减少了移位操作。因为线上使用 delta of delta 值压缩效果差别不大,但是字段 a 压缩前经过了排序,能保证 delta 为非负数,因此在对 a 进行 delta 值压缩时,可以优化符号位,进一步降低压缩率。

总结:

方案的核心是索引的设计,主要优化方向是均衡算法的读请求数量和读的总量,尽量使得 SSD的 IOPS 和读速度都尽量同时打满。

你的 Java 写的像 cxk 队伍

初赛:

由于服务内响应时间为指数分布,如果发生过载,则会导致该分布不再符合指数分布。具体实现是,记录从某一时刻后开始的请求,一定时间后,记录已完成的请求的数量和响应时间。如果没有过载(如左图),则应符合截尾指数分布。如果发生过载(如右图),则应不符合截尾指数分布。不妨假设没有过载,根据统计数据,可以用最大似然估计,估计出模型的参数,再使用 KS 检验(KS 检验可以告诉我们观测到的数据是否符合某一理论分布)反过来确认假设是否成立。如果假设成立,则说明没有过载,可以增加对该 Provider 的压力;如果假设不成立,则说明发生过载,需要减少 Provider 的压力。经过调参优化,最终成绩为 129.36 万。

复赛:

对于第一阶段的 put()操作,性能的瓶颈主要在于磁盘的连续 IO 速度,然而 CPU 的资源也十分紧张。为了提高写入的性能,我们一方面需要尽量减少磁盘 I/O 的字节数,另一方面需要减少 CPU 的开销。一个通用的数值压缩器,模仿了 UTF-8对数值进行变长编码的方式,可以将 long 范围内的整数压缩为 1~9 字节。数值的前导 0 越多,压缩的效率越高。

对于每一个发送线程,我们存储 threadXXX.zp.data 和 threadXXX.body.data 两个文件(其中 XXXX 为线程 ID 号)。threadXXX.zp.data 文件存储的事经过数值压缩器压缩的(deltaT,a)数对,由于我们只存储每一条数据中 t 相对上一条数据的 t 的差值,因此绝大多数情况下,每条记录中 t 的存储只需要1 字节,而 a 根据数据方位的不同会占用不同的字节数,对于 48bit 左右的 a 值,需要 7 个字节进行存储。threadXXX.body.data 文件中存储的是完整的未经过处理的 body 数据,每条数据 34 字节。按这样计算,所有线程写入磁盘量大约为 84GB,第一阶段得分约为 6800 分。

查询阶段的方法没看太懂,不过看起来也是 t,a 两个维度的数据存储。

地表最菜战队伍

初赛:

思路:

加权轮询,消费能力作为权重(TPS—每秒处理事务数)

  • 每次请求成功后给对应的 Provider 提权
    • Goal = (int)(gyy.bz_elapsed.get(host)*8.33/(delta+1)+1)
      goal 为提权大小,bz_elapsed 为平均 RTT,delta 为该次请求 RTT.
      由于统计 RTT 周期为 120ms,因此乘以 8.33可以估计成 TPS
      由于 delta 为除数但是可能为 0,因此加上 1
      由于 delta 可能大于 bz_elapsed,导致结果为 0,因此加上 1.
  • 每次请求成功都会对权值进行调整,对服务能力变化比较敏感。

两个思路:

关于平均 RTT 的统计(响应时间百分比)

  • 将响应时间升序排序
  • 取前 95%,然后求均值;或者取前 50%,取最大值。

可以减少因排队造成的统计响应时间偏大。

关于理论最优分配方案(已知响应时间RTT、最大并发数的前提下):

  • 打满 RTT 最低的 Provider
  • 继续往最低的 Provider 发送请求
  • 使 Provider 的响应时间 RTT 排队至第二低 RTT 相同
  • 依次类推,即可使得 Provider 总体响应时间 RTT 最低。

复赛:

赛题分析:

t为模拟时间戳,相邻 t 差值不大,可用 delta of delta 压缩

a 的分布比 t 随机,信息增益大,因此尽量将消息按 a 连续存储

按 a 连续存储的消息,相邻 a 的差值减小,也可用 delta of delta 压缩

按 a 连续存储的消息,相邻a 差值不小于 0,压缩时可忽略符号位。

为了通用性,body 不做处理。

三阶段查询时可以忽略 body,因此 t、a、body 分开存储。

查询时以 t 和 a 作为条件,读 t 的时候也需要读啊,因此 t 和 a 按块交替存储

多个线程发送数据,且单个线程内按 t 升序排列,为了减小读取时的 IOPS 需要归并排序。

核心思路:

  1. 按块存储
    归并排序多个线程消息后,可以得到全局按 t 升序排列的数据,相邻一定个数的消息封装成块—block,再将相邻一定个数的块按 a 升序排列。
    块的结构:

    • t 的范围
    • a 的范围
    • sum的结果
    • 压缩后的 t
    • 压缩后的 a

    每次查询时可以根据 t 的范围、a 的范围提前预知是否需要读取该 block。
    若完全不符合,不读取

    若完全符合,也不读取(直接使用 sum 结果)

  2. 合并读取
    先找出需要读取的 block,将相邻要读的 block 一次读取。

  3. “隔五“----合并读取后依然是 IOPS 瓶颈,因此需要用吞吐量换 IOPS。

关于磁盘性能

磁盘性能指标

IOPS 和吞吐量

总结

大佬的解题思路都是惊人的相似,这就是所谓的“大佬都强的一模一样,我们却菜的千奇百怪”吧。总结一下前五队伍对于复赛的解题思路都主要围绕三个点:

  1. 索引的设计,减少无效的 IO。
  2. 数据的压缩,通过对时效数据的压缩,使得文件占用的存储空间减少,减少存储与读取文件时的 IO 以及更大程度的利用不多的内存。
  3. 依据现有硬件情况,调整程序,使得文件的 IOPS 与吞吐量达到平衡,最大化磁盘的使用效率。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值