An Introduction to Be-trees and Write Optimization 学习笔记
文章目录
1 背景
Be-Tree结构如下:
-
在B-Tree和Bε-Tree中,内部节点存储枢轴键和子指针,叶节点存储键值对(按key排序)。
问:叶子节点多大?键值对怎么在叶子节点中存储?一个叶子多个键值对?
-
大小为B的叶子包含B个键值对,下面称之为
items -
Bε-Tree的区别在于,内部节点还为缓冲区分配了一些空间,每个内部节点中的缓冲区用于存储
messages(消息),messages就是编码后的更新操作(插入、删除),这个更新最终将应用于该节点下面的叶子节点中的items(键值对)问:本文的B-Tree是不是B+Tree,因为感觉所有k-v都保存在叶子,中间节点只做索引
2 插入和删除操作
2.1 插入过程
-
插入操作被编码为插入消息
insert messages -
寻址到特定key,然后把
insert messages添加到根节点的buffer中 -
当一个节点的buffer填满后,将一批message(符合该孩子的message)刷新到该节点的一个孩子
-
通常选取具有最多未决消息
pending messages的孩子**快速响应:**这样可以尽快把
pending message刷到节点里面去**分摊IO成本:**这样也可以下刷的时候保证每次写数据量不会太少,数据太少就变成随机小写了
-
每条message最终都会传递到适当的叶子节点,并将新的k-v添加到叶子
-
叶节点变得太满时分裂(同B树)
-
内部节点有太多孩子时分裂(同B树),buffer中的message会在两个新节点间分配
2.2 性能关键
-
批量从根节点向下刷数据,新消息存储在根节点附近,避免全盘查找
-
仅当buffer满时(积累足够message)才向下刷,分摊IO成本
-
对小的、随机的插入有很好的优化效果
2.3 删除过程
- 删除操作被编码为墓碑消息
tombstone message - 当墓碑消息刷新到叶子节点时,删除对应的
item和这个tombstone message - 在墓碑消息刷到叶子之前,被删除的
item甚至整个叶子节点都持续存在 - 删除过程和插入类似,也是消息传递过程
2.4 优化
-
避免大量消息全部流入一个叶子节点
-
直接将所有消息以及该叶子的所有其他未决消息刷新到叶子
-
启发式的方法(
TokuDB和BetrFS)
3 查询操作
3.1 查询过程
包括两部分:
-
从根到叶子的查询(同B-Tree)
-
从根到叶子路径上节点的缓冲区中查询对应消息
-
要在查询结果返回前应用相关消息
eg:对键k的查询,在叶子中找到条目(k, v),且内部节点缓冲区中有墓碑消息,则查询返回NOT FOUND。
注意:在这种情况下,查询不需要更新叶子。最终当墓碑消息刷到叶子时就更新了(?不是要在查询结果返回前应用相关消息吗)。
-
范围查询和点查询类似,区别在于遍历时要检查和应用整个key范围的消息。
3.2 buffer组织
- 为了高效查找和插入,通常将buffer组织成平衡的二分查找树(如红黑树)。
- 缓冲区中的消息按其key排序,后跟时间戳。时间戳确保按正确的顺序应用消息。
因此,将消息插入缓冲区,在缓冲区中搜索以及从一个缓冲区刷新到另一个缓冲区都非常快。
4 性能分析
4.1 模型假设
对比B-Tree和Bε-Tree,有以下假设:
- 假设所有键值对大小相同
- 假设每个节点容纳
B个键值对 - 假设整棵树容纳
N个键值对 - 假设每个节点仅需一次I/O来访问
4.2 复杂度分析
对比B-Tree和Bε-Tree的渐进复杂度:
- 插入开销降低
εB^(1-ε)倍 - 单点查询开销复杂度不变,实际由于树高变为原来的
1/ε倍,因此IO开销当ε=1/2时变为原来的2倍 - 范围查询开销 = 第一个键的单点查询开销 + 范围扫描成本(约为键数目
k除以块大小B)
4.3 缓存机制
实际上树靠近根部的节点常常会缓存在RAM中,用LRU算法替换。因此实际搜索成本可能比O(logBN)次I/O要小得多,如果仅叶子节点不在缓存中,则只要1次I/O。
5 节点大小B对性能的影响
- B-Tree使用小节点(几十到几百KB)来保证插入和删除性能,但导致范围查询性能不佳。
- Bε-Tree在大节点(几百KB到几MB)情况下,仍有高效的插入、删除和范围查询性能。
- Bε-Tree树高变为原来的
1/ε倍,前提是节点大小相同,使用大节点之后可以减小树高。
6 参数ε对性能的影响
-
ε增加,枢轴键和子指针占比增大,缓冲区减小,增加查询性能(极端为B树)
-
ε减小,枢轴键和子指针占比减小,缓冲区增大,增加插入性能(极端为buffered repository树)
-
ε取1/2时具有接近B树的查询性能,和更优的插入性能(插入成本除以
√B),且可以选择大节点 -
实际要支持可变长的key,因此B和ε是不确定的。TokuDB和BetrFS中固定节点大小4MB,分支因子范围是4到16。所以至少可以256KB一批地刷新数据。
7 使用指南
关键点:写性能比读性能高几个数量级。
7.1 引入upsert
避免read-modify-write的模式
upsert是一种消息,它使用一个回调函数来编码并更新,而该消息可以在不首先知道键值的情况下发出。upsert可以对任何异步修改进行编码,这些修改仅取决于键,旧值以及可以与upsert消息一起存储的一些辅助数据。- 墓碑消息是
upsert的一个特例。upsert还可以用于增加计数器,更新文件访问时间,在取款后更新用户的帐户余额以及许多其他操作
upsert总结:
- 其实是查询+插入的组合操作,可以理解为“更新”。
u p s e r t ( k , ( f , Δ ) ) = v ← q u e r y ( k ) + i n s e r t ( k , f ( v , Δ ) ) upsert(k,(f,Δ)) = v←query(k) + insert(k,f(v,Δ)) upsert(k,(f,Δ))=v←query(k)+insert(k,f(v,Δ))
-
当
upsert消息刷到叶子节点时,f(v,Δ)将代替旧值v。 -
如果
upsert消息还没刷到叶子时进行query,则在query返回结果之前将upsert消息应用到指定key。 -
树中对于同一个key可能存在多个
upsert消息(多次修改),需要在根到叶的路径上收集所有upsert消息,并按时间顺序(时间戳)应用到指定key。
7.2 引入secondary indices
利用插入性能来提升查询性能
secondary indices是二级索引,可以维护多个索引,查询时根据查询的类型选择适当的索引- B-Tree维护多个索引的开销很大,因为插入和查询开销几乎相同,对于每列都维护索引是不现实的。
- Bε-Tree则由于插入开销远小于查询开销,可以维护所有可能用到的索引。
设计辅助索引的三个规则:
-
对所有用于查询的列建立索引
例如有三列(k1,k2,k3)的表,可能根据k1或k2查询,则需要维护两个Bε-Tree,一个按k1排序,一个按k2排序
-
保证每个索引都能直接查询到全部信息(k1,k2,k3)
例如应用程序使用键k2查找k3值,则按k2排序的索引应为每个条目存储相应的k3值。
很多辅助索引仅返回主索引的键,比如使用键k2查找k3值,会返回k1,再根据k1在主索引中找k3,但Bε-Tree不是。
-
应使应用程序尽可能执行范围查询
一个按k2排序
-
保证每个索引都能直接查询到全部信息(k1,k2,k3)
例如应用程序使用键k2查找k3值,则按k2排序的索引应为每个条目存储相应的k3值。
很多辅助索引仅返回主索引的键,比如使用键k2查找k3值,会返回k1,再根据k1在主索引中找k3,但Bε-Tree不是。
-
应使应用程序尽可能执行范围查询
例如应用程序要找满足a ≤ k1 ≤ b,且k2满足某个谓词条件的所有条目,则应用程序应该维护一个二级索引:该索引按k1排序,且只包含k2满足条件的条目。