上期分享👉《 2022 OceanBase数据库大赛决赛经历分享 》
本期邀请来自西北工业大学的亚军团队 426白给突击队 的队长王炳杰,为大家分享决赛性能优化实操,欢迎分享给感兴趣的朋友,共同学习成长。
本文更多是思路的分享,并不关注实现细节。初赛在满分后一周左右我写了篇总结,把大部分赛题的思路大致记录了一下;决赛在这篇博客中主要按时间顺序来梳理优化的过程,字少图多。
关于初赛
仓库:luooofan/miniob-2022
总结:Summary · Issue #25 · luooofan/miniob-2022
01. 决赛题目分析
👉 训练营链接
02. 优化思路
以下主要按照时间顺序来梳理我们的优化思路。
针对 Demo 的优化
OceanBase 一开始给了一个单线程的旁路导入 Demo
其流程如下图所示:
整体可划分为前后两部分:
- 前半部分数据预处理
输入是 csv 文件,输出是有序的数据文件;首先读数据到内存中,然后经过 CSVParser 组件解析出一行一行的数据(ObNewRow),再经过 RowCaster 组件把 ObNewRow 转换为 ObLoadDatumRow(这种数据结构可以直接走写宏块的流程),之后交给外部排序组件,这里使用归并排序。
- 后半部分写宏块生成 SSTable
输入是有序的数据文件,输出是 SSTable;数据有序化后即可按序交给 SSTableWriter 组件来写宏块进而生成 SSTable。
针对这个 Demo,我们做了两步优化:
- 把数据预处理部分多线程并行化:直接对 csv 文件分块,每个线程处理一个块的数据,加速 22min+;
- 调大 fragment size 参数(归并排序每次要落盘一个有序的 fragment):增加了归并路数,减少了归并次数,加速 9min+。
这两个优化做完之后,开始思考以下几个问题:
调整整体架构
基于上述几个问题我们调整了 Demo 的整体架构。
调整后使用了桶排序,整个流程分四部分:
1. Do Sample
主线程对csv数据文件随机采样,从而确定各个桶的范围,比如第一个桶负责0-99,第二个桶负责100-199,以此类推。
2. Data Preprocess
通过单生产者多消费者模型完成数据分桶的任务,生产者不断地读数据到内存中,每个消费者取出 buffer,仍然是通过 parser 和 caster 生成 datum row 后,交给 bucket writer 处理,bucket writer 根据行主键二分查找到对应的桶,在序列化后异步写落盘。
3. Sort & Write MacroBlocks
等全部线程完成第二阶段后,在第三阶段通过多线程来完成桶内数据的排序以及写宏块的任务。每个线程负责连续的几个桶,比如负责0到9号这十个桶的数据,每次读取一个桶内的所有数据行到内存,解序列化后存储在 memory store 中,排序后交给 macro block writer 来写宏块,接着处理下一个桶,直到把该线程负责的所有桶都处理完。
4. Create SSTable
等全部线程完成第三阶段后,主线程生成 sstable,完成导入。
这样调整后解决了前面提出的四个问题,加速 10min+。然后来分析一下调整后的性能:
有几点要说明:
在第一第二阶段,io_submit 和 io_destroy 属于意料之外的时间引入,分析后发现我们的异步写其实很弱很弱,其原因应该是系统参数限制,👉 让 io_submit 阻塞的另一种方法 - 知乎。其中 io_destory 的时间可以通过新起一个线程来避免,加速 23s。
第一第二阶段的 CPU 和 IO 利用率都不高,是因为存在互相等待的情况,也就是并没有同时的充分地利用两种资源,这里有很大的优化空间。
第三第四阶段明显 CPU 是瓶颈,之后逮着火焰图优化就行。
优化性能瓶颈
到此时已经调整好了我们的整体架构,之后就是针对资源瓶颈进行优化了。
IO 瓶颈
首先想到了可以做压缩,用 CPU 来换 IO;具体实现上,在序列化之后做压缩,解序列化之前做解压缩,调优后使用了 lz4 算法;做完后加速 7min+,这部分的 CPU 利用率涨到了很高。
后续在针对 CPU 优化了很多后,更换了一个具有更高压缩率的算法 zstd138,加速 8s。
CPU 瓶颈
1. 火焰图
2. 内存 Sort:使用主键索引
如图,原程序中对 memory store 的内存排序要使其完全有序化,有大量的内存操作,在火焰图上可以看到很宽一块儿,那我们可以提取主键列+索引列进行排序,通过有序索引来从 MemoryStore 中取得对应的行,加速 92s。
3. SIMD 优化
如图,使用 SIMD 可以做到单指令同时比较 16 Byte,从而快速确定分隔符的位置,进而通过列数来分行。
PS:为了方便,图中只画了对 8 Byte 同时比较
该优化完成后,在日志中 Parse 的时间统计加快了 44s,但是根据提交分数计算只快了 32s。我们一开始怀疑是评测机器的原因,还特意请来哥帮忙看了看评测日志,但是结果和本地是一致的。
然后我们在分析资源利用情况后发现,在第二阶段执行过程中会有 CPU 轮番摸鱼,也就是说做了这个优化后 CPU 不再是这部分的瓶颈了,瓶颈又变成了 IO。由于资源瓶颈的变化导致了总体性能提升并没有 Parse 组件的提升大。
其他针对 CPU 的一些小优化:组件输入输出批量化、绑核、循环展开、分支预测等
无用优化:SOA(优化前没有好好分析导致白白浪费了时间)
IO 瓶颈
前面做完 SIMD 优化之后,瓶颈再次来到了 IO。
1. BinaryRow
从 csv 文件中解析出来的 ObNewRow 还是要先转换为 DatumRow 之后再序列化落盘,这里其实没必要转换为 DatumRow,可以设计自己的数据结构,很多队伍也都做了这个优化,只不过叫法不一样
我们设计的 BinaryRow,其连续的存储每一个cell,并且只存储必要信息,同时舍弃了部分可以从表 schema 中获取到的结构信息,既减少了IO开销,又减少了大量内存操作,还节余出了部分内存空间,加速 73s。
2. 利用内存:留存部分 buckets
把一部分桶的数据直接留在内存中:
- 减少写读磁盘的数据量
- 减少这部分数据 [de]serialize、[de]compress 的开销
- 加速 17s
3. 利用内存:留存更多 buckets
对留在内存中的数据做压缩:
- 进一步减少写读磁盘数据量
- 对于之前留存的 buckets 增加了 [de]serialize、[de]compress 的开销
PS1:这里其实有个小 trick 是在前半部分数据预处理的时候统计出 binary row 的 data 部分最大需要多少字节,到后半部分分配的时候就可以省很多内存空间;
PS2:这个算是决赛最后一天做的一个重要优化,当时有点子着急,本来应该给这部分单独设置一个压缩算法变量然后调优的,结果没设,所以其实是直接调整了整体的压缩算法,从 zstd138 改回了 lz4,相当于把前面改用压缩算法带来的提升给撤销了,受限于此,也导致没调到这个优化最优的效果。
总的来说还是带来了 21s 的提升。
03. 总结
决赛优化
1. 首先使用桶排序替代归并排序:消去归并操作;
2. 然后在数据预处理部分,使用生产者消费者模型:利用了 CPU 资源,还优化了顺序读的时间;
3. 在消费者这里:使用 SIMD 做解析、引入 BinaryRow,减少了 CPU 的耗用;
4. 压缩、桶数据留内存、以及 BinaryRow 的设计,都减少了写读磁盘的数据量;
5. 在 bucket writer 这里使用了 LinuxAIO 异步写;
6. 第三部分做了多线程并行化,充分利用 CPU 资源;
7. 这里也通过引入 BinaryRow 减少了解序列化的开销,Sort 使用索引排序,MacroBlockWriter 循环展开分支预测等优化减少了 CPU 的耗用。
不足之处
优化上没做的主要是:
1. 列值编码
2. SSTableWriter
经验
1. 要做好代码 review、做好文档和记录;
2. 一定要对每次执行的结果备份!方便结果分析以及做 PPT。
王炳杰同学的分享到这里就结束了,关于参赛体验,如果大家有什么想交流的,欢迎评论区留言探讨~