An Introduction to Be-trees and Write Optimization 学习笔记

paper链接An Introduction to Be-trees and Write Optimization

1 背景

Be-Tree结构如下:

An Introduction to Be-trees and Write Optimization 学习笔记

  1. 在B-Tree和Bε-Tree中,内部节点存储枢轴键和子指针,叶节点存储键值对(按key排序)。

    问:叶子节点多大?键值对怎么在叶子节点中存储?一个叶子多个键值对?

  2. 大小为B的叶子包含B个键值对,下面称之为items

  3. Bε-Tree的区别在于,内部节点还为缓冲区分配了一些空间,每个内部节点中的缓冲区用于存储messages(消息)messages就是编码后的更新操作(插入、删除),这个更新最终将应用于该节点下面的叶子节点中的items(键值对)

    问:本文的B-Tree是不是B+Tree,因为感觉所有k-v都保存在叶子,中间节点只做索引


2 插入和删除操作

2.1 插入过程

  1. 插入操作被编码为插入消息insert messages

  2. 寻址到特定key,然后把insert messages添加到根节点的buffer中

  3. 当一个节点的buffer填满后,将一批message(符合该孩子的message)刷新到该节点的一个孩子

  4. 通常选取具有最多未决消息pending messages的孩子

    **快速响应:**这样可以尽快把pending message刷到节点里面去

    **分摊IO成本:**这样也可以下刷的时候保证每次写数据量不会太少,数据太少就变成随机小写了

  5. 每条message最终都会传递到适当的叶子节点,并将新的k-v添加到叶子

  6. 叶节点变得太满时分裂(同B树)

  7. 内部节点有太多孩子时分裂(同B树),buffer中的message会在两个新节点间分配

2.2 性能关键

  1. 批量从根节点向下刷数据,新消息存储在根节点附近,避免全盘查找

  2. 仅当buffer满时(积累足够message)才向下刷,分摊IO成本

  3. 小的随机的插入有很好的优化效果

2.3 删除过程

  1. 删除操作被编码为墓碑消息tombstone message
  2. 当墓碑消息刷新到叶子节点时,删除对应的item和这个tombstone message
  3. 在墓碑消息刷到叶子之前,被删除的item甚至整个叶子节点都持续存在
  4. 删除过程和插入类似,也是消息传递过程

