本人以初赛Rank17,复赛Rank12的成绩结束了这次比赛。本文主要讲述复赛的关注点和优化点。不会扩展去讨论做过的尝试,有兴趣的小伙伴可以交流。
https://github.com/Lvnszn/2021-Tianchi-AnalyticDB-stage2
比赛链接在这,对详细内容有兴趣的小伙伴可以去看看。大体就是在PMEM这个介质上面实现load和quantile函数,load函数是让参赛者在这个函数里面做加载数据去写自己想要的数据,quantile函数将列的所有值排序后,返回第 N * p 个值。如果 N * p 不为整数,则向上取整。
column = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # column 为整型,有 10个 元素
quantile(column, 0.5) = 5
quantile(column, 0.8) = 8
quantile(column, 0.25) = 3
quantile(column, 0) = 0
quantile(column, 1) = 10
初赛和复赛整体是一样的,都是先实现load函数,接着通过quantile函数来查询对应的数据。
初赛跟复赛做比对的话,初赛的机器资源少,数据少,查询的方式简单。复赛的难点是数据量增加了3倍以上并且在查询阶段增加了并发查询。
初赛方案笔者是选择了同步的方式,一个线程里面做了所有的事情,只需要选择合适的线程数就可以完成目标。经过初赛的一些积累,决定为了最大化IO和CPU的利用率,复赛选择了异步的方案。异步可以把做IO密集型和CPU密集型的操作分开来,这样可以不会出现同时IO或者同时做解析工作,并且兼顾到顺序读,顺序写。
大致基调确定下来之后,笔者针对PMEM这个盘做了很多测试并且也对题目做了非常多的分析:
基于上述几点,我们可以选择读,解析分桶和写的情况,可以开启若干个线程去读取文件,读取好的数据在分发给解析的线程,解析之后的数据在给到写线程刷盘。查询的时候直接根据查询数字的头部byte定位到对应的桶,然后在对桶做排序和查询。
笔者能直接获取到的数据大致如下:2张表,4列,40亿条数据,8c8g的服务器。
经过分析题目,我们可以知道40亿数据大概70G(40亿*19/1024/1024/1024)的数据量,也就是说我们需要读取70G的数据,如果按照Long的长度也就是8个byte去落盘的话,大概是30G。按照字符串的类型去存的话就是70G,所以选择落盘Long类型的数据。
接下来笔者会从第一阶段到第二阶段的顺序来讲述对应的优化点。
如果选择了异步的方案,需要创建资源池,减少对象的申请导致耗时过长。在初始化阶段尽量做到并行初始化,线程数尽量不要超过CPU的数量。
可以采取滚动读法,每个线程的ReadBuffer是固定大小,直接读取之后控制好换行符传给解析和分桶线程去做相应的处理。如果觉得滚动读法麻烦的话,也可以让每个线程负责对应part的数据,但是这样会有木桶效应,必须得等待最慢的线程读完之后才算读完了整个文件。
这部分获取到了上游传过来的数据,此处线程需要做的是通过乘10法把long数据给算出来,会比parseLong要更快一些。此处可以优化,预测第19位是否是,或者\n,如果是以上的符号的话,则循环展开把数据算出来,这样可以快1-2s的样子。生成之后的数据先存到一个数组上,然后针对数据通过位运算进行分桶,头部byte是桶ID,剩下的7个byte是需要落盘的数据。不要小看这1个byte,少了1个Byte等于少了3G多的数据,按照读来算的话大概是1-2s的样子。
byte temp = unsafe.getbyte(address+i);
if (unsafe.getbyte(address+i+18) == 44) {
unsafe.copyMemory(null, address+i+1, tmpBytes, UnsafeUtil.BYTE_ARRAY_BASE_OFFSET, 18);
for (int innerIdx = 0; innerIdx < 18; innerIdx++) {
val = val * 10 + (temp & (0x0f));
temp = tmpBytes[innerIdx];
}
i+=18;
} else {
do {
val = val * 10 + (temp & (0x0f));
temp = unsafe.getbyte(address+(++i));
}
while (temp != ',');
}
刷盘线程获取到了分桶之后的数据,根据他的桶ID写到对应的文件里面。笔者的设计是一列2048个桶的方法落盘,一共4096个文件。
根据查询的百分比知道大概的位置,位置与桶的数量总和做比较,最终会大于桶N小于桶N+1,那么数据会在桶N里面。
知道了桶N和表名和列名,就可以查对应的桶文件,根据表名和列名查出对应位置的数据。由于盘速度很快,这个IO大概就5ms左右就能查出来了,测试下来两线程效果最好,然后通过`|`运算把头部的byte合并回来。然后把数据通过quickSelect算法找出对应位置的数据。
基于上面的方案,如果不关注细节的话,基于这些trick基本能达到50S左右的时间。针对细节性的问题比如cpu cache,分支预测和伪共享等。大家大方向不会差太远,很多都是细节性的问题,有机会在开坑。在比赛最后一天找到了并行的方案,也就是一下子读两张表,大概2-3s的提升,可惜有Bug没实现出来,最后成绩镇楼并且谢谢大家的观看。