原文发布于 quant67.com,转载请保留出处。
第 6 篇在讲 Snapshot 与 log GC 时预告过一件事:TiKV 的 Region 除了 Leader/Follower 这两种参与选举和提交的角色,还能挂一种只接收日志、不投票的副本。这正是 TiFlash 的接入点------Raft Learner。TiFlash 不是"给 TiKV 装了个列存插件",而是一整套独立部署、独立存储引擎的进程,通过 Learner 身份订阅 Region 的 Raft 日志,把行存变更实时重放成列存数据。
本篇只钉两件事:Learner 为什么能做到"不影响 TiKV、又能拿到全部写入" ,以及一条 Raft 日志具体怎么变成列存里的一行 。列存本身的向量化执行、压缩编码、跳数索引这些通用机制,本站已经在列存引擎系列用 ClickHouse/DuckDB 讲过完整源码级拆解,本文不重复,只在必要处指出 TiFlash 复用/借鉴了哪一部分。
版本锚定 :TiFlash 7.x/8.x 。核心机制引用 Huang et al., TiDB: A Raft-based HTAP Database , VLDB 2020(A 级,论文 §4.2)与 TiDB 官方文档 TiFlash Overview (A 级)。存储引擎内部实现(DeltaTree/
StorageDeltaMerge)引用自 TiFlash 开源仓库源码路径dbms/src/Storages/DeltaMerge/(A 级,社区源码解读交叉核对官方文档结论)。
一、为什么是 Learner,不是 Follower
Raft 协议里的 Follower 要参与日志提交的多数派确认和 Leader 选举投票;一旦把分析副本做成 Follower,它的健康状况会直接影响这个 Region 能不能形成多数派、要不要参与选主。TiFlash 团队的选择是引入 Learner 角色:只异步接收并 apply 已提交的日志,不投票、不参与 commit quorum。VLDB 2020 论文原话(§4.2):TiFlash "是由 Learner 节点组成的,它们只接收来自 Raft 组的日志并把行存 tuple 转换成列存数据;它们不参与提交日志或选主的 Raft 协议,因此对 TiKV 造成的负担极小"。
这个设计直接回答了 HTAP 系统的经典矛盾------资源隔离 和强一致性通常二选一:Learner 让 TiFlash 物理上独立部署(CPU/内存/磁盘不占 TiKV),同时因为日志本身携带 Raft 的全序与 commit index,TiFlash 依然能拿到与 TiKV 完全一致的写入顺序,不需要另起一套复制协议。
Learner 副本和 Region 的生命周期绑定在一起:Region 在 TiKV 侧 split/merge 时,TiFlash 侧对应的分区跟随同步 split/merge,逻辑上是"同一个 Region 的另一种物理形态",而不是一份独立维护的镶入式副本。
二、日志到列存:DeltaTree 的 Delta/Stable 两层
TiFlash 的计算引擎复用了 ClickHouse 的向量化执行框架(TiDB 官方文档原话:"提供列存存储,配合一层由 ClickHouse 高效实现的 coprocessor"),但存储引擎是自研的 DeltaTree (源码里叫 StorageDeltaMerge),不是 ClickHouse 的 MergeTree------这是本篇要划清的第一条分工线:向量化执行、表达式求值这些机制属于列存引擎系列已经拆过的范畴;DeltaTree 这套"为了追近实时更新而设计的列存布局"才是 TiFlash 独有、本篇要讲的内容。
DeltaTree 把每个 Region 对应的数据组织成若干 Segment,每个 Segment 内部按时间分两层:
- Delta Layer (
DeltaValueSpace):写优化层,新到的 Raft 日志先落进内存 MemTable,再 flush 到磁盘上的增量结构; - Stable Layer (
StableValueSpace):读优化层,数据按(主键, 版本)全局有序存储在只读文件DTFile里,只能整体替换、不能原地修改。
当 Delta Layer 积累到阈值,后台触发一次 Delta Merge(也叫 Major Compaction):把 Delta 与 Stable 的数据按主键有序合并,生成新的 Stable Layer,旧的 Delta 数据被清空。这个结构本质上是"只有两层的 LSM"------比通用 LSM 少了多层落盘的写放大,但仍然保留了"先写优化结构、后台合并到读优化结构"的核心思路。TiFlash 社区文档给出的经验值:不考虑压缩时理论写放大约 19 倍,但因为列存对同类型数据压缩效率高,生产环境实际写放大常低于 5 倍(B 级,社区技术博客,与官方文档的架构描述交叉一致,量化数字本身未见于 VLDB 论文,标注为工程经验值)。
Segment 会随着数据量增长做 split,随着数据量缩小做 merge,这与 TiKV Region 的 split/merge 是两条独立但类比的机制------一个在行存 key range 层面切分,一个在列存 Segment 层面切分,二者通过"同一批 Region"这个锚点对齐,但各自的触发阈值、执行路径互不复用。
2.1 扫描代价:Delta Index 与 Rough Set Filter
Delta Layer 和 Stable Layer 的数据在写入时天然不是全局有序合并好的------Delta 只保证内部小批次(社区文档称为 ColumnFile)各自有序,彼此之间的 key 范围可能重叠;Stable 层则整体有序。一次扫描要把这两层数据按主键版本正确地归并输出,如果每次都做多路归并,CPU 开销会随 Delta 层堆积的碎片增多而上升。
DeltaTree 用一个 Delta Index(社区文档描述为一个 B+ 树结构)缓存"上一次归并的结果位置",把开销较高的多路归并问题转换成增量式的顺序操作------新写入到达时只需要在 Delta Index 上做局部更新,而不必对整个 Segment 重新归并。这是 DeltaTree 相对于"每次扫描都从零归并 Delta + Stable"的关键优化。
Stable Layer 的每个 DTFile 内部按等长的 Pack (默认约 8192 行,由 dt_segment_stable_pack_rows 一类参数控制)组织数据,每个 Pack 附带粗粒度统计信息 (社区文档称为 Rough Set Filter,例如每列的 min/max),扫描时可以先用这些统计信息判断整个 Pack 是否可能命中过滤条件,命中不了的 Pack 直接跳过磁盘 I/O------这是列存里常见的跳数索引思路,列存引擎系列第 7 篇已经对 ClickHouse 的等价机制(minmax/set/bloom 跳数索引)做过源码级拆解,DeltaTree 的 Rough Set Filter 是同一类思路在 TiFlash 存储格式上的实现,本文不重复讲跳数索引的通用原理。
2.2 分区与 Region 不是一一对应
VLDB 2020 论文(§4.2)指出,TiFlash 里"每张表被划分成许多分区,每个分区覆盖 TiKV 侧若干个连续的 Region",用更大的分区做 range scan 效率更高------这意味着 TiFlash 的 Segment/分区边界与 TiKV 的 Region 边界不是强制一一对应的,而是"若干个 Region 对齐进同一个分区"。这个设计选择的动机很直接:列存做范围扫描本来就偏好大块连续数据,如果分区跟 Region 一样细碎,范围扫描要在很多小分区之间来回切换,抵消了列存批量读取的优势。
这也带来一个推论:TiKV 侧的 Region split 更频繁、更细粒度,不代表 TiFlash 侧的分区也要跟着以同样的粒度切分;TiFlash 只需要保证"每个分区完整覆盖它所对齐的那些 Region",具体的分区切分阈值由 DeltaTree 自己的 Segment split 逻辑决定,与 TiKV 的 Region 分裂阈值(如第 8 篇讲的默认 96 MB)是两套独立的参数体系。
2.3 MVCC 过滤:读时按版本裁剪
DeltaTree 写入的每一行数据都带着来源事务的版本信息(对应 TiKV MVCC 里的 commit_ts)。查询读取时,TiFlash 需要在扫描出的原始数据流上再叠加一层按版本过滤 (社区源码里对应 DMVersionFilterBlockInputStream 这一层):同一行可能存在多个历史版本,只有版本号不超过查询时间戳、且是该时间戳下最新版本的记录才会被保留。这一步在概念上与 TiKV 侧读 CF_WRITE 找可见版本(第 2 篇讲过的 MVCC 读路径)是同一个问题在不同存储格式上的解法:TiKV 靠 CF_WRITE 的 key 编码顺序定位版本,TiFlash 靠扫描流上叠加的版本过滤算子定位版本,两者共享的是同一套上游 MVCC 语义,只是物理实现完全独立。
三、读路径的一致性锚点:ReadIndex 与 safe-ts
TiFlash Learner 异步 apply 日志,意味着它的数据永远比 TiKV Leader 滞后一点。要在这种滞后下仍然提供快照隔离的正确读取,TiFlash 在收到分析查询时会向对应 Region 的 TiKV Leader 发一次轻量级的 ReadIndex 请求:Leader 记录当前的 commit index 作为下界,TiFlash Learner 等待自己的 apply 进度追上这个位点,再按查询指定的时间戳过滤数据返回。TiDB 官方文档把这个过程描述为"通过验证复制进度来实现与 TiKV 相同的 Snapshot Isolation"。
这一机制的工程效果是:TiFlash 不需要引入第二套两阶段提交或额外的分布式锁,只靠"Raft 日志的全序 + 复制进度确认"就能拿到与 TiKV 侧完全对齐的一致性快照。代价是每次读都有一次(通常很轻)与 Leader 的往返;论文实测显示这一开销在混合负载下对 OLAP 侧吞吐影响低于 5%、对 OLTP 侧吞吐影响约 10%(VLDB 2020,A 级实验数据,具体硬件与负载设定见论文 §6)。
这里只点出"读路径需要一次一致性确认"这个结论;把这套机制正式定义为"safe-ts"、量化可见时延、并回链 HTAP 三档新鲜度坐标,是下一篇的任务,本篇不重复展开。
四、组件划分与副本开通流程
TiFlash 进程内部分成三个组件(TiDB 官方文档):列存存储模块 (本文讲的 DeltaTree)、tiflash proxy (负责 Multi-Raft 协议通信,相当于 TiFlash 侧的 raftstore)、pd buddy (与 PD 协作,以表为单位把 TiKV 数据复制到 TiFlash)。这个三段划分说明一个容易被忽略的事实:TiFlash 并不是自己实现了一套 Raft 协议 ,tiflash proxy 复用的是与 TiKV 同源的 Multi-Raft 通信层,只是本地状态机换成了 DeltaTree,而不是 RocksDB------这也是为什么 TiFlash 能无缝挂进已有的 Region Raft 组,不需要额外设计一套复制协议。
开通一张表的 TiFlash 副本,用户侧只需要执行:
sql
ALTER TABLE x SET TIFLASH REPLICA n;
VLDB 2020 论文 §4.2 描述了这条语句触发的后台流程:该表被划分成许多分区,每个分区对应 TiKV 侧一段连续的 Region;相关 Region 的 Raft Leader 开始向新增的 Learner 副本同步数据------如果数据量小,走正常的日志复制;如果数据量大到日志复制效率太低,Leader 会直接发一份数据快照 给 TiFlash,减少海量历史数据补齐时的日志重放开销。初始化完成后,TiFlash 实例转入监听模式,持续消费该 Region 后续的 Raft 日志更新。这条流程与第 6 篇讲过的 TiKV Follower 追加新副本时的 Snapshot 机制是同一套基础设施的复用,只是接收端从"另一个 TiKV 副本"换成了"TiFlash Learner"。
五、存算分离:Write Node、Compute Node 与 S3
前四节讲的是 TiFlash 的耦合存算架构 (coupled storage and compute)------每个 TiFlash 节点既存数据又跑查询,这是默认部署形态,也是 VLDB 2020 论文描述的原始设计。从 TiDB v7.0 起,TiFlash 新增了一种可选的存算分离架构(Disaggregated Storage and Compute Architecture,v7.4 起 GA,TiDB Docs,A 级),把原本耦合在一个进程里的两种职责拆成两类可以独立扩缩容的节点:
- Write Node(写节点) :继续以 Raft Learner 身份接收日志、维护本地 DeltaTree(第二节讲的机制不变),并把数据持久化到 S3 或兼容对象存储;
- Compute Node(计算节点):不持有本地持久数据,处理 TiDB 发来的查询请求时,先向 Write Node 请求数据快照,读取尚未上传到 S3 的最新数据(Write Node 本地),其余大部分历史数据直接从 S3 读取。
这个拆分直接回应了耦合架构下的一个运维痛点:分析查询的计算压力和数据的持久化容量压力,往往不按同一个节奏增长 ------一次复杂聚合查询可能需要更多 CPU,但不需要更多存储;反之历史数据堆积需要更多存储,但不代表查询并发也在同步上升。耦合架构下扩容只能整节点扩,两种压力被迫绑在一起;拆分后 Compute Node 和 Write Node 可以按各自的压力曲线独立扩缩容。这与 Amazon Aurora(Verbitski et al., Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases , SIGMOD 2017,A 级,云数据库存算分离的奠基性工程论文)确立的"计算与存储分离、共享底层持久层"范式同源;微软的 Socrates(Antonopoulos et al., Socrates: The New SQL Server in the Cloud , SIGMOD 2019,A 级)在此基础上进一步把日志也拆成独立的第三层。TiFlash 的 Write/Compute 拆分可以理解为这一类云原生数据库架构范式在 HTAP 列存副本场景上的具体应用------TiFlash 官方文档没有直接引用 Aurora 或 Socrates 作为设计来源,这里的谱系关联是本文基于架构特征做出的归类,用来帮助读者定位这一设计在云原生数据库演进脉络里的位置。
两种架构目前互斥(TiDB Docs,A 级):一个 TiDB 集群里的所有 TiFlash 节点必须是同一种架构,不支持原地切换,只能整体下线重建;且存算分离架构只支持 S3 兼容对象存储,不支持本地磁盘。这意味着选择存算分离不是没有代价的默认升级,而是一次架构决策------用"独立弹性扩缩容"换"依赖对象存储、迁移成本高",具体值不值得切换取决于分析负载的资源压力曲线是否真的和存储容量压力曲线脱钩,这一点需要结合自身工作负载判断,官方文档本身也未承诺所有场景都受益。
六、MPP 执行:跨 TiFlash 节点的数据交换
前面几节讲的是单个 TiFlash 节点上"日志怎么变成列存",但一次分析查询通常要跨多个 TiFlash 节点并行执行、再汇总结果------这就是 第 13 篇提到但未展开的 MPP 模式 。TiDB 的物理计划在 MPP 模式下会插入一对专门的算子(TiDB Docs, Explain Statements in the MPP Mode,A 级):
- ExchangeSender:把本地计算的中间结果,按某种策略发给其他 TiFlash 节点;
- ExchangeReceiver:接收其他节点发来的数据,作为本节点后续计算的输入。
ExchangeSender 支持三种分发策略:
| 策略 | 行为 | 典型场景 |
|---|---|---|
| HashPartition | 按 Join key 或聚合 key 的哈希值分片,同一个 key 总是发往同一个下游节点 | Shuffle Hash Join、Hash Aggregation |
| Broadcast | 把整份数据复制发送给所有上游节点 | Broadcast Join(小表广播) |
| PassThrough | 只发给唯一一个上游节点 | 最终结果汇总回 TiDB |
Join 算法的选择由 TiDB 优化器基于代价估算决定(tidb_broadcast_join_threshold_size/_count 等系统变量,A 级):小表广播(Broadcast Join)省去了对大表做哈希重分布的网络开销,但要求被广播的表足够小;两表都不小时退化为 Shuffle Hash Join,两侧各自按 Join key 哈希重分布,保证相同 key 落到同一节点再本地 Join。这一层的决策逻辑与第 13 篇讲的"优化器决定发什么,本系列不重复"边界一致------本节只钉 MPP 模式下数据怎么在 TiFlash 节点间流动,不重复 Join 算法选择的代价模型细节。
这一层机制再次印证第四节"TiFlash 不自己发明 Raft"的对称结论:TiFlash 也不需要自己发明一套分布式 Shuffle 协议------ExchangeSender/Receiver 是 TiFlash 计算引擎自身的算子,工作在已经从 Raft Learner 拿到数据之后的计算阶段,与 Raft 复制、DeltaTree 存储是两个完全独立的层:一个解决"数据怎么从 TiKV 同步过来",一个解决"数据同步完之后,多节点怎么协作算出一个分布式查询的结果"。
七、与列存引擎系列的分工
| 话题 | 本篇(TiFlash Learner) | columnar-engine 系列 |
|---|---|---|
| 数据从哪来 | Raft 日志重放(行存变更转列存) | 独立写入路径(INSERT/批量导入),无 Raft 依赖 |
| 存储引擎 | DeltaTree(StorageDeltaMerge),Delta+Stable 两层 |
MergeTree(ClickHouse)/ Row Group(DuckDB) |
| 向量化执行 | 复用 ClickHouse 式执行框架,不重复讲机制 | 详细拆解 Block/Processor、compiled vs vectorized 争论 |
| 一致性目标 | 与同 Region 的 TiKV 行存快照对齐(HTAP 场景) | 独立引擎的自身 MVCC/快照隔离,不涉及跨引擎对齐 |
| 分裂/合并 | Segment split/merge,与 TiKV Region 松耦合对齐 | Part/Row Group 的 merge,与其他存储无关 |
这条边界的意义:如果你的问题是"TiFlash 的向量化聚合为什么快",答案已经在 columnar-engine 系列讲过;如果你的问题是"TiFlash 怎么知道该转哪些数据、转多快才不算数据丢失",答案在本篇------是 Raft Learner 的日志订阅与 Delta Merge 机制,不是列存执行引擎本身。
八、常见误解
误解一:TiFlash 副本是普通的 Raft Follower,只是存储格式不同。 不是。Follower 参与提交多数派与选主,Learner 不参与。这个角色差异是 TiFlash 能够"故障不影响 TiKV 可用性、同步不占用 TiKV 提交延迟"的根本原因,不能简单理解为"换了个存储引擎的 Follower"。
误解二:TiFlash 直接在 RocksDB 的 SST 上做列存扫描。 不对。TiFlash 是完全独立的存储引擎(DeltaTree),既不复用 TiKV 的 RocksDB 实例,也不共享 CF 布局;它接收的是已经提交的 Raft 日志(逻辑操作),自己重新组织成 Segment/Delta/Stable 结构,物理上和 TiKV 的存储完全隔离。
九、开放问题
- 写放大与压缩收益的权衡边界:Delta Merge 的理论写放大与列存压缩收益之间的关系,社区博客给出的经验值(约 19 倍理论、生产低于 5 倍)尚缺乏跨版本、跨压缩算法的系统对比,公开资料未给出可复现的基准方法。
- Learner 同步滞后的可观测性 :如何在不引入额外协议的前提下,把"这个 Region 的 Learner 落后多久"做成对运维友好、跨版本稳定的可观测指标,仍是工程演进中的话题,具体细节见第 15 篇与第 17 篇排障。
- 存算分离架构下 Compute Node 读 S3 的尾延迟边界:官方文档说明 Compute Node 需要访问 Write Node 获取最新数据、访问 S3 获取历史数据,但对象存储的访问延迟分布与本地磁盘不同,这对分析查询尾延迟的具体影响,官方公开资料未给出量化对比,本文不代替官方给出结论。
十、小结
三句话小结:
- TiFlash 用 Raft Learner 接收日志、不参与提交与选主,这是它能物理隔离部署又保持强一致读的关键设计。
- TiFlash 的存储引擎是自研的 DeltaTree(Delta 写优化层 + Stable 读优化层,靠 Delta Merge 定期合并),与 ClickHouse 的 MergeTree 是两套不同的存储布局。
- 读一致性靠 ReadIndex 确认复制进度实现,具体新鲜度量化与三档坐标回链留给下一篇。
参考资料
- Huang D., et al. TiDB: A Raft-based HTAP Database . VLDB 2020, §4.2(A 级,Learner 角色与列存转换机制定义)。www.vldb.org/pvldb/vol13...
- TiDB Docs, TiFlash Overview(docs.pingcap.com/tidb/stable/tiflash-overview,A 级)。
- PingCAP Blog, What I Learned from PingCAP's VLDB Paper(pingcap.com/blog,B 级,工程视角复述论文机制)。
- TiFlash 社区源码解读,DeltaTree 存储引擎设计与实现分析(tidb.net/blog、pingkai.cn,B 级,交叉核对官方文档架构描述;量化写放大数字标注为工程经验值)。
- TiDB Docs, TiFlash Disaggregated Storage and Compute Architecture and S3 Support(docs.pingcap.com,A 级,v7.0 experimental / v7.4 GA,Write Node/Compute Node 划分与限制)。
- TiDB Docs, Use TiFlash MPP Mode 、Explain Statements in the MPP Mode(A 级,ExchangeSender/ExchangeReceiver 三种分发策略、Broadcast/Shuffle Join 选择变量)。
论文(A 级,架构范式对照,非 TiFlash 官方声明的直接设计来源)
- Verbitski, A., et al. Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases. SIGMOD 2017。
- Antonopoulos, P., et al. Socrates: The New SQL Server in the Cloud. SIGMOD 2019。
站内
- 列存引擎系列 index(向量化执行、MergeTree 机制,不在本篇重复)。
- 本系列 第 6 篇 Snapshot、log GC 与副本迁移(Learner 角色预告)、第 13 篇 TiDB SQL 层边界、第 15 篇 新鲜度与一致性。
下一篇 :新鲜度与一致性
返回 系列目录