纸上得来终觉浅?从 0 到 1 实现分布式 KV 后,我才读懂了 TiDB 的设计
1 前言1.1 项目背景项目源码GDUT-CJL/Raft at dev4.2还在迭代作为一个从机械转码的研究生作者的编程之路始于研究生期间的项目。那时我需要处理大量并行计算任务经常思考一个问题当多个节点需要协作时如何保证它们看到的数据是一致的这个问题在我自学计算机、读到 Raft 论文时找到了答案。Raft 通过领导者选举和日志复制巧妙地解决了分布式共识问题——我被这种设计的优雅深深吸引。但看懂论文和真正理解是两回事。我试过回答如果 Leader 挂了会发生什么网络分区怎么处理这类问题却发现自己根本说不清楚。 于是我决定与其继续看不如亲手写一个。三个月后JLDB 诞生了——一个基于 Raft 的分布式 KV 存储系统。它支持多存储引擎、水平分片、流水线复制甚至还有一个轻量级的 PD 路由模块。1.2 本文目的“本文将围绕以下三个问题展开探讨希望能给同样对分布式存储感兴趣的同学提供一些参考TiKV 的核心架构是什么相较于我的项目工业级系统在哪些维度做了关键优化这些优化背后反映了哪些设计权衡”2 初识TiDB架构对于TiKV的学习相对来说是比较友好的。因为TiKV作为一个国产的数据库许多的说明和使用手册都是中文的而且也很容易在官网中获取得到TiDB 简介 | TiDB 文档中心这里作者也把TiDB的官方文档放在这里官方文档更加权威本小节作者也是参考其中的内容进行了探究。2.1 TiDB“三层结构”TiDB主要分为三层主要的结构TiDB Server、TiKV Server、PD Server2.1.1 TiDB ServerTiDB Server:SQL 层对外暴露 MySQL 协议的连接 endpoint负责接受客户端的连接执行 SQL 解析和优化最终生成分布式执行计划。TiDB 层本身是无状态的实践中可以启动多个 TiDB 实例通过负载均衡组件如 TiProxy、LVS、HAProxy、ProxySQL 或 F5对外提供统一的接入地址客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。TiDB Server 本身并不存储数据只是解析 SQL将实际的数据读取请求转发给底层的存储节点 TiKV或 TiFlash。主要功能处理客户端的连接作为数据库的“门户”TiDB Server 负责接收并管理来自各种客户端如应用程序、MySQL 客户端的网络连接。它兼容 MySQL 协议使外部应用可以像使用 MySQL 一样连接和操作 TiDB。SQL语句的解析和编译接收 SQL 语句后TiDB Server 会对其进行词法分析、语法分析验证其正确性。接着进入编译阶段包括语义分析、逻辑优化如子查询优化、谓词下推和物理优化如选择访问路径、索引最终生成一个可在分布式环境下高效执行的物理执行计划。关系型数据库与KV转化这是 TiDB 的核心设计。TiDB Server 内部维护着一个逻辑 Schema 到物理存储的映射关系。它会将关系型数据模型如表、行、索引透明地编码为 TiKV 可以存储的 Key-Value 对。例如表中的每一行数据、每一个索引项都会被编码成一个独立的 KV 条目并通过一个统一的 Key 映射方案来定位。SQL语句的执行作为“计算引擎”TiDB Server 负责驱动并执行上一步生成的物理计划。它会根据计划向底层的 TiKV 存储节点发送数据读取Get/Scan或 Coprocessor 计算请求并对返回的中间结果进行汇总、连接、排序、聚合等高级计算最终生成 SQL 查询结果集返回给客户端。在线DDL的执行schema load、worker、start job为保证业务连续性TiDB 支持在线变更表结构。TiDB Server 中的相关模块会协调整个 DDL 过程Owner 选举出一个 TiDB 节点作为 DDL 工作的协调者该协调者会将 DDL 任务封装为一个Job并放入作业队列专用的DDL Worker 会顺序地从队列中取出 Job 并执行通过多阶段变更如 Add Index 的 backfill 阶段来最小化对线上操作的影响。垃圾回收GC模块老版本数据的回收由于 TiDB 使用 MVCC 机制数据的更新和删除会产生历史版本。GC 模块负责定期默认每10分钟清理那些已提交事务不再需要的旧版本数据以回收存储空间。它通过计算一个“安全点”来确定哪些历史版本可以被安全删除并向 TiKV 发送清理指令。2.1.2 PD ServerPD Server整个 TiDB 集群的元信息管理模块负责存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构提供 TiDB Dashboard 管控界面并为分布式事务分配事务 ID。PD 不仅存储元信息同时还会根据 TiKV 节点实时上报的数据分布状态下发数据调度命令给具体的 TiKV 节点可以说是整个集群的“大脑”。此外PD 本身也是由至少 3 个节点构成拥有高可用的能力。建议部署奇数个 PD 节点。主要功能整个集群TiKV的元数据存储PD 存储并维护着整个集群所有Region数据分片的元数据信息包括每个 Region 的范围、副本位置、状态如 Leader 在哪等。它相当于一个全局的“路由表”TiDB Server 在执行查询时会向 PD 询问数据应该去哪个 TiKV 节点查找。分配全局ID和事务IDPD 作为一个全局唯一的 ID 分配器为整个集群分发各种类型的 ID例如表 ID、数据库 ID、事务 ID 等。这确保了在分布式环境下所有标识符的全局唯一性和有序性是数据组织和事务处理的基础。生成全局时间戳TSOPD 是集群的“授时中心”它通过一个高可用的服务为所有分布式事务分配全局单调递增的时间戳。这个 TSO 是实现 MVCC 多版本并发控制和保证分布式事务全局顺序一致性的关键。收集集群信息进行调度PD 持续从所有 TiKV 节点收集实时状态信息如存储容量、Region 大小、负载情况。基于这些信息PD 的调度器会自动做出决策发出调度指令例如在节点间均衡 Region 分布、为故障节点补充副本、迁移热点 Region 的 Leader 以分散读写压力等从而保证集群的负载均衡和高可用。提供TiDB Dashboard服务PD 集成了 TiDB Dashboard这是一个内置的、图形化的集群管理界面。用户可以通过 Web 浏览器直接访问方便地监控集群状态、分析 SQL 性能、诊断问题、生成诊断报告极大地简化了日常运维工作。2.1.3 存储节点TiKV Server TiFlashTiKV Server负责存储数据从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region每个 Region 负责存储一个 Key Range从 StartKey 到 EndKey 的左闭右开区间的数据每个 TiKV 节点会负责多个 Region。TiKV 的 API 在 KV 键值对层面提供对分布式事务的原生支持默认提供了 SI (Snapshot Isolation) 的隔离级别这也是 TiDB 在 SQL 层面支持分布式事务的核心。TiDB 的 SQL 层做完 SQL 解析后会将 SQL 的执行计划转换为对 TiKV API 的实际调用。所以数据都存储在 TiKV 中。另外TiKV 中的数据都会自动维护多副本默认为三副本天然支持高可用和自动故障转移。主要功能数据持久化数据最终会持久化存储在磁盘上确保即使发生断电等故障数据也不会丢失。TiKV 底层使用 RocksDB 作为单机存储引擎来实现高效的本地数据持久化分布式事务支持支持完整的分布式 ACID 事务。它采用了Google Percolator事务模型通过两阶段提交2PC和乐观锁机制保证跨多个物理节点的数据读写操作具有原子性和一致性。副本的强一致性和高可用性数据以Region为单位通过Raft 共识算法复制到多个节点通常为3副本。这确保了数据的强一致性所有副本数据相同和高可用性少数副本故障不影响服务并能自动恢复。MVCC多版本并发控制通过为每次数据修改分配一个全局递增的时间戳版本号即在key后面添加时间戳版本号来实现多版本并发控制。这使得读写操作互不阻塞读操作可以获取到某个时间点的一致性快照而无需加锁。Coprocessor算子下移了提升复杂查询的效率TiKV 提供了 Coprocessor 框架。它允许将一部分计算任务如过滤、聚合从数据库计算层“下推”到存储层执行大幅减少了需要传输的数据量降低了网络开销和延迟。TiFlashTiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是在 TiFlash 内部数据是以列式的形式进行存储主要的功能是为分析型OLAP的场景加速。主要功能列式存储提高分析查询效率TiFlash 采用列式存储格式。它将表中每一列的数据连续存储在一起而非像行式存储那样存储整行。这种格式特别适合分析场景因为这类查询通常只涉及少数几列但需要扫描海量行列存可以极大地减少 I/O 数据量并利用向量化计算技术大幅提升聚合、扫描等操作的性能。支持强一致性和实时性TiFlash 节点通过Raft Learner 协议异步地从 TiKV 复制数据。这意味着它不仅能提供与行存一致的快照隔离级别还能实时读取到最新的数据无需等待 ETL 过程。用户进行复杂分析时看到的就是业务当前时刻的真实数据状态。业务隔离TiFlash 节点是独立部署和扩展的。分析型查询AP负载可以被定向到 TiFlash 节点上执行从而与在 TiKV 上运行的事务处理负载TP负载在物理资源上完全隔离。这避免了分析查询消耗大量资源影响核心在线业务真正实现了 HTAP 的“业务隔离”目标。智能选择TiDB 的智能优化器可以自动判断一条 SQL 查询更适合在行存TiKV还是列存TiFlash上执行。对于分析型查询优化器会自动选择将计算下推到 TiFlash而事务型查询则继续使用 TiKV。用户也可以通过 SQL 提示手动指定执行引擎。2.2 TiKV 的核心组件了解了整体TiDB的架构可以发现raft的协议主要是应用在TiKV这一层于是作者深入一个 TiKV 节点内部对其架构进行进一步拆解下面是作者关于TiKV Server的一些个人理解一个TiKV节点有多个Region副本每个Region的多个副本分布在不同TiKV节点TiKV节点参与多个Raft集群每个Region对应一个Raft集群Region是存储单位按Key Range划分每个Region的Raft组有一个Leader处理该Region的读写TiKV使用Multi-Raft多个Region的Raft组并行运行PD管理所有Region的元数据TiKV节点向PD汇报PD下发调度指令Multi-Raft是 TiKV 的核心设计 一个 TiKV 节点上同时运行多个 Raft Group每个对应一个 Region这样可以实现数据的水平分片分散 Leader 压力支持细粒度的调度如热点 Region 迁移2.3 对比视角JLDB 与 TiKV 的相似起点读完 TiKV 的架构作者不禁感到一种似曾相识——JLDB 的设计与 TiKV 有着惊人的相似之处维度JLDBTiKV共识机制自研Raftetcd/raft-rs存储引擎多引擎可插拔最终采用Rocksdb较多Rocksdb分片机制Hash分片Range分片路由服务Lightingweight PDOpenrestyPD通讯协议gRPCgRPC客户端协议RESP自定义我们解决了相似的问题如何用 Raft 保证数据一致性如何将数据分布到多个节点如何路由请求到正确的节点如何优化读写性能但走向了两条不同的路JLDB 追求 灵活性与学习价值 ——支持多种存储引擎、自研 Raft 以深入理解原理TiKV 追求 极致性能与生产就绪 ——基于成熟库、深度优化、完善的运维体系接下来的章节我将从四个维度详细对比这两种设计选择背后的考量。3 核心对比对比维度 JLDB (Raft-KV) TiDB (TiKV)优化启发共识组单 Group类似 etcdMulti-Raft数百/千个 Group突破单组写入瓶颈数据分片一致性哈希 (静态)Range 分片 (动态分裂)实现自动负载均衡读性能优化Lease Read (已实现) PipelineLease Read Follower Read ReadIndex分散 Leader 读压力存储引擎单 RocksDB (统一写入)双 RocksDB (raftdb kvdb)避免日志写放大批量处理Batch 提交Batch System WriteBatch显著降低 I/O 次数对于raft的算法的优化可以参看这篇文章(44 封私信 / 80 条消息) TiKV 源码解析系列 - Raft 的优化 - 知乎3.1 性能优化批量处理的“工业级进化”作者在项目中实现了批量提交将多条日志打包一次性提交获得了 4 倍的吞吐量提升。而在 TiKV 中则更进了一步通过批量系统Batch System和WriteBatch两种方式协同Batch System批量处理系统raftstore 模块会定期轮询通常是每 1 秒一次一次性驱动数十甚至上百个 Ready 的 Raft Group。所有待持久化的数据被聚合到一起放入RocksDB 的 WriteBatch中一次性写入磁盘将 I/O 请求次数显著降低。WriteBatch 批处理组通过一次Write系统调用将同一批次的多个 Raft Group 的状态变更包括多项 Raft 日志一次性持久化到磁盘。在 3 副本配置下这能大幅减少磁盘 I/O 和系统调用防止写入瓶颈。JLDB中采用的批量处理的测试结果测试场景并发操作类型是否批量QPS平均延迟P99 延迟成功率低并发写密集10RCSET/RCGET✅ 批量81.54122.62ms186.11ms100%低并发写密集10SET/GET❌ 未批量12.10816.97ms962.75ms100%低并发写密集10RCSET/RCGET❌ 未批量11.49859.95ms1.06s100%中并发混合读写50RCSET/RCGET✅ 批量381.07131.13ms189.94ms100%中并发混合读写50SET/GET❌ 未批量12.123.88s4.24s100%高并发写密集200RCSET/RCGET✅ 批量328.81601.58ms800.92ms100%高并发写密集200SET/GET❌ 未批量12.1613.56s16.73s100%高并发写密集150RCSET/RCGET❌ 未批量11.2411.24s14.21s100%高并发读密集200RCSET/RCGET✅ 批量238.71828.26ms1.32s100%高并发读密集200SET/GET❌ 未批量11.8613.90s17.52s100%QPS 提升数倍至数十倍采用批量提交后低并发写密集场景 QPS 提升约7.1 倍高并发场景提升高达29.3 倍。延迟大幅降低批量提交将高并发下的平均延迟从13 秒级降至600 毫秒级P99 延迟从16 秒降至800 毫秒。吞吐量显著增加不批量时所有场景 QPS 始终在 11~12 左右近乎平坦而批量后 QPS 随并发合理增长中并发即可达 381说明系统吞吐能力被有效释放。3.2 存储隔离多引擎 vs Column Family这是 TiKV 解决写放大问题的关键核心每个 TiKV 实例内部维护着两个独立的 RocksDB 实例。raftdbRaft 日志数据库专门存储 Raft 协议的日志是典型的顺序写模式单独部署可以最大化 I/O 性能避免影响用户数据。kvdbKV 存储数据库存储用户的实际数据。由于其写入模式是随机的对应到 SSTable 的不同层级若与日志混布会产生磁盘 I/O 干扰导致写入延迟飙升。对比小结我的项目中Raft 日志和数据都写入了同一个 RocksDB 实例这在高负载下可能造成写放大与性能抖动。未来可考虑将日志和数据存储分离或者采用最新的Partitioned-Raft-KV方案将数据按 Region 粒度打散分担到不同的 RocksDB 实例中以彻底消除写放大问题。3.3 分片架构从“静态”到“动态”的智能调度这本质上是关于“数据分片与负载均衡”的机制选择一致性哈希 vs. Range 分片JLDB采用一致性哈希进行静态分片。特点是实现相对简单添加节点时会涉及部分数据迁移。TiKV采用Range 分片 (Region)。它将整个 Key 空间划分为一系列连续的范围。动态分裂Dynamic Split热点处理在 TiKV 中当某个 Region 的数据量或 QPS 超过阈值时默认约 96MBTiKV 会自动将该 Region 分裂为两个。负载打散PD 组件会持续监控这些 Region并通过调度操作将新分裂的 Region 自动迁移到负载较低的节点上实现全自动的负载均衡。对比小结一致性哈希解决了分布式系统的基本拓扑问题。TiKV 的 Range PD 机制则额外解决了“热点数据的自动均衡”问题。3.4 读性能优化引入 Follower Read探究raft的线性一致性读方法-CSDN博客我项目中实现的 Lease Read 已在本地实验中将吞吐量从 185 req/s 提升至 16,775 req/s提升约 90 倍大幅降低了读延迟。TiKV 除了也实现 Lease Read 外还引入了Follower Read它将部分强一致性读请求分流到 Follower 节点上。由于 Follower 通过 Raft 日志被动同步在保证线性一致性的前提下它们也能提供最新的数据读取服务。这让系统的整体读吞吐能力随节点数线性扩展不再让 Leader 成为性能瓶颈。JLDB引入Follower Read的测试报告指标直接读取Leader状态机 (leaseRead)Raft一致性读 (每次走Raft协议)最终一致性读 (任意节点读取)总请求数10,00010,00010,000总耗时596.11ms54.04s629.24ms吞吐量 (req/s)16,775.42185.0515,892.23平均延迟560.53μs53.72ms586.89μs最小延迟80.56μs5.25ms79.35μs最大延迟22.98ms161.24ms21.46msP95延迟1.61ms99.36ms1.69msP99延迟5.06ms120.49ms5.80ms错误数0004 总结与思考4.1 核心差异总结项目模块JLDBTiKV (TiDB)差异本质共识与分片Single Raft Consistent HashingMulti-Raft Range Splitting解决热点的复杂性存储引擎配置1 x RocksDB (Mix)2 x RocksDB (Separated)写入性能保障与隔离读取优化Lease ReadLease Read Follower Read最大化读吞吐量批量处理粒度Batch Raft CommandsBatch System WriteBatch极致降低 I/O 次数4.2 技术启示从我的 JLDB 项目到 TiKV表面上看是代码量与工程复杂度的几何级增长但更深层的差异是一场从“先让它正确”到“再让它稳健”的思维跃迁。TiKV 教会我的不只是如何实现一个更快的 Raft而是分布式存储系统不仅要能在理想环境下跑通测试用例更要能在真实世界中承受高并发、节点故障、网络分区、热点访问等一系列“不友好”的场景。这种对异常、对性能、对可观测性的极致追求才是一套工业级系统的灵魂所在。当然TiKV 身上值得我继续学习的远不止本文提到的这些。流水线复制、异步提交、事务模型、内存池管理、PD 智能调度……每一个方向都像一座深不见底的宝藏等待我去挖掘也为我继续优化 JLDB 提供了清晰的技术路线图。我会沿着这条路继续走下去。也希望这篇对比笔记能给同样热爱分布式存储的同学一点启发亲手造过轮子你才会真正理解工业级轮子好在哪里。JLDB 永远不会成为 TiKV但它让我真正理解了 TiKV 为什么这样设计。从这个角度来说造轮子的时间花得值。5 附JLDB架构图图 JLDB整体架构图图 JLDB单节点集群图图 JLDB数据流