0
0
0
0
博客/.../

TiFlash 到底是怎么做到"一份数据两种引擎"的?——拆解 TiDB HTAP 的列存同步机制

 TiDBer_zpjleyd4  发表于  2026-10-09

一、先说结论:HTAP 的难点从来不是"能不能列存"

用 TiDB 做 HTAP,很多人第一反应是"行存 + 列存,双引擎嘛,能有多难"。

难的不是加一个列存引擎。难的是这个问题:列存副本怎么在不影响事务的前提下,和行存保持强一致?

这不是个新问题,但它有个非常刁钻的约束条件——TP 业务不能因为多了个 AP 副本而变慢,AP 查询也不能读到"半截"的数据。你没法像离线数仓那样"T+1 同步",因为那样实时分析就无从谈起;你也没法让 AP 查询去等 TP 的锁,因为那会把两边的延迟绑在一起。

TiDB 的解法概括起来是一句话:Raft Learner + Multi-Raft 异步复制 + Learner Read 读一致性校验。

下面拆开看。

二、TiFlash 不是"另一个数据库",它是一个 Raft Learner

先纠正一个常见的理解偏差:TiFlash 不是独立集群,也没有自己的存储引擎选主逻辑。

在 TiDB 的架构里,一张表的每个 Region 都有一个 Raft Group。默认 3 副本落在 TiKV 上,这是 Voter。当你执行:ALTER TABLE orders SET TIFLASH REPLICA 1;

TiDB 会在这个 Raft Group 里加一个特殊角色:TiFlash 作为 Learner(学习者副本)。

Learner 和 Voter 的区别是整个机制的关键:

维度

Voter(TiKV)

Learner(TiFlash)

参与 Raft 选举

是

否

参与多数派投票

是

否

影响写入延迟

是(需要多数派确认)

否(异步接收日志)

数据格式

行存(RocksDB)

列存(Delta + Stable)

数据一致性

强一致

通过 Learner Read 校验

"不参与选举"这一点,就是 TiFlash 能做到"零侵入"的根本原因。

写入链路是这样的:客户端写入 TiKV → Raft Leader 把日志复制给 2 个 TiKV Follower 和 1 个 TiFlash Learner → 只要 2 个 TiKV Follower 确认(形成多数派),这次写入就返回成功。TiFlash 什么时候追上来,不影响写入的响应时间。

所以在理想的网络条件下,加一个 TiFlash 副本对 TP 业务的影响是接近零的——这不是靠优化换来的,是架构上就不在关键路径里。

三、列存内部为什么有两份数据:Delta 和 Stable

TiFlash 收到 Raft 日志之后,不能直接把它变成列存文件。原因是列存擅长"批量写、追加写",而 Raft 日志是"小批、乱序、持续不断"的。

所以 TiFlash 内部做了两层缓冲:

  • Delta 层:接收到的新写入先以行存格式落在这里,保证写入能快速追加。
  • Stable 层:后台异步把 Delta 按列组织、压缩、归并成真正的列存数据。
  • 查询时:需要同时读 Delta + Stable 两层,按版本号(MVCC)合并出对当前快照可见的结果。

这个设计有一个直接推论,也是实际运维中最容易踩的坑:

Delta 层积压 = 查询变慢。 如果写入流量很大而 TiFlash 的归并跟不上,Delta 层会持续堆积,每次查询都要多扫一堆增量数据。表现出来就是"TiFlash 查询时快时慢",但你去查 SQL、查索引都没问题。

排查方法很直接,看 TiFlash 监控里 Delta Merge 相关的指标(TiFlash_Storage_DeltaMerge、TiFlash_Proxy_Region_Count),或者直接查系统表看 Region 的 Delta 堆积情况。如果是这个问题,能做的有:加大 storage.dt_page_gc_threshold、提归并并发、或者干脆把 TiFlash 的磁盘换成更好的 NVMe——Delta Merge 是 IO 密集型操作。

四、Learner Read:怎么保证不读到"旧数据"

这是整个机制里最精妙的一步。

因为 TiFlash 是异步追日志的,它天然落后于 TiKV 一点点。那 AP 查询怎么保证不读到落后数据?

TiDB 的做法是:在 TiFlash 执行 TableScan 之前,先去问一下 Raft Leader。

具体流程:

  1. TiDB 给 TiFlash 下发查询,带上一个 start_ts(事务开始时间戳,来自 PD 的 TSO)。
  1. TiFlash 在扫描前,用 start_ts 向该 Region 的 Raft Leader 发一次请求,问:"在 start_ts 这个时间点,你的 Raft 日志 index 是多少?"
  1. Leader 返回一个 index,记为 read_index。
  1. TiFlash 检查自己本地 Learner 的 index 有没有追上 read_index。没追上就等,追上了才继续扫。
  1. 扫描时按 MVCC 过滤,只返回 commit_ts <= start_ts 的数据版本。

