泛读笔记:《Two Birds With One Stone: Designing a Hybrid Cloud Storage Engine for HTAP》
最后更新时间:
文章总字数:
预计阅读时间:
页面浏览: 加载中...
1. 摘要
企业对最新数据的实时分析需求日益增长。然而,当前的解决方案无法在同一系统中高效地结合事务处理和分析处理,而是依赖ETL(提取-转换-加载)流程将事务数据传输到分析系统,从而导致数据洞察的延迟。
本文针对这一需求,提出了一种面向云环境的新型存储引擎设计——Colibri,该引擎支持超越主内存的混合事务与分析处理(HTAP)。Colibri 采用混合列存与行存架构,针对两种工作负载进行优化,并充分利用新兴硬件趋势。它通过有效区分冷热数据,以适应不同的访问模式和存储设备。
实验表明,在固态硬盘(SSD)和云对象存储上处理混合工作负载时,Colibri 可实现高达 10 倍的性能提升。
2. 背景
2.1. AP和TP负载特点不同
- AP:列存、压缩、顺序扫描
- TP:点查、行存、索引
- 过去,纯内存的HTAP较容易实现,但是扩展性差、对大数据集来说昂贵
- 对于内存放不下的数据集,必须要考虑再持久设备中如何存放
2.2. 存储技术的发展
- SSD带宽高,延迟低
- 云对象存储带宽高,中等延迟,不受限制的容量,高耐用性,高可用,便宜
3. 本文贡献
- 行列混合的存储
- 冷热数据分离
- 利用了现代存储设备(SSD和Object store)的高带宽
4. 本文的设计总览
- 分离冷热数据:
- 热数据以行存的形式与索引一同放在page server
- 冷数据以列存的形式压缩存放在对象存储中
- 利用存储设备的高带宽:所有节点都应该能够直接访问对象存储
- 减少日志条数:主节点能够减少日志传输,log server储存更少的数据,page server更少回放
- 对于大块的操作(bulk operations)应该能够旁路log server和page server:对于加载大规模数据要能够直接写入对象存储,只需要少量修改元数据
- 使用log server和page server来避免写入对象存储或者其他数据库的高延迟
- 使用传统buffer manager,以及page-based索引
5. 混合存储引擎
5.1. 行列混合储存
使用B+树做索引,冷数据通过压缩以列存形式存在数据块文件(data block file)中,热数据以行存的形式存在数据页(data page)中。如果冷数据需要修改,会先解压以热数据形式存放。图4中各个模块如下:
- 内部节点(64KB):内有4093个孩子
- 叶节点(64KB):内有51块
- 键(绿色小块):RowId(应该是8B)
- 压缩块(1272B)(白色小块):存放了一个指向数据块文件的ref,以及一些统计信息(比如minmax-bounds)
- 数据块文件(大约16MB):内部能存约262,144条记录
- 非压缩块(1272B)(白色小块):存放了(最多78个)指向数据页的ref
- 数据页(64KB):内部能存约380条记录(以PAX格式)
其中,写数据块文件不需要额外的wal。
5.2. 插入
插入只会插入到最右端的叶节点。
- 对于少量插入,直接写入最右端的叶节点,等插满后会将非压缩快转换成压缩块
- 对于大批量的插入,直接生成列存压缩的数据块文件,最后更新B+树。这样只需要记录一条日志,并且减少了复制脏页等开销
5.3. 删除
对于非压缩块:标记删除
对于压缩块,会在page-server中额外存放一个bitset页
对于批量删除,会将要删除的RowId排序,同一数据块的删除会一起执行,只需要写一条日志
会进行异步维护数据块,如果被压缩的数据块中有超过1/4被删除了,会重写该数据块
5.4. 更新
- 对于非压缩块:in-place更新
- 对于压缩块:标记删除,并按常规逻辑插入,即out-of-place更新
5.5. 维护
- 合并过小的b+树节点
- 将热数据块转换成冷数据块格式
- 将标记删除的行物理去除
- 将空文件从对象存储中删除
短维护(<50us)会在访问B+树时一并进行;长维护会在背景异步执行。
冷热数据的界限,本文认为是10s没有修改。(如下表,对于buffer size为8~16G的情况下,冷热界限从10s到30s造成的性能损失最大)
5.6. 集成现代缓存管理器
Umbra(本文使用的codebase,该组以往的工作)实现了针对事务工作负载优化的先进缓存管理器。 对于列存,由于需要解压,需使用可变大小的页面(前期工作),并且由于列存是不可变的,不需要写回。
5.7. MVCC
使用了HyPer的MVCC实现,版本链处于内存中。
5.8. 恢复
加载6Billion行数据,在写入检查点之前crash掉
| Load time | WAL size | #Log entries | Recovery | |
|---|---|---|---|---|
| Umbra | 3 099 s | 1.28 TB | 6 166 M | 5 352 s |
| Colibri | 453 s | 1.02 GB | 5.17 M | 2.99 s |
5.9. 列存数据格式
列存格式选择Data Blocks:
- 对于点查,能够快速解压出单个元组
- 对于带过滤的查询,有较好的性能
| Full Table Scan [row/s] | Filtered Scan [row/s] | Point Lookups [row/s] | Size | |
|---|---|---|---|---|
| PAX (uncompr.) | 68.4 M | 98.2 M | 1.01 M | 88.7 GB |
| Arrow | 34.1 M | 16.8 M | 183 | 96.8 GB |
| Data Blocks | 31.5 M | 99.4 M | 1.18 M | 34.1 GB |
| Parquet | 19.0 M | 7.29 M | 758 | 34.5 GB |
| Parquet (Snappy) | 11.2 M | 5.82 M | 752 | 23.9 GB |
5.10. 价格优化
对象存储为每次请求付费。S3推荐每次请求16MB数据,这样最便宜,且能够有效利用带宽。
对于数据块中的262144行记录,每列差不多0.25~0.5MB数据。因此需要将多个数据块合在一起成为一个segement。
5.11. 二级索引
针对TP负载,可以选择构建二级索引,存在数据页中,所有改动都需要先写日志。
针对AP负载,可以选择SMA(为每一小块记录最大值最小值等聚合结果)或者缓存来过滤数据。
6. 查询处理
- 使用乐观锁优化点查
- 对于扫描查询,使用下图步骤:
- b+树索引收集需要扫描的数据块并放入环形队列,如果是数据页则传给Task4并行处理
- 并行从队列中取出数据块读取任务,如果数据块不在缓存中,则使用Task3异步IO读取,在缓存中则直接交给Task4
- 异步IO读取数据块
- 并行处理数据块
带宽优化的表扫描: