本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
引言
远端IO请求是没有缓存的情况下轻量级查询中最耗时的地方,对象存储的一次查询又非常久, 使用Velox查询远端存储Parquet文件的过程中发起几次IO请求就成了小查询冷读性能的关键点。
要回答清楚这个问题,下面的问题就需要搞明白:
- Velox IO读取的最小粒度是什么
- Velox IO合并的时机是什么
- IO合并后如何回填AsyncCache
Velox的IO模块中的CoalescedIO
设计的很有意思,其选择了IO发起和IO执行分离的哲学去执行,这样就可以在实际执行前对IO执行合并,减少实际发往远端存储的请求数。
而LazyVector则是经典的执行引擎优化方案,将数据提取的时机尽可能向后延迟,只有在真正访问数据时才触发加载操作,这样做有如下优点:
- 内存占用时间短,避免中间结果物化
- 避免加载不必要的列数据
- 减少不必要的计算,不会立即执行操作,而是记录下需要执行的操作。计算只在结果真正需要被物化(例如,写入内存、输出给用户、作为JOIN的输入、需要具体值)时才发生。 如果查询在后续步骤中被过滤掉(例如,WHERE 条件不满足),那么为该行所做的中间计算就完全避免了。
CoalescedIO & LazyVector这两个机制搞明白,IO路径基本就清楚了。
总结
- Velox Parquet IO最小粒度ColumnChunk
- IO合并的粒度是RowGroup,多个RowGroup会发起多个IO和文件大小无关
- 多个IO在合并时基于之间的距离判断是否可合并,RemoteRead 512K,此值可配置
- 小文件(默认8MB以下)会导致IO浪费,至少发起两次IO
细节
关键类为:
CacheInputStream CachedBufferedInput
DirectInputStream DirectBufferedInput
分别代表在有无AsyncCache情况下的IO合并机制实现。
在SplitReader中会生成一个BufferedInput,传递到ParquetReader内部,负责接受最细粒度的请求
可以在velox/dwio/parquet/reader/ParquetData.cpp:92 ParquetData::enqueueRowGroup中看到最小的读取单位是columnChunk。
velox/dwio/common/CachedBufferedInput.cpp:makeLoads
velox/common/base/CoalesceIo.h:coalesceIo
单个RowGroup的多个ColumnChunk的读取均为一个IO,会加入到CachedBufferedInput,每个PagerReader中会将CacheInputStream赋值进去;每个RowGroup会执行一次Load,其中合并IO,多个RowGroup是多个IO操作
LazyVector在查询时触发到ColumnReader + PageReader的读取,触发CacheInputStream::Next,内部触发CoalescedLoad的读取。
多个IO在合并时基于之间的距离判断是否可合并:
SSD 20000 不可修改
Remote 512K 可修改
Parquet中元数据查询直接触发远程的IO,最少一次IO,最多两次IO;
小文件(默认8MB以下)会导致IO浪费,因为元数据会载入全部数据,但是数据本身是前面一次拉取重叠的,但是还是会再拉取一次数据,每个文件至少两次IO;
lazyVector其实是实现的时候把ColumnLoader放进去了,而Pagereader中包含着InputStream,这样就可以在实际使用的时候直接触发合并后的IO操作了
合并IO后如何执行回填? velox/dwio/common/CachedBufferedInput.cpp 471
每个key回填自己的那一部分到cache中去
结束语
这篇文章的目的是记录我认为的关键点,本来想把每个细节具体的代码在哪里标记清楚,但是太花时间,直接给结论,再加上一些关键的点