第 4 步的"等",就是你偶尔会在 TiFlash 慢查询日志里看到的 wait_index 耗时。正常情况下它接近 0(因为 TiFlash 通常只落后毫秒级),但如果 TiFlash 的同步链路出了问题(网络抖动、磁盘打满、Delta Merge 卡住),这个等待会变成几十秒,查询就会超时。

这解释了为什么 TiFlash 节点的磁盘 IO 打满会同时导致:查询变慢 + 写入变慢 + 查询超时。 三者其实是同一个根因——Learner 追不上,读请求全卡在 wait_index。

一个实操建议:TiFlash 节点务必和 TiKV 节点独立部署、独立磁盘。官方也明确建议不要共享磁盘(尤其不要和 TiKV 共用同一块盘)。共享磁盘带来的 IO 争抢会直接放大 Learner Read 的等待时间。

五、和 ClickHouse / Doris 的方案对比:差异在哪

评估阶段大家一定会问:既然要实时分析,我直接用 ClickHouse 或 Doris 不好吗?这里给一个我实际用过的对比角度。

维度

TiDB + TiFlash

ClickHouse

Doris

数据一致性方案

Raft Learner + Learner Read

通常靠 Binlog/Kafka 同步,最终一致

靠 Broker Load / Routine Load,最终一致

是否需要额外同步链路

不需要,复制在 Raft 层内完成

需要(CDC + 消息队列 + 消费程序)

需要(Load 或 Flink 作业)

一致性保证

可做到快照读强一致

视同步方案,通常端到端延迟

视同步方案,通常端到端延迟

点查/事务能力

原生支持,行存强项

弱,不适合事务

弱,不适合事务

典型定位

一套系统扛 TP + AP,省掉同步链路和运维成本

极致 AP 性能,超宽表聚合

数仓场景,兼顾一定并发

选型的关键判断点不是"谁的 AP 性能更强",而是"你愿不愿意养一条额外的同步链路"。

如果你的业务已经有一套 TiKV 在扛事务,那加 TiFlash 的边际成本极低——不需要建 CDC 任务、不需要维护 Kafka Topic、不需要处理同步程序的故障重启、也不需要在业务侧做双写补偿。同步语义由 Raft 保证,这是自研链路很难做到的事。

反过来,如果你的 AP 场景是超宽表、超高基数聚合,或者数据量到了几十 TB 且基本不做更新,那 ClickHouse 的极致性能仍然有明显优势,加一条同步链路是值得的。

一句话总结:TiFlash 的价值不是"比 ClickHouse 快",而是"让 HTAP 不需要多养一条链"。

六、落地检查清单

如果你准备开 TiFlash,下面这几条建议在动手前先过一遍:

  1. 先评估 Delta Merge 的 IO 压力。写入量大的表尤其注意,TiFlash 磁盘 IOPS 要留足余量,别和 TiKV 共用盘。
  1. TiFlash 副本数不是越多越好。SET TIFLASH REPLICA N 是为了高可用和并行度,但每个副本都要独立消费 Raft 日志并做归并,写放大是 N 倍。多数场景 1~2 个副本够了。
  1. 确认表的写入频率。如果一张表每秒几万行更新,TiFlash 的 Delta 层会一直被追着跑,这类表要谨慎开列存——它更适合"写少读多"或"批量写"的分析型表。
  1. 先看慢查询里的 wait_index。这是判断"是 SQL 问题还是同步问题"的第一道分水岭。wait_index 高 → 去看同步链路和磁盘;wait_index 低但扫描量大 → 去看查询计划和下推。
  1. 提前规划资源隔离。AP 查询会吃掉 TiKV/TiFlash 的 CPU,如果不做隔离,TP 的 P99 会被拖累。这部分是系列第三篇的主题。

小结

回到开头那个问题:TiFlash 怎么做到强一致又不影响 TP?

三个机制各解决一个问题:Raft Learner 解决"不拖慢写入",Delta + Stable 双层解决"增量小批写如何融入列存",Learner Read 解决"异步副本如何保证读到正确的快照"。

理解这三件事之后,很多运维现象就变得可以解释了——为什么磁盘 IO 会同时影响读写、为什么 Delta Merge 是性能关键路径、为什么 TiFlash 需要独立部署。这些不是玄学,都是机制的直接推论。

下一篇我们进入查询侧,看 MPP 和 延迟物化 这两个机制,是怎么把一条慢查询从 22 秒压到 1 秒的。

<TiFlash 到底是怎么做到"一份数据两种引擎"的?——拆解 TiDB HTAP 的列存同步机制> by <TiDBer_zpjleyd4> is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论