2.4 优化

  1. 避免大量消息全部流入一个叶子节点

  2. 直接将所有消息以及该叶子的所有其他未决消息刷新到叶子

  3. 启发式的方法(TokuDBBetrFS


3 查询操作

3.1 查询过程

包括两部分:

  1. 从根到叶子的查询(同B-Tree)

  2. 从根到叶子路径上节点的缓冲区中查询对应消息

    • 要在查询结果返回前应用相关消息

      eg:对键k的查询,在叶子中找到条目(k, v),且内部节点缓冲区中有墓碑消息,则查询返回NOT FOUND。

      注意:在这种情况下,查询不需要更新叶子。最终当墓碑消息刷到叶子时就更新了(?不是要在查询结果返回前应用相关消息吗)。

范围查询和点查询类似,区别在于遍历时要检查和应用整个key范围的消息。

3.2 buffer组织

  1. 为了高效查找和插入,通常将buffer组织成平衡的二分查找树(如红黑树)。
  2. 缓冲区中的消息按其key排序,后跟时间戳。时间戳确保按正确的顺序应用消息。

因此,将消息插入缓冲区,在缓冲区中搜索以及从一个缓冲区刷新到另一个缓冲区都非常快。


4 性能分析

4.1 模型假设

对比B-TreeBε-Tree,有以下假设:

  1. 假设所有键值对大小相同
  2. 假设每个节点容纳B个键值对
  3. 假设整棵树容纳N个键值对
  4. 假设每个节点仅需一次I/O来访问

4.2 复杂度分析

对比B-Tree和Bε-Tree的渐进复杂度:

An Introduction to Be-trees and Write Optimization 学习笔记

  1. 插入开销降低εB^(1-ε)
  2. 单点查询开销复杂度不变,实际由于树高变为原来的1/ε倍,因此IO开销当ε=1/2时变为原来的2倍
  3. 范围查询开销 = 第一个键的单点查询开销 + 范围扫描成本(约为键数目k除以块大小B

4.3 缓存机制

实际上树靠近根部的节点常常会缓存在RAM中,用LRU算法替换。因此实际搜索成本可能比O(logBN)次I/O要小得多,如果仅叶子节点不在缓存中,则只要1次I/O。


5 节点大小B对性能的影响

  1. B-Tree使用小节点(几十到几百KB)来保证插入删除性能,但导致范围查询性能不佳。
  2. Bε-Tree在大节点(几百KB到几MB)情况下,仍有高效的插入删除范围查询性能。
  3. Bε-Tree树高变为原来的1/ε倍,前提是节点大小相同,使用大节点之后可以减小树高

6 参数ε对性能的影响

  1. ε增加,枢轴键和子指针占比增大,缓冲区减小,增加查询性能(极端为B树)

  2. ε减小,枢轴键和子指针占比减小,缓冲区增大,增加插入性能(极端为buffered repository树)

  3. ε取1/2时具有接近B树的查询性能,和更优的插入性能(插入成本除以√B),且可以选择大节点

  4. 实际要支持可变长的key,因此B和ε是不确定的。TokuDB和BetrFS中固定节点大小4MB,分支因子范围是4到16。所以至少可以256KB一批地刷新数据。


7 使用指南

关键点:写性能比读性能高几个数量级。

7.1 引入upsert

避免read-modify-write的模式

  • upsert是一种消息,它使用一个回调函数来编码并更新,而该消息可以在不首先知道键值的情况下发出。
  • upsert可以对任何异步修改进行编码,这些修改仅取决于旧值以及可以与upsert消息一起存储的一些辅助数据
  • 墓碑消息是upsert的一个特例。
  • upsert还可以用于增加计数器,更新文件访问时间,在取款后更新用户的帐户余额以及许多其他操作

upsert总结:

  1. 其实是查询+插入的组合操作,可以理解为“更新”。

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,Δ))=vquery(k)+insert(k,f(v,Δ))

  1. upsert消息刷到叶子节点时,f(v,Δ)将代替旧值v。

  2. 如果upsert消息还没刷到叶子时进行query,则在query返回结果之前将upsert消息应用到指定key。

  3. 树中对于同一个key可能存在多个upsert消息(多次修改),需要在根到叶的路径上收集所有upsert消息,并按时间顺序(时间戳)应用到指定key。

7.2 引入secondary indices

利用插入性能来提升查询性能

  • secondary indices是二级索引,可以维护多个索引,查询时根据查询的类型选择适当的索引
  • B-Tree维护多个索引的开销很大,因为插入和查询开销几乎相同,对于每列都维护索引是不现实的。
  • Bε-Tree则由于插入开销远小于查询开销,可以维护所有可能用到的索引。

设计辅助索引的三个规则:

  1. 对所有用于查询的列建立索引

    例如有三列(k1,k2,k3)的表,可能根据k1或k2查询,则需要维护两个Bε-Tree,一个按k1排序,一个按k2排序

  2. 保证每个索引都能直接查询到全部信息(k1,k2,k3)

    例如应用程序使用键k2查找k3值,则按k2排序的索引应为每个条目存储相应的k3值。

    很多辅助索引仅返回主索引的键,比如使用键k2查找k3值,会返回k1,再根据k1在主索引中找k3,但Bε-Tree不是。

  3. 应使应用程序尽可能执行范围查询

一个按k2排序

  1. 保证每个索引都能直接查询到全部信息(k1,k2,k3)

    例如应用程序使用键k2查找k3值,则按k2排序的索引应为每个条目存储相应的k3值。

    很多辅助索引仅返回主索引的键,比如使用键k2查找k3值,会返回k1,再根据k1在主索引中找k3,但Bε-Tree不是。

  2. 应使应用程序尽可能执行范围查询

    例如应用程序要找满足a ≤ k1 ≤ b,且k2满足某个谓词条件的所有条目,则应用程序应该维护一个二级索引:该索引按k1排序,且只包含k2满足条件的条目。

相关文章: