前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、TiDB 简介

TiDB 是一个开源的分布式 NewSQL 数据库,采用 HTAP(混合事务分析处理) 架构,同时支持 OLTP(在线事务处理)和 OLAP(在线分析处理)场景。其核心设计目标是 水平扩展、高可用、强一致性,并兼容 MySQL 协议,降低用户迁移成本

image-20250715114059410

1、核心组件详解

+-----------------------+
|      Application      |
+-----------+-----------+
            |
            v
+-----------------------+
|      TiDB Server      |  (SQL Layer, 无状态)
+-----------+-----------+
            |
            v
+-----------------------+
|        PD Server      |  (元数据管理 & 调度)
+-----------+-----------+
            |
            v
+-----------------------+
|      TiKV Server      |  (行式存储, OLTP)
+-----------+-----------+
            |
            v
+-----------------------+
|     TiFlash Server    |  (列式存储, OLAP)
+-----------------------+

1)TiDB ServerSQL 层)

功能

  • 兼容 MySQL 5.7+ 协议,支持 JDBCORM 框架(如 MyBatisHibernate)。
  • 负责 SQL 解析、优化、执行计划生成,将 SQL 转换为分布式 Key-Value 操作(TiKV API)。
  • 无状态设计,可通过负载均衡(如 LVSHAProxy)水平扩展。

关键特性

  • 分布式执行引擎:将复杂查询(如 JoinAggregation)拆分为并行任务下发到 TiKV/TiFlash
  • 事务管理:基于 PD 分配全局事务 ID,协调 TiKV 完成分布式事务(2PC)。
  • 统计信息:收集表数据分布,优化查询计划。

2)PD (Placement Driver) Server(调度中心)

功能

  • 集群的 “大脑”,负责 元数据管理、调度、全局事务 ID 分配
  • 提供 TiDB Dashboard(内置监控和管理界面)。

核心机制

  • Region 调度
    • 负载均衡:监控节点负载,迁移 Region 使存储和流量均匀。
    • 热点调度:检测热点 Region(如频繁访问的 Key Range),自动分裂或迁移。
    • 副本修复:节点宕机时,在其他健康节点上补足副本。
  • 全局路由表
    • 记录所有 Region 的分布信息(Key → Region → TiKV节点)。
    • 客户端(TiDB)通过 PD 定位 Key 所在的 RegionLeader 副本位置。
  • 高可用
    • 基于Raft 协议实现多副本(建议 3/5 节点部署)。
    • 领导者故障时自动切换。

关键数据结构

  • Region 映射表:记录每个 Key Range 对应的 TiKV 节点。
  • 全局时间戳(TSO:为分布式事务提供单调递增的时间戳。

3)TiKV Server(分布式 Key-Value 存储)

功能

  • 分布式行式存储引擎,默认 3 副本,通过 Raft 协议保证数据一致性。
  • 支持 ACID 事务(默认快照隔离级别)。

核心设计

  • 数据分片(Region
    • 每个 Region 管理一段连续的 Key Range(如 [a, b))。
    • 默认 96MB ,超过一定阈值触发分裂(Split),过小则合并(Merge)。
  • 分布式事务
    • 采用 Percolator 模型2PC + 乐观锁)。
    • 事务流程:
      1. PD 获取 start_ts(事务开始时间戳)。
      2. 写入数据时暂存到锁(Lock),提交时通过 2PC 确认。
      3. 清理锁并标记提交成功(commit_ts)。
  • 存储引擎
    • 基于 RocksDBLSM-Tree),适合高吞吐写入。

4)TiFlash Server(列式存储引擎)

功能

  • 列式存储,加速分析型查询(如 GROUP BYSUM)。
  • 通过 Raft Learner 机制从 TiKV 异步同步数据(近实时)。

关键特性

  • MPPMassively Parallel Processing)引擎

    • 将复杂查询拆分为多个并行任务,下推到 TiFlash 节点执行。
  • 智能选择执行引擎

    • 优化器自动判断使用 TiKVOLTP)或 TiFlashOLAP)。

    • 用户也可强制指定:

      SELECT /*+ READ_FROM_STORAGE(TIFLASH[t]) */ * FROM t;
      

2、数据读写流程示例

1)写入流程(INSERT

  1. 客户端 发送 SQLTiDB Server
  2. TiDB 解析 SQL,向 PD 查询目标数据的 Region 位置。
  3. PD 返回 Leader TiKV 节点地址。
  4. TiDBTiKV Leader 发起写请求:
    • TiKV 通过 Raft 协议同步数据到 Follower
    • 写入成功后返回客户端 ACK

2) 查询流程(SELECT

  1. TiDB 解析 SQL,判断使用 TiKV(点查)或 TiFlash(分析)。
  2. 若是聚合查询:TiDBTiFlash 下发 MPP 任务,并行计算后汇总结果。
  3. 若是事务查询:从 TiKV 读取最新已提交数据(基于 MVCC)。

3、特性和应用场景

1)推荐场景

  • 数据量预计快速增长至亿级以上,需避免分库分表。
  • 要求高可用、强一致事务(如金融、支付)。
  • 需同时支持 OLTP(交易)与 OLAP(分析)的混合场景。
  • 希望降低分布式数据库开发与运维成本。

2)不推荐场景

  • 数据量小(百万级以下)、业务逻辑简单,MySQL 单机即可满足。
  • MySQL 生态深度依赖(已使用分库分表、使用类似 MyCat 等中间件)
  • 无扩展需求的传统业务。

1)5 大核心特性

a、一键水平扩缩容:

得益于 TiDB 存储计算分离的架构的设计,可按需对计算、存储分别进行在线扩容或者缩容,扩容或者缩容过程中对应用运维人员透明。

b、高可用:

数据采用多副本存储,数据副本通过 Muli-Raft 协议同步事务日志,多数派写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本地理位置、副本数量等策略,满足不同容灾级别的要求。

c、实时 HTAP

Hybrid Transactional/Analytical Processing(混合事务分析处理)):提供行存储引擎 [TiKV]、列存储引擎 [TiFlash] 两款存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎 TiFlash 之间的数据强一致。TiKVTiFlash 可按需部署在不同的机器,解决 HTAP 资源隔离的问题。

d、云原生的分布式数据库:

专为云而设计的分布式数据库,通过 [TiDB Operator]可在公有云、私有云、混合云中实现部署工具化、自动化。

e、兼容 MySQL 协议和 MySQL 生态:

兼容 MySQL 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到 TiDB。提供丰富的[数据迁移工具]帮助应用便捷完成数据迁移。

2)4 大应用场景

a、金融行业场景:

金融行业对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高。传统的解决方案的资源利用率低,维护成本高。TiDB 采用多副本 + Multi-Raft 协议的方式将数据调度到不同的机房、机架、机器,确保系统的 RTO (<= 30sRPO = 0

  • RTORecovery Time Objective,恢复时间目标):指系统从故障发生到 完全恢复服务 的最大允许时间。≤ 30s 表示必须在 30秒内 让业务重新正常运行。
  • RPORecovery Point Objective,恢复点目标):指系统恢复后允许的 数据丢失量 的最大时间窗口。= 0 表示要求 零数据丢失,即故障前的最后一笔操作也必须恢复。

b、海量数据及高并发的 OLTP 场景:

传统的单机数据库无法满足因数据爆炸性的增长对数据库的容量要求。TiDB 是一种性价比高的解决方案,采用计算、存储分离的架构,可对计算、存储分别进行扩缩容,计算最大支持 512 节点,每个节点最大支持 1000 并发,集群容量最大支持 PB 级别。

  • 业务快速扩展场景:初创公司或业务增长迅速的企业,TiDB 的弹性扩展能力可避免频繁重构数据库架构。
  • 海量数据在线业务:电商订单、金融交易(亿级数据),需实时查询与统计,MySQL 分库分表难以满足,TiDB 可直接应对。
  • 高并发读写场景:秒杀、社交平台 Feed 流,TiDB 的分布式架构可承载百万级 ` QPS,避免 MySQL` 单机瓶颈。

c、实时 HTAP 场景:

  • TiDB 适用于需要实时处理的大规模数据和高并发场景。TiDB4.0 版本中引入列存储引擎 TiFlash,结合行存储引擎 TiKV 构建真正的 HTAP 数据库,在增加少量存储成本的情况下,可以在同一个系统中做联机交易处理、实时数据分析,极大地节省企业的成本。

  • TiDB 支持 HTAP(混合事务与分析处理),无需数据同步至数仓,直接在生产库执行复杂分析查询。

d、数据汇聚、二次加工处理的场景

TiDB 适用于将企业分散在各个系统的数据汇聚在同一个系统,并进行二次加工处理生成 T+0T+1 的报表。与 Hadoop 相比,TiDB 要简单得多,业务通过 ETL 工具或者 TiDB 的同步工具将数据同步到 TiDB,在 TiDB 中可通过 SQL 直接生成报表。

三、TiDB 数据库的存储

image-20250715153146406

1、Key-Value

TiKVTiDB 的底层存储引擎,其核心设计是一个 分布式的、有序的 Key-Value 存储系统。它的数据模型和实现方式与传统关系型数据库(如 MySQL)完全不同,而是更接近底层存储系统(如 RocksDBHBase)。以下是关键点的深入解析:

1)TiKVKey-Value 存储模型

(1)核心设计:有序的 Key-Value MapTiKV 的数据模型可以抽象为一个 全局有序的 Key-Value Map,具备以下特性:

  • KeyValue 都是字节数组([]byte,没有预定义的结构。
  • Key 按字典序(二进制顺序)排列,支持高效的范围查询(Range Scan)。
  • 提供原子性操作:如 GetPutDeleteScan

(2)与 SQL 表的区别::TiKV 本身不感知“表”、“行”、“列”等 SQL 概念,它只认识 KeyValue

  • TiDB 的 ` SQL 层负责将表数据编码为 Key-Value,再存储到 TiKV` 中。
  • 例如,一行 SQL 数据可能被拆解为多个 Key-Value 对(如行数据 + 索引数据)。

2)TiKVKey-Value 存储实现

a、底层存储引擎:RocksDB

TiKV 使用 RocksDB(基于 LSM-Tree 的高性能存储引擎)作为单机存储,其特点:

  • 所有 Key-ValueKey 排序存储,支持高效的 SeekNext操作。
  • 写入先写入内存(MemTable),再刷盘(SSTable),适合高吞吐场景。
  • 支持快照(Snapshot)和事务(Transaction)。

b、分布式扩展:Multi-Raft

  • TiKV 将数据分片为 Region,每个 Region 是一个独立的 Raft 组。
  • 每个 Region 的多个副本(通常 3 个)分布在不同的 TiKV 节点上,通过 Raft 协议保证一致性。

3)问题

问题1:TiDBKeyvalue 的组成是什么样的

(1)Key 的组成TiDB 中每个 Key 是一个 编码后的字节序列,包含多层信息(以表数据为例):

Key = [TablePrefix][TableID][RecordPrefix][RecordID]
  • TablePrefix:标识这是一个表数据(而非索引或其他元数据)。
  • TableID:表的唯一 ID
  • RecordPrefix:标识这是行数据(Row)。
  • RecordID:行的主键值(或 TiDB 隐式分配的 _tidb_rowid)。

示例:假设表 usersTableID=123,某行主键 user_id=100,则其 Key 可能编码为:

Key = [0x74][0x0000007B][0x72][0x00000064]
       │      │           │      └── 主键值 100
       │      │           └── 行数据标识
       │      └── 表 ID 123
       └── 表数据前缀

(2)Value 的组成Value 存储的是 行的实际数据,编码为二进制格式(如 Protocol Buffers):

Value = [Column1][Column2][Column3]...
  • 如果表 usersname(字符串)、age(整数)两列,Value 可能存储为:

    Value = ["Alice"][25]
    

问题2:数据如何存储在 Region 中?

  • Region 管理的是 Key 的区间(如 [start_key, end_key)),但实际存储的是 完整的 Key-Value
  • 查找流程
    1. TiDB 计算某行数据的 Key(如主键 user_id=100对应的 Key)。
    2. 通过 PD 查询该 Key 所属的 Region(如 Region1负责 [0x50, 0xA0))。
    3. 从该 RegionLeader 副本中读取或写入对应的 Key-Value 数据

问题3:为什么只提 Key 不提 Value

  • Region 的分片依据是 Key
    • 数据分片(Region)的边界由 Key 的区间决定(如 [a, b)),而 Value 是跟随 Key 存储的。
    • 调度和负载均衡时,PD 只需关注 Key 的分布,无需感知 Value 的内容。
  • Value 是“被动存储”的:只要 Key 定位到正确的 Region,其对应的 Value 自然会被存取。

问题4:TiDBKey 编码是什么样的

1)表数据(Row

  • Key[TablePrefix][TableID][RecordPrefix][PrimaryKey]

  • Value:行的所有列数据(如 {"name": "Alice", "age": 25})。

    • Key = [0x74][0x0000007B][0x72][0x00000064]
             │      │           │      └── 主键值 (100)
             │      │           └── 行数据标识 (RecordPrefix)
             │      └── 表 ID (123)
             └── 表数据前缀 (TablePrefix)
      

(2)唯一索引

  • Key[TablePrefix][TableID][IndexPrefix][IndexValue][PrimaryKey]

    • 例如对 email字段建唯一索引,Key 可能包含 "alice@example.com"
  • Value:空(因为可通过索引直接定位到行数据)。

    • [0x69][0x0000007B][0x69][0x00000001]["Alice"][0x00000064]
           │      │           │      │        │         └── 主键值(100)
           │      │           │      │        └── 索引列值("Alice")
           │      │           │      └── 索引 ID(假设为 1)
           │      │           └── 索引标识('i')
           │      └── 表 ID(123)
           └── 索引前缀(0x69)
      

(3)非唯一索引

  • Key[TablePrefix][TableID][IndexPrefix][IndexValue][PrimaryKey]
  • Value:空(因为可通过索引直接定位到行数据)。

问题5:为什么这样涉及Key的编码

  1. 高效定位数据
    • 通过前缀(TablePrefix + TableID)快速过滤出某张表的数据。
    • 通过 RecordPrefix区分行数据和索引数据。
  2. 支持范围查询Key 按字典序排列,查询 user_id BETWEEN 10 AND 20时,直接扫描连续 Key 区间即可。
  3. 兼容索引:索引的 Key 类似,但前缀和结构略有不同(例如索引 Key 会包含索引列的值)。

问题6:为什么选择 Key-Value 模型

  • 灵活性:无需预定义 Schema,适合分布式扩展。
  • 高性能:有序 Key 存储 + LSM-Tree 结构,写入和范围查询效率高。
  • 分布式友好:易于分片(Region)和负载均衡。

2、Region

为了便于理解,假设所有的数据都只有一个副本。可以将 TiKV 看作一个巨大而有序的 KV Map,为了实现存储的水平扩展,数据将被分散在多台机器上。对于一个 KV 系统,将数据分散在多台机器上有两种比较典型的方案:

  • Hash:按照 KeyHash,根据 Hash 值选择对应的存储节点。
  • Range:按照 KeyRange,某一段连续的 Key 都保存在一个存储节点上。

TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,可以用 [StartKey,EndKey) 这样一个左闭右开区间来描述。每个 Region 中保存的数据量默认维持在 256 MiB 左右(可以通过配置修改)。

1)Region 的核心特性

  • 分片单位
    • 每个 Region 负责一段连续的 Key Range(如 [start_key, end_key)),默认 96MB~256MB(可配置)。
    • Region 超过阈值时自动 分裂(Split,过小时可能 合并(Merge
  • 数据分布
    • PD 调度器确保 Region 均匀分布在集群所有 TiKV 节点上。
    • 新增节点时,PD 自动从其他节点迁移 Region 到新节点(无需人工干预)。

2)RegionRaft 的协同设计

角色 写请求 日志生成 读请求(默认) 选举参与 数据同步 用途场景
Leader 处理所有读写请求
Follower ✅(可配置) 数据同步,分担读负载
Learner 副本迁移或扩容的过渡阶段

a、Raft Group 组成

  • 每个 Region 对应一个独立的 Raft Group,包含多个副本(通常 3 个)。
  • 副本角色:
    • Leader:处理所有读写请求,日志复制
    • Follower:同步数据,Leader 故障时参与选举。
    • LearnerLearnerRaft 协议中的临时角色,主要用于副本扩容或迁移

b、读写流程

  • 写请求
    1. Leader 接收写请求(如 Put(key, value))。
    2. 通过 Raft 日志复制到多数 Follower
      1. MemTable 写满后触发 flush(默认128MB)
      2. 将日志数据顺序写入磁盘 SST 文件
      3. 更新元数据标记已持久化的日志索引
    3. 提交成功后返回客户端。
  • 读请求:直接由 Leader 处理(保证强一致性),无需 Raft 交互。

image-20250716164224335

c、典型场景

  1. 正常运行Leader 处理写请求,Follower 同步数据,提供读服务(若开启 follower-read)。
  2. 副本调度PDLearner 副本加入 Raft Group,待其同步后晋升为 Follower
  3. 故障恢复Leader 故障时,Raft 协议从 Follower 中选举新的 Leader,确保服务连续性。

d、关键设计原则

  • 强一致性:通过 Raft 协议保证数据在 Leader 和 Follower 之间的一致性。
  • 高可用性:多副本(默认 3 个)和自动故障转移机制(Leader 选举)。
  • 灵活扩展:通过 Learner 副本实现无缝的副本迁移和负载均衡。

3)region 大小

  • 稳态目标256MB(PD 调度的理想参考值)
  • 分裂触发
    • 渐进式分裂:Region 达到 144MB(96MB×1.5)开始准备
    • 强制分裂:Region 达到 384MBregion-max-size)必须分裂
  • 合并触发:相邻 Region 均小于 54MBregion-merge-size)时可能合并

  • 影响:
    • Region 设置过小可能导致 Region 数量过多,增加资源开销和调度复杂度。
    • Region 设置过大可能导致单个 Region 负载过重,影响查询性能和系统稳定性。

3)问题分析

a、为什么必须通过 Leader

  • Raft 要求所有写操作必须由 Leader 发起,避免多 Leader 导致数据冲突(脑裂问题)。
  • 读操作可以直接走 Leader(保证强一致性),或配置为从 Follower 读(可能读到旧数据)。

b、数据一致性如何保证?

  • 写一致性:日志必须复制到多数派(N/2+1)才会提交,即使部分节点宕机也不丢数据。
  • 读一致性:默认从 Leader 读,总能读到最新已提交的数据。

c、故障场景怎么处理?

  • 短期宕机Raft Leader 重试,依赖多数派维持服务。
  • 长期宕机PD 调度器介入,补足副本并移除故障节点。
  • 设计哲学Raft 保证 短期一致性PD 保证 长期高可用。两者协同实现生产级分布式存储。
行为 Raft 协议原生能力 PD 调度器的增强
Follower 短暂宕机 Leader 重试,不补副本 不干预
Follower 长期宕机 无法自动修复 自动补副本,移除宕机节点
数据一致性保障 依赖多数派存活 确保最终副本数达标(如 3 副本)
触发条件 超时机制(选举/心跳) 定时健康检查 + 策略配置

3、RocksDB

RocksDB 作为 TiKV 的核心存储引擎,用于存储 Raft 日志以及用户数据。每个 TiKV 实例中有两个 RocksDB 实例,两个 RocksDB 实例是 并行运行在同一个进程(TiKV 进程)中,但它们的数据是物理隔离

  • 一个用于存储 Raft 日志(通常被称为 raftdb
  • 一个用于存储用户数据以及 MVCC 信息(通常被称为 kvdb)。
    • kvdb 中有 4 个 ColumnFamilyraftlockdefaultwrite

问题:为什么要这样设计?

目的 说明
性能隔离 Raft 日志写入频繁且对延迟敏感,与用户数据混合可能互相影响。
运维灵活 可以为 raftdbkvdb 配置不同的磁盘路径(比如 raftdb 放在更快的 SSD 上)。
资源控制 可以分别设置 raftdbkvdb 的内存缓存大小、线程数、压缩策略等。

1)RocksDB 实例

TiKV 采用RocksDB 实例设计,实现数据与日志的物理隔离:

存储内容 RocksDB 实例 I/O 模式 主要用途
用户数据(Key-Value kvdb 宏观:随机机写入 存储所有 Region 的实际数据
Raft 日志 raftdb 顺序追加 存储 Raft 协议的操作日志
  • raft 列:用于存储各个 Region 的元信息。仅占极少量空间,用户可以不必关注。
  • lock 列:用于存储悲观事务的悲观锁以及分布式事务的一阶段 Prewrite 锁。
    • 当用户的事务提交之后,lock cf 中对应的数据会很快删除掉,因此大部分情况下 lock cf 中的数据也很少(少于 1GB)。
    • 如果 lock cf 中的数据大量增加,说明有大量事务等待提交,系统出现了 bug 或者故障。
  • write 列:用于存储用户真实的写入数据以及 MVCC 信息(该数据所属事务的开始时间以及提交时间)。
    • 当用户写入了一行数据时,如果该行数据长度小于或等于 255 字节,那么会被存储 write 列中,否则该行数据会被存入到 default 列中。
    • 由于 TiDB 的非 unique 索引存储的 value 为空,unique 索引存储的 value 为主键索引,因此二级索引只会占用 writecf 的空间。
  • default 列:用于存储超过 255 字节长度的数据。

2)RocksDB 的内存占用

a、BlockBlockCacheRegion关系

  • Region 是数据分布式管理的基本单位TiKV 将整个 Key-Value 空间按范围划分成多个小段,每一段就是一个 `Region

  • Block 是磁盘存储的基本单元TiKV 使用 RocksDB 作为底层存储引擎,RocksDB 将存储在磁盘上的文件按照一定大小切分成 Block,默认大小是 64KBBlock 是数据在磁盘上物理存储的最小单位,包含实际的键值对数据。

  • Region 包含多个 Block:一个 Region 中的数据会存储在磁盘上的多个文件中,这些文件又会被切分成多个 BlockBlock 是构成 Region 数据存储的底层单元。
  • BlockCache:作为内存缓存。读取 Region 数据时,会以 Block 为单位从磁盘读取并加载到内存中的 BlockCache,若 BlockCache 中存在所需 Block,则可直接从内存读取,无需访问磁盘,从而提高读取性能

b、BlockBlockCache 的设计逻辑

  • BlockCache 的作用:作为内存缓存,它利用 LRU(最近最少使用)算法优先保留高频访问的 Block,大幅降低磁盘 IO 次数。TiKV 默认分配系统总内存的 45%BlockCache(上限建议 60%),这是因为读取性能对数据库体验至关重要,而内存资源的合理分配能最大化缓存命中率。

c、MemTable 的内存控制

写入 RocksDB 中的数据会写入 MemTable

  • 写入流程:数据先写入 MemTable(内存结构),避免直接落盘的性能开销。当单个 MemTable 达到 128MB 时切换到MemTable,旧 MemTable 会异步刷盘为 SST 文件。
  • 内存上限控制TiKV 包含 2RocksDB 实例(分别对应 kvraft 数据),共 4 个 ColumnFamily(CF),每个 CF 最多允许 5MemTable(含活跃和待刷盘的)。按单个 128MB 计算, 4 × 5 × 1 28MB = 2.5GB 的上限设计,既能保证写入吞吐量(避免频繁刷盘),又能防止内存占用过高导致 OOM,这部分配置确实不建议轻易修改。

image-20250716165441717

设计选择 优点 缺点 解决方案
先写WAL 保证数据安全 额外I/O开销 顺序追加优化
内存缓冲 高速响应写入 内存占用受限 MemTable轮流工作
异步刷盘 不阻塞客户端 可能短暂数据风险 后台线程加速处理

3)RocksDB 的空间占用

  • RocksDB 的空间放大(≈1.11)是 LSM-tree 结构为优化写入性能的固有特性,通过合并机制控制在较低水平。
  • TiKV 的额外空间开销来自 MVCC 的多版本存储,由 GC 机制动态平衡(旧版本及时回收则总放大可控)。

  • 两者共同构成了 TiKV 的空间占用特性,是 “高性能写入” 与 “事务支持” 权衡的结果。
  • 多版本

    • 版本追加:L0 层的 SST 文件由 MemTable 刷盘生成,由于写入顺序是 “追加” 而非 “原地更新”,新写入的 key 会直接生成新的 SST 文件,导致 L0 中不同 SSTkey 范围可能重叠(同一 key 可能有多个版本)。这是为了保证写入性能(避免随机写磁盘),但会暂时保留旧版本。

    • 版本合并: L0 文件数量达到阈值,会触发与 L1 的合并(compaction)。合并时会按 key 范围重新整理,删除重复版本(只保留最新的),因此 L1 及更深层的 SST 文件范围不重叠,同一 key 仅存一个版本。当某一层级的 SST 文件总大小超过配置的目标大小时,会触发该层级与下一层级的 Compaction

    • 简言之,多版本仅 “临时” 存在于 L0,深层则通过合并保证版本唯一性,平衡了写入性能和空间效率。

    • MemTable 无序 多版本 ~128MB <1%
      L0 可能重叠 多版本 不定 ~1%
      L1 不重叠 单版本 8MB ~9%
      L2+ 不重叠 单版本 递增 ~90%
  • 空间放大
    • 层级合并:当某一层级的 SST 文件总大小超过配置的目标大小时,会触发该层级与下一层级的 Compaction,随着不断的 Compaction,数据会从高层级逐渐向下层级移动,越老的数据越容易被移动到更低的层级,经过多次这样的合并操作后,最终会到达 LN 层,LN 层存储着相对最老、最冷的数据
    • 空间放大:指 “实际存储占用” 与 “有效数据大小” 的比值。RocksDB 中,每层总大小是上一层的 N 倍(TiKV 默认 N=10),因此空间放大系数约为 1.11
  • MVCC 机制的额外影响:
    • 异步删除:TiKV 会通过异步 GC(垃圾回收) 定期清理过期的旧版本(默认每 10 分钟触发一次),删除不再需要的版本数据
    • 空间占用:未被 GC 回收的旧版本(最近 10 分钟内的写入)会占用额外空间。

4)RocksDB 后台线程与 Compact

RocksDB 中,将内存中的 MemTable 转化为磁盘上的 SST 文件,以及合并各个层级的 SST 文件等操作都是在后台线程池中执行的。后台线程池的默认大小是 8,当机器 CPU 数量小于等于 8 时,则后台线程池默认大小为 CPU 数量减一。通常来说,用户不需要更改这个配置。如果用户在一个机器上部署了多个 TiKV 实例,或者机器的读负载比较高而写负载比较低,那么可以适当调低 rocksdb/max-background-jobs 至 3 或者 4。

5)WriteStall

RocksDBL0 与其他层不同,L0 的各个 SST 是按照生成顺序排列,各个 SST 之间的 key 范围存在重叠,因此查询的时候必须依次查询 L0 中的每一个 SST为了不影响查询性能,当 L0 中的文件数量过多时,会触发 WriteStall 阻塞写入。

如果用户遇到了写延迟突然大幅度上涨,可以先查看 Grafana RocksDB KV 面板 WriteStall Reason 指标,如果是 L0 文件数量过多引起的 WriteStall,可以调整下面几个配置到 64

rocksdb.defaultcf.level0-slowdown-writes-trigger
rocksdb.writecf.level0-slowdown-writes-trigger
rocksdb.lockcf.level0-slowdown-writes-trigger
rocksdb.defaultcf.level0-stop-writes-trigger
rocksdb.writecf.level0-stop-writes-trigger
rocksdb.lockcf.level0-stop-writes-trigger

4、Titan

Titan 是基于 RocksDB 的高性能单机 key-value 存储引擎插件。当 value 较大(1 KB 以上或 512 B 以上)的时候,Titan 在写、更新和点读等场景下性能都优于 RocksDB。但与此同时,Titan 会占用更多硬盘空间和部分舍弃范围查询。随着 SSD 价格的降低,Titan 的优势会更加突出,让用户更容易做出选择。

1)核心特性

  • 支持将 valueLSM-tree 中分离出来单独存储,以降低写放大。
  • 已有 RocksDB 实例可以平滑地升级到 Titan,这意味着升级过程不需要人工干预,并且不会影响线上服务。
  • 100% 兼容目前 TiKV 所使用的所有 RocksDB 的特性。

2)适用场景

Titan 适合在以下场景中使用:

  • 前台写入量较大,RocksDB 大量触发 compaction 消耗大量 I/O 带宽或者 CPU 资源,造成 TiKV 前台读写性能较差。
  • 前台写入量较大,由于 I/O 带宽瓶颈或 CPU 瓶颈的限制,RocksDB compaction 进度落后较多频繁造成 write stall
  • 前台写入量较大,RocksDB 大量触发 compaction 造成 I/O 写入量较大,影响 SSD 盘的寿命。

开启 Titan 需要考虑以下前提条件:

(1)Value 较大。value 平均大小比较大,或者数据中大 value 的数据总大小占比比较大。目前 Titan 默认 1KB 以上大小的 value 是大 value,根据实际情况 512B 以上大小的 value 也可以看作是大 value

(2)没有范围查询或者对范围查询性能不敏感。Titan 存储数据的顺序性较差,所以相比 RocksDB 范围查询的性能较差,尤其是大范围查询。在测试中 Titan 范围查询性能相比 RocksDB 下降 40% 到数倍不等。

  • 数据分离导致访问路径变长(Value分离)
    • 当进行范围扫描时,Titan 不仅需要读取 key 的顺序,还需要根据每个 keyBlobFile 中查找对应的 value,这就引入了额外的 I/O 操作和随机读,降低了效率。
  • 缓存命中率下降:
    • RocksDB 使用 Block Cache 来缓存 SST 中的 block,一个 block 包含多个 key-value 对,适合范围查询复用。
    • TitanBlobCache 是针对单个 value 的缓存,无法很好地支持连续 value 的访问。
  • BlobFile 没有良好的局部性
    • RocksDB 的 SST 文件是按 key 排序的,在做 range scan 时,利用操作系统的预读机制,高效地批量读取连续的数据块。
    • Titan 的 BlobFile 是按写入顺序存储的,key 的顺序和 value 在 BlobFile 中的位置没有关联

(3)磁盘剩余空间足够,推荐为相同数据量下 RocksDB 磁盘占用的两倍

  • Titan 降低写放大是通过牺牲空间放大达到的
  • keyvalue被分开存储,每个 key 都需要保存一个指向 value 的指针,这会占用额外空间。
  • BlobFile 中存在垃圾数据(旧版本 value),在 GC 前不会被回收,也会导致空间占用上升
  • Titan 逐个压缩 value,压缩率比 RocksDB
    • RocksDB 是按 block(通常是几十 KB)为单位进行压缩的,利用相邻数据的重复性可以获得更高的压缩率。
    • Titan 是按单个 value 进行压缩的,缺乏上下文信息,压缩效率较低

优势:

  • 减少写放大:在 RocksDB 中,当存储大量数据时,特别是在执行 compaction 操作时,可能会产生显著的写放大问题。Titan 通过将大 value 存储在独立的 BlobFile 中,并仅在 LSM-tree 中存储指向这些值的小型索引,减少了 compaction 过程中需要移动的数据量,从而降低了写放大。
  • 提高写入性能:由于减少了写放大,Titan 能够提供更高的写入性能,特别是在高并发写入场景下。这对于需要频繁更新和插入大量数据的应用程序来说是一个重要的改进。
  • 更好的缓存使用效率Titan 设计允许更高效的缓存使用,尤其是针对小 key 和大 value 的情况。LSM-tree 只存储较小的键值对,使得同等内存容量下可以缓存更多的索引、布隆过滤器和 block,进而提高了点查询性能。

  • 更有效的空间管理Titan 引入了垃圾回收机制来清理不再使用的 BlobFile 数据,这有助于更有效地利用磁盘空间,避免了由于无效数据占据的空间浪费。

3)核心原理

TitanFlush Compaction 的时候将 value 分离出 LSM-tree,这样写入流程可以和 RocksDB 保持一致,减少对 RocksDB 的侵入性改动。

image-20250716172448054

a、键值分离存储:减少写入放大

Titankeyvalue 分离存储,核心逻辑如下:

  • 分离策略
    • value 大小 ≥ min_blob_size(默认 32KB)时,value 被写入独立的 BlobFileSST 文件仅存储 key 和指向 BlobFile 的索引(类似指针)。
    • value 仍直接存储在 SST 文件中,保持对 RocksDB 接口的兼容。
  • BlobFie 结构
    • 有序存储BlobFile 中的 key-value 按顺序排列,支持预读取(prefetch)优化顺序读取性能。
    • 元数据管理:每个 BlobFile 包含 meta blockmeta index block,记录文件属性和索引信息,便于快速定位数据。
  • 写入流程:写入时,大 value 先写入 WALWrite-Ahead Log),再通过 TitanTableBuilder 生成 BlobFileSST 文件仅记录 keyBlobFile 索引。这种设计避免了大 valueLSM-tree 中被多次重写,将写入放大降低 50% 以上

b、智能 GC 机制:高效回收空间

由于 value 被分离存储,当 key 过期或删除时,对应的 value 不会立即被移除,因此需要通过 GC 来回收这些不再使用的空间。Titan 有两种 GC 策略:传统 GCLevel Merge默认)

  • 传统 GC
    • 原理Titan 定期扫描各个 BlobFile,统计它们里面有多少是“有用的”数据,有多少是“垃圾”。
    • 选择策略:选择那些垃圾最多BlobFile 来进行回收。
    • 操作方式:把这些 BlobFile 中有用的 value 提取出来,写入新的 BlobFile,跳过无效的垃圾数据,从而释放空间。
    • 缺点:这种 GC 是独立运行的,可能会在系统忙的时候也运行,造成额外的 I/O 负载。
  • Level Merge(层级合并)
  • 原理:利用 RocksDB 本身做 LSM-tree compaction(合并) 的过程,在合并 SST 文件时,同步处理对应的 BlobFile

  • 优势
    • 不是单独运行,而是在 compaction 时顺便清理 BlobFile,效率更高。
    • 高效:不需要额外扫描所有 BlobFile,只处理当前 compaction 涉及的数据
    • 低开销:复用 compactionI/OCPU 资源,减少系统压力。
    • 空间利用率高:及时清理无效数据,避免磁盘浪费。
  • 操作方式:只清理那些在 SST 合并过程中涉及的 BlobFile,避免全盘扫描。
特性 传统GC Level Merge
是否独立运行 是(单独运行) 否(配合 compaction)
回收效率 一般 更高
系统开销 较大 更低
适合场景 垃圾较多、低负载时 高并发、写入密集型场景

4)配置

TitanRocksDB 兼容,也就是说,使用 RocksDB 存储引擎的现有 TiKV 实例可以直接开启 Titan。

a、数据迁移

当你在 TiDB 中启用 Titan 存储引擎后,并不会立刻把所有数据迁移到 Titan。数据会逐步迁移,主要通过以下两个方式:

  1. 前台写入:新写入的数据如果满足条件(value 大于 min-blob-size,默认 32KB),会直接写入 Titan。
  2. 后台 CompactionRocksDB 在做 compaction(合并 SST 文件)时,会检查数据大小,把符合条件的大 value 分离到 TitanBlobFile

b、常用参数

参数分类 参数名 推荐值 适用场景 注意事项
存储分离阈值 min-blob-size 1KB-128KB - 1KB:高频点查 - 32KB:混合负载(默认) - 128KB:顺序扫描为主 值越小写性能越好,但会降低扫描性能
压缩策略 blob-file-compression zstd - none:超高频更新 - lz4:平衡场景 - zstd:冷数据/高压缩比需求 需配合zstd-dict-size(建议32KB)使用
缓存配置 blob-cache-size 总内存×50% - BlockCache Value频繁读取场景 需确保BlockCache足够容纳LSM-Tree热数据
GC回收机制 discardable-ratio 0.3(30%) 增大:减少GC频率,但增大空间, 减小:降低空间放大,但是频繁 GC 低于0.2可能导致GC过频繁
并发控制 max-background-gc 4-8线程 GC压力大的场景(CPU>70%) 每线程需预留1-2核CPU资源
I/O 限流 rate-bytes-per-sec 100-200MB/s 高负载场景防止I/O饱和 需根据磁盘类型调整(NVME可设更高)
共享缓存(v8.0+) shared-blob-cache true 内存受限环境 当开启共享缓存时,Block 文件具有更高的优先级,TiKV 将优先满足 Block 文件的缓存需求,然后将剩余的缓存用于 Blob 文件

5、Partitioned Raft KV

在 TiKV 中,这是从 v6.6.0 版本开始引入的一种新的存储引擎架构,它的核心思想是:每个 Region 使用一个独立的 RocksDB 实例来存储数据,而不是像以前那样所有 Region 共享一个 RocksDB 实例。所以,它也被称为 Partitioned Raft KV,即“分区 Raft KV”。

问题1:为什么引入它?

在旧版本中(v6.6.0 之前),TiKV 使用单个 RocksDB 实例来管理所有 Region 的数据,这在面对大规模集群、高写入负载时,会带来一些性能瓶颈。写放大和读放大严重扩缩容速度慢Region 之间资源争抢,性能互相影响难以支持更大规模的数据和集群

1)核心优势

优势 说明
更好的写入性能 每个 Region 独立写入自己的 RocksDB,减少写放大,提高吞吐
更快的扩缩容 Region 搬迁更轻量,不涉及整个 RocksDB 的操作
Region 间物理隔离 数据操作互不干扰,提升稳定性和可预测性
支持更大集群 更好地利用内存和 CPU 资源,相同硬件下支持更多数据和更大的集群
资源控制更精细 可以为每个 RocksDB 实例设置不同的配置,如内存、压缩策略等

a、更好的写入性能

  • 减少写放大:在传统的单个 RocksDB 实例中,所有 Region 的数据共享同一个 LSM-tree 结构,进行 compaction 操作时会涉及大量数据的重写,导致写放大问题。而在 Partitioned Raft KV 中,每个 Region 独立拥有一个 RocksDB 实例,compaction 只影响单个 Region 的数据,减少了不必要的数据重写,从而降低了写放大。
  • 并行处理能力增强:由于每个 Region 数据独立管理,多个 Region 的写入可以并行执行,充分利用多核 CPU 资源,提高整体写入吞吐量。

b、更快的扩缩容速度

  • 轻量化搬迁:当需要进行节点间的数据迁移(如扩容或缩容)时,只需迁移特定 Region 对应的 RocksDB 实例,而不需要像以前那样处理整个 RocksDB 实例的数据,大大简化了搬迁过程,提高了迁移效率。

c、Region 间物理隔离

  • 减少相互影响:在传统模式下,如果某个 Region 发生频繁的写入或读取操作,可能会对其他 Region 的性能造成影响。而通过为每个 Region 分配独立的 RocksDB 实例,实现了真正的物理隔离,确保各个 Region 的操作不会互相干扰,提升了系统的稳定性和可预测性。
  • 减少资源争用:由于不同 Region 的数据操作互不干扰,避免了因共享 RocksDB 实例导致的资源竞争问题,使得扩容或缩容操作更加平滑和高效。

d、支持更大集群

  • 更高效的内存利用:虽然每个 Region 需要维护自己的 RocksDB 实例,但可以通过合理配置(如调整缓存大小),使得整个 TiKV 节点上的内存使用更加均衡,支持更多的 Region 和数据量。
  • 分布式计算友好:这种架构有利于分布式计算任务的执行,因为每个 Region 的数据独立,便于并行处理和分布式调度。

e、资源控制更精细

  • 动态调整:根据实际运行情况,可以动态调整某些 RegionRocksDB 配置,以适应不断变化的工作负载,提升整体系统性能。

  • 更细粒度的资源控制:可以根据每个 Region 的具体需求,灵活配置对应的 RocksDB 参数(如缓存大小、压缩策略等),实现更精细化的资源管理和优化。

2)使用限制(实验特性)

注意:该功能在引入时为实验特性,存在一定限制:

限制项 说明
❌ 不支持基于 EBS 的快照备份 暂不支持 BR 等工具的某些备份方式
❌ 不支持 Online Unsafe Recovery 和 Titan 与部分高级功能不兼容
❌ 不支持部分 tikv-ctl 命令 unsafe-recoverraw-scan
❌ 不兼容 TiFlash 暂不支持与 TiFlash 联合使用
❌ 初始化后不能开关 一旦启用,不能关闭;初始化前也不能中途启用

4、MVCC

TiKV 支持多版本并发控制。假设有这样一种场景:某客户端 A 在写一个 Key,另一个客户端 B 同时在对这个 Key 进行读操作。如果没有数据的多版本控制机制,那么这里的读写操作必然互斥。在分布式场景下,这种情况可能会导致性能问题和死锁问题。

有了 MVCC,只要客户端 B 执行的读操作的逻辑时间早于客户端 A,那么客户端 B 就可以在客户端 A 写入的同时正确地读原有的值。即使该 Key 被多个写操作修改过多次,客户端 B 也可以按照其逻辑时间读到旧的值。

TiKVMVCC 是通过在 Key 后面添加版本号来实现的。没有 MVCC 时,可以把 TiKV 看作如下的 Key-Value 对:

Key1 -> Value
Key2 -> Value
……
KeyN -> Value

有了 MVCC 之后,TiKVKey-Value 排列如下:

Key1_Version3 -> Value
Key1_Version2 -> Value
Key1_Version1 -> Value
……
Key2_Version4 -> Value
Key2_Version3 -> Value
Key2_Version2 -> Value
Key2_Version1 -> Value
……
KeyN_Version2 -> Value
KeyN_Version1 -> Value
……

注意,对于同一个 Key 的多个版本,版本号较大的会被放在前面,版本号小的会被放在后面(见 [Key-Value]一节,Key 是有序的排列),这样当用户通过一个 Key + Version 来获取 Value 的时候,可以通过 KeyVersion 构造出 MVCCKey,也就是 Key_Version。然后定位到第一个大于等于这个 Key_Version 的位置。

四、TiDB 数据库的计算

分布式 SQL 的核心计算靠近数据(下推) + 并行处理(分片) + 结果合并Reduce)。

1、表数据与 Key-Value 的映射关系

1)表数据与 Key-Value 的映射关系

唯一KEY在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个 (Key, Value) 键值对,需要考虑如何构造 Key。首先,OLTP 场景下有大量针对单行或者多行的增、删、改、查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一 ID(显示或隐式的 ID),以方便快速定位。其次,很多 OLAP 型查询需要进行全表扫描。如果能够将一个表中所有行的Key 编码到一个区间内,就可以通过范围查询高效完成全表扫描的任务。

设计思想:基于上述考虑,TiDB 中的表数据与 Key-Value 的映射关系作了如下设计:

  • Id为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用 TableID 表示。表 ID 是一个整数,在整个集群内唯一。
  • IdTiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID

每行数据按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]

2)索引数据和 Key-Value 的映射关系

唯一索引:对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID

二级索引:对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null

3)映射关系小结

上述所有编码规则中的 tablePrefixrecordPrefixSepindexPrefixSep 都是字符串常量,用于在 Key 空间内区分其他数据,定义如下:

tablePrefix     = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep  = []byte{'i'}

2、SQL 层简介

TiDBSQL 层,即 TiDB Server,负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层 TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等

1)SQL 运算

最简单的方案就是通过 表数据与-key-value-的映射关系)方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。

比如 select count(*) from user where name = "TiDB" 这样一个 SQL 语句,它需要读取表中所有的数据,然后检查 name 字段是否是 TiDB,如果是的话,则返回这一行。具体流程如下:

  1. 存储-构造 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,使用 0MaxInt64 根据行数据的 Key 编码规则,就能构造出一个 [StartKey, EndKey)的左闭右开区间。
  2. 查询-扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据。
  3. 过滤数据:对于读到的每一行数据,计算 name = "TiDB" 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据。
  4. 计算 Count(*):对符合要求的每一行,累计到 Count(*) 的结果上面。

这个方案是直观且可行的,但是在分布式数据库的场景下有一些显而易见的问题:

  • 在扫描数据的时候,每一行都要通过 KV 操作从 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大。
  • 并不是所有的行都满足过滤条件 name = "TiDB",如果不满足条件,其实可以不读取出来。
  • 此查询只要求返回符合要求行的数量,不要求返回这些行的值。

2)分布式 SQL 运算

为了解决上述问题,计算应该需要尽量靠近存储节点,以避免大量的 RPC 调用。

1、首先,SQL 中的谓词条件 name = "TiDB" 应被下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。

2、然后,聚合函数 Count(*) 也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个 Count(*) 的结果即可

3、再由 SQL 层将各个节点返回的 Count(*) 的结果累加求和。

image-20250715174642419

3、查询流程

SELECT department, AVG(salary) FROM employees WHERE age > 30 GROUP BY department

1) 数据分布假设

  • employees 被分片到 3 个 TiKV 节点(按主键范围):
    • Region1(TiKV-1):[Alice, Bob]
    • Region2(TiKV-2):[Carol, Dave]
    • Region3(TiKV-3):[Eve, Frank]
  • 每个 Region 存储部分数据(包含 department, salary, age列)。

2)分布式执行流程图

image-20250715182722747

3)分步流程

阶段 1:SQL 解析与优化(TiDB Server

  1. 解析 SQL:识别出需下推的操作:
    • 过滤条件:age > 30
    • 聚合函数:AVG(salary)
    • 分组键:department
  2. 生成分布式计划:将过滤和聚合下推到 TiKV 节点,TiDB 仅负责汇总。

阶段 2:分布式计算(TiKV 节点):每个 TiKV 节点 并行执行

  1. 谓词下推(Filter Pushdown
    • 扫描本地数据,过滤出 age > 30的行。
    • 例如 TiKV-1 保留 [Alice(HR,50K), Bob(Eng,60K)]
  2. 聚合下推(Aggregation Pushdown)
    • department分组计算 AVG(salary)
      • TiKV-1HR=50K, Eng=60K
      • TiKV-2Eng=60K
      • TiKV-3HR=55K

阶段 3:结果汇总(TiDB Server

  1. 接收部分结果:从 TiKV 节点收集预聚合结果:

    TiKV-1: [(HR, 50K), (Eng, 60K)]
    TiKV-2: [(Eng, 60K)]
    TiKV-3: [(HR, 55K)]
    
  2. 全局聚合(Reduce 阶段):合并相同分组的结果:

    • HR(50K + 55K) / 2 = 52.5K
    • Eng(60K + 60K) / 2 = 60K
  3. 返回最终结果

+------------+-------------+
| department | AVG(salary) |
+------------+-------------+
| HR         | 52.5K       |
| Eng        | 60K         |
+------------+-------------+

4)问题分析

问题1: 为什么 TiDB Server 需要参与部分计算?

  • TiDB Server 参与计算:负责 SQL 解析、分布式计划生成、跨节点数据合并、复杂运算
    • 分布式协调需求:跨节点数据合并(如 JOINUNION)需中央节点协调。

    • SQL 复杂性:存储层(TiKV/TiFlash)不支持所有 SQL 语法(如递归 CTE)。

    • 事务一致性:全局事务管理(如多表更新)需 TiDB 协调。

  • TiDB Server 不参与计算:已下推的操作(如谓词过滤、单表聚合、索引扫描)由存储层完成。
  • 设计目标计算尽量靠近数据(下推),仅必要时由 TiDB 协调,实现高性能分布式查询。

问题2:为什么 TiDB 不直接在 TiKV 上完成最终汇总计算

  • 存算一体 vs 存算分离
  • 目标场景不同TiDB 优先保证 OLTP 事务性能Doris 优先 OLAP 分析性能

  • 架构复杂度:在 TiKV 上实现全局聚合会引入分布式协调开销,违背其“简单存储”的设计初衷。
    • Doris:采用的是存算一体的架构,这意味着存储和计算资源通常是绑定在一起的。这种设计的一个优点是在处理大规模数据聚合时可以直接在存储节点(Backend 节点)之间进行 Shuffle 操作来完成全局聚合,减少了中央节点的数据汇总压力,并且可以更高效地利用本地数据。

    • TiDB:采用的是存算分离的架构,即计算层(TiDB Server)和存储层(TiKV)是分开的。这样的设计有助于实现更高的灵活性和可扩展性,例如 TiDB Server 可以独立扩缩容而不影响底层存储,同时也简化了系统的设计,避免了存储节点之间的直接通信。

  • 结果汇总位置的不同
    • Doris中,由于其存算一体的特点,结果可以在 Backend 节点间通过网络传输来完成最终的聚合工作,这使得它能够有效地支持复杂查询的执行。
    • 而在TiDB中,由于采用了存算分离的策略,TiDB Server 负责从多个 TiKV 节点收集中间结果并进行最后的合并。这样做不仅简化了架构,也确保了 TiKV 可以专注于高性能的存储和简单的计算任务。
  • 协议兼容性和 SQL 语义的支持
    • TiDB 兼容 MySQL 协议,因此需要支持复杂的 SQL 特性,如事务、触发器等。这些复杂逻辑通常难以完全下推到存储层处理,这也是为什么 TiDB 选择在计算层完成最终的结果汇总。
    • 此外,考虑到 TiKV 作为行存引擎,在处理复杂聚合查询(如多阶段 GROUP BY, JOIN等)时效率较低,这也进一步说明了为何不在 TiKV 上做全局汇总。

问题3:Tidb region 副本不参与查询?

  • 架构设计目标
    • TiDB 是面向 OLTP 场景设计的,强调一致性、事务支持,因此牺牲了部分查询并发能力以换取稳定性。TiDB 查询只访问 Raft Leader,确保读取到的数据是最新的(强一致性)如果是 flower 副本的话,少数派确认可能没有数据
    • DorisTablet 多副本机制中,所有副本都可用于查询,多个副本之间可以分担查询压力Doris 主要面向分析型场景OLAP,更关注查询性能和并发能力,对实时一致性的要求相对较低。
特性 TiDB Doris
存储模型 行存(TiKV 列存(BE)
副本是否参与查询 默认不参与(仅 Leader 所有副本均可参与
是否支持读副本 支持(需手动开启) 天然支持
架构类型 存算分离 存算一体
查询调度位置 TiDB Server 统一汇总 BE 节点本地执行
典型使用场景 OLTP + 实时分析 OLAP 分析

五、TiDB 数据库的调度

PDTiDB 集群的“大脑”,负责全局调度和元数据管理,通过实时监控集群状态,动态调整数据分布和副本策略,解决您提到的所有复杂场景问题。以下是 PD 如何应对各类问题的详细设计:

问题场景 流程
数据分布不均 根据 Region 的空间占用对副本进行合理的分布
跨机房容灾 集群进行跨机房部署的时候,要保证一个机房掉线,不会丢失 Raft Group 的多个副本
节点扩缩容 添加一个节点进入 TiKV 集群之后,需要合理地将集群中其他节点上的数据搬到新增节点
副本管理 短暂掉线要评估、长时间掉线要调度;副本不够要补充、副本过多要删除
Leader 集中 读/写通过 Leade进行,Leader 的分布只集中在少量几个节点会对集群造成影响
热点集中 并不是所有的 Region 都被频繁的访问,可能访问热点只在少数几个 Region,需要通过调度进行负载均衡
资源竞争 集群在做负载均衡的时候,往往需要搬迁数据,这种数据的迁移可能会占用大量的网络带宽、磁盘 IO 以及 CPU,进而影响在线服务。

1、调度的需求

为了满足上面这些需求,需要收集足够的信息,比如每个节点的状态、每个 Raft Group 的信息、业务访问操作的统计等;其次需要设置一些策略,PD 根据这些信息以及调度的策略,制定出尽量满足前面所述需求的调度计划;最后需要一些基本的操作,来完成调度计划。以上问题可归为以下两类:

需求类型 核心问题 实现目标
第一类:容灾功能(必需)    
副本数量管理 副本过少(节点宕机)或过多(节点恢复) 确保每个 Raft Group 始终有 N 个副本
拓扑感知分布 副本集中导致单点故障(如同一机房) 跨机房/机架分布,满足多数派存活
节点异常处理 节点宕机(短暂或永久) 快速恢复数据可用性
第二类:资源优化(扩展性)    
Leader 分布均衡 Leader 集中在少数节点导致负载不均 避免单节点过载
存储容量均衡 部分节点磁盘满,其他节点闲置 提高存储利用率
热点访问均衡 少数 Region 被频繁访问(如热门商品) 分散热点压力
负载均衡速率控制 数据迁移占用带宽/CPU,影响在线服务 平衡迁移速度与业务稳定性
节点状态管理 手动维护节点(上线/下线) 灵活扩缩容

2、调度的基本操作

调度的基本操作指的是为了满足调度的策略。上述调度需求可整理为以下三个操作:

  • 增加一个副本
  • 删除一个副本
  • Leader 角色在一个 Raft Group 的不同副本之间 transfer(迁移)

3、信息收集

调度依赖于整个集群信息的收集,简单来说,调度需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息,TiKV 节点信息和 Region 信息:

1)TiKV 节点汇报

每个 TiKV 节点会定期向 PD 汇报节点的状态信息

  • 总磁盘容量,可用磁盘容量
  • 承载的 Region 数量
  • 数据写入/读取速度
  • 发送/接受的 Snapshot 数量(副本之间可能会通过 Snapshot 同步数据)
  • 是否过载
  • labels 标签信息(标签是具备层级关系的一系列 Tag

a、节点状态流程

image-20250715211602723

b、节点状态含义

(1)Up正常服务

  • 含义TiKV Store 正常运行,提供读写服务。

  • 触发条件

    • 节点启动并成功注册到 PD。
    • 心跳持续正常(默认每 10 秒上报一次)。

(2)Disconnect心跳中断

  • 含义:PDTiKV Store 心跳丢失,可能因网络抖动或节点短暂故障
  • 触发条件:心跳丢失超过 20 秒

  • 恢复路径
    • 心跳恢复:自动回到 Up 状态。
    • 持续中断:超过 max-store-down-time(默认 30 分钟)转为 Down

(3)Down(节点不可用)

  • 含义:节点长时间不可用,PD 判定需补副本。

  • 关键行为

    • PD 在其他健康节点上补足缺失的 Region 副本(通过 Raft Snapshot)。
    • 若节点恢复:需同步最新数据,确认无冲突后回到 Up
    • 若未恢复:补足副本后,原节点标记为 Tombstone

(4)Offline(手动下线中)

  • 含义:管理员主动下线节点,迁移其数据。
  • 触发条件:通过 pd-ctl执行 store delete <store_id>
  • 关键行为
    • PD 逐步将该节点的 Region 迁移到其他 Up节点。
    • 完成条件leader_countregion_count均为 0(通过 pd-ctl store <store_id>查看)。
    • 注意事项
      • Offline 状态下,禁止关闭该 Store 服务以及其所在的物理服务器,否则可能导致数据迁移失败。
      • 如果集群里不存在满足搬迁条件的其它目标 Store(例没有足够的 Store 能够继续满足集群的副本数量要求),该 Store 将一直处于 Offline 状态

(5)Tombstone(完全下线)

  • 含义:节点数据已全部迁移,可安全移除。
  • 清理方式
    • 手动清理:执行 pd-ctl remove-tombstone
    • 自动清理Tombstone 状态满 1 个月后 PD 自动删除记录。

2)Raft GroupLeader 汇报

每个 Raft GroupLeaderPD 之间存在心跳包,用于汇报这个 [Region 的状态],主要包括下面几点信息:

  • Leader 的位置
  • Followers 的位置
  • 掉线副本的个数
  • 数据写入/读取的速度

4、调度的策略

1)一个 Region 的副本数量正确

PD 通过某个 Region Leader 的心跳包发现这个 Region 的副本数量不满足要求时,需要通过 Add/Remove Replica 操作调整副本数量。出现这种情况的可能原因是:

  • 某个节点掉线,上面的数据全部丢失,导致一些 Region 的副本数量不足
  • 某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了副本的 Region 的副本数量过多,需要删除某个副本
  • 管理员调整副本策略,修改了 [max-replicas]的配置

2)一个 Raft Group 中的多个副本不在同一个位置

注意这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个副本不落在一个节点上,以避免单个节点失效导致多个副本丢失。在实际部署中,还可能出现下面这些需求:

  • 多个节点部署在同一台物理机器上
  • TiKV 节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性
  • TiKV 节点分布在多个 IDC 中,希望单个机房掉电时,也能保证系统可用性

这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的『容错单元』,希望这个单元内部不会存在一个 Region 的多个副本。

3)副本在 Store 之间的分布均匀分配

由于每个 Region 的副本中存储的数据容量上限是固定的,通过维持每个节点上面副本数量的均衡,使得各节点间承载的数据更均衡。

4)Leader 数量在 Store 之间均匀分配

Raft 协议要求读取和写入都通过 Leader 进行,所以计算的负载主要在Leader 上面,PD 会尽可能将 Leader在节点间分散开。

5)访问热点数量在 Store 之间均匀分配

每个 Store 以及 Region Leader 在上报信息时携带了当前访问负载的信息,比如 Key 的读取/写入速度。PD 会检测出访问热点,且将其在节点之间分散开。

6)各个 Store 的存储空间占用大致相等

每个 Store 启动的时候都会指定一个 Capacity 参数,表明这个 Store 的存储空间上限,PD 在做调度的时候,会考虑节点的存储空间剩余量。

7)控制调度速度,避免影响在线服务

调度操作需要耗费 CPU、内存、磁盘 IO 以及网络带宽,需要避免对线上服务造成太大影响。PD 会对当前正在进行的操作数量进行控制,默认的速度控制是比较保守的,如果希望加快调度(比如停服务升级或者增加新节点,希望尽快调度),那么可以通过调节 PD 参数动态加快调度速度。

8)调度的实现

信息收集:PD 不断地通过 Store 或者 Leader 的心跳包收集整个集群信息,并且根据这些信息以及调度策略生成调度操作序列。

信息反馈:每次收到 Region Leader 发来的心跳包时,PD 都会检查这个 Region 是否有待进行的操作,然后通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。

调度执行:注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 根据当前自身状态来定。

a、整体流程

1. Region Leader (Store 1) 发送心跳给 PD
   └─ 携带信息:Region 负载高、Store 1 CPU 使用率80%

2. PD 分析后决定:
   └─ 建议将 Leader 转移到 Store 3(负载较低)

3. PD 在心跳回复中携带:
   └─ 调度建议:TransferLeader(Region 123, From Store1, To Store3)

4. Region Leader 收到后:
   └─ 检查 Store 3 是否可用
   └─ 若无冲突,开始 Leader 转移流程
   └─ 若正处理重要事务,可能延迟执行

5. 下次心跳时:
   └─ 报告调度执行结果(成功/失败/延迟)
   └─ PD 根据结果决定是否重新调度

b、普通 Follower 副本迁移流程

  1. 添加 LearnerPD 在目标 TiKV 节点上为 Raft Group 添加一个 Learner 副本
  2. 数据同步LearnerLeader 异步复制数据(不参与写确认)
  3. 提升为 Follower:当 Learner 数据接近最新时,Leader 将其提升为正式 Follower
  4. 移除旧副本:从原 TiKV 节点移除旧的 Follower 副本

c、Leader 副本迁移流程

  1. 完成上述 1-3 步:先在目标节点建立 Learner 并提升为 Follower
  2. Leader 转移PD 发起 Leader Transfer 命令:
    • Leader 主动让位
    • Follower 发起选举成为新 Leader
  3. 清理旧 Leader:新 Leader 将原 Leader 副本降级并移除(保证正确的副本数量)

六、TSO

TSO (TimeStamp Oracle) 是 TiDB 分布式数据库实现全局一致性的核心组件

它为整个分布式系统提供严格递增、唯一的时间戳服务。

1、TSO 的核心作用

  1. 全局事务排序:为所有分布式事务分配唯一时间戳
  2. MVCC 版本控制:支持多版本并发控制机制
  3. 一致性保证:确保分布式事务的线性一致性

2、TSO 的架构设计

1)物理组成

  • PD (Placement Driver) 内置TSO 服务内嵌在 PD 组件中
  • 高可用部署:通常部署 3-5PD 节点组成集群
  • Leader 提供服务:只有 PD Leader 节点提供 TSO 服务

2)逻辑结构

每个 TSO 由两部分组成: 物理时间戳(高位) 逻辑计数器(低位)
  • 物理部分:毫秒级精度系统时间
  • 逻辑部分:每毫秒内的递增序列号
    • 用于需要在一毫秒内使用多个时间戳的情况,或某些事件可能触发时钟进程逆转的情况。在这些情况下,物理时间戳会保持不变,而逻辑时间戳保持递增。该机制可以确保 TSO 时间戳的完整性,确保时间戳始终递增而不会倒退。

3、获取 TiDB 当前的 TSO

TSO 时间戳是按事务分配的,所以需要从包含 BEGIN; ...; ROLLBACK 的事务中获取时间戳。TSO 时间戳是一个十进制数

BEGIN; SET @ts := @@tidb_current_ts; ROLLBACK;
Query OK, 0 rows affected (0.0007 sec)
Query OK, 0 rows affected (0.0002 sec)
Query OK, 0 rows affected (0.0001 sec)

SELECT @ts;
+--------------------+
| @ts                |
+--------------------+
| 443852055297916932 |
+--------------------+
1 row in set (0.00 sec)

TSO 时间戳的二进制细节:

0000011000101000111000010001011110111000110111000000000000000100  ← 该值是二进制形式的 443852055297916932
0000011000101000111000010001011110111000110111                    ← 前 46 位是物理时间戳
                                              000000000000000100  ← 后 18 位是逻辑时间戳

七、TiDB 数据倾斜问题解决方案

1、数据倾斜的本质与影响

本质:数据在分布式集群中分布不均,导致部分 TiKV 节点负载过高(如存储量、读写请求数远超其他节点)。 影响

  • 热点节点成为性能瓶颈,导致整体集群吞吐量下降;
  • Raft 日志同步延迟增加,影响事务一致性;
  • 部分节点资源(CPU/IO/ 内存)利用率过高,可能触发服务降级。

2、数据倾斜的常见原因

TiDB 解决数据倾斜需从 设计(表结构)、调度(PD 策略)、业务(流量打散) 三层入手:

  • 预防优先:设计阶段避免自增主键、合理选择分片键;
  • 动态调度:通过 PD 自动迁移热点 Region,配合参数优化提升调度效率;
  • 业务适配:对高频操作添加缓存或分片查询,减轻集群压力。

通过上述方案,可有效将数据倾斜率(单节点负载 / 平均负载)控制在 1.5 倍以内,保障集群性能稳定性。

1)表结构设计问题

  • 主键或分区键选择不当(如使用自增 ID、时间字段未分片),导致数据集中在少数 Region
  • 未创建合适的二级索引,查询或写入操作集中在特定数据范围。

2)集群调度策略不足

  • PDPlacement Driver)调度参数未优化,Region 迁移不及时或策略不合理。

3)业务访问模式倾斜

  • 高频操作(如更新、查询)集中在某段时间或某个值域(如订单表按日期查询近期数据);
  • 缓存失效或业务突发流量导致瞬时访问集中。

3、TiDB 数据倾斜解决方案

1) 表结构与索引优化

a、主键设计:避免自增 ID 与热点键

  • 反模式:使用自增主键(如 AUTO_INCREMENT),导致新数据始终写入最后一个 Region

  • 优化方案

    • 哈希分片主键:将主键与哈希值结合(如 id + MOD(id, 100)),打散数据分布;

    • 复合主键:使用业务字段 + 随机数组合

      CREATE TABLE t (
        user_id BIGINT,
        rand_num INT,
        -- 其他字段
        PRIMARY KEY (user_id, rand_num)
      );
      
    • UUID 主键:使用 UUID()UUID_TO_BIN() 生成无序主键(需注意 UUID 索引查询效率)。

b、分区表与分片规则

  • 范围分区(适用于时间序列数据)按时间字段分区(如年 / 月),并结合哈希分片避免单分区过热:
REATE TABLE t (
  id BIGINT,
  create_time DATETIME,
  -- 其他字段
  PRIMARY KEY (id, create_time)
) 
PARTITION BY RANGE COLUMNS(create_time) (
  PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
  PARTITION p2025 VALUES LESS THAN ('2026-01-01'),
  -- 更多分区
);
  • 哈希分区(适用于均匀分布场景):对高频访问字段添加哈希分片(如用户 ID):
CREATE TABLE t (
  user_id BIGINT,	
  -- 其他字段
  PRIMARY KEY (MOD(user_id, 100), user_id)
);

c、二级索引优化

  • 主键打散的是“主表数据”的物理分布,但二级索引是独立存储的结构,它的分布由“索引键”决定!

  • 对高频查询 / 更新字段创建索引时,避免索引键倾斜:

    • 反例:对 status 字段(值分布不均,如 90%active)创建索引,导致查询集中在少数 Region
    • 优化:添加随机后缀或组合字段(如 (status, RAND())),打散索引访问。

2)集群调度与参数优化

a、PD 调度参数调整

  • 通过 pd-ctl 命令优化 Region 迁移策略

开启负载均衡

pd-ctl config set enable-rebalance-hot-region true
pd-ctl config set hot-region-schedule-limit 4  # 热点 Region 调度并发数

调整 Region 大小阈值

pd-ctl config set max-region-size 1048576  # 单位 KB,默认 96MB,可减小至 64MB 提升分片密度
pd-ctl config set max-region-keys 200000    # 单个 Region 最大键数,默认 20 万,可根据数据大小调整

热点 Region 强制迁移

pd-ctl hot-region schedule table table_id  # 对指定表的热点 Region 触发调度

b、TiKV 存储层优化

  • 调整 TiKV 存储参数,提升热点 Region 处理能力:
[rocksdb]
max-sub-compactions = 4  # 并发压缩任务数,提升写入性能
max-background-jobs = 20 # 后台任务数,默认 16,可增加至 20

3)业务层面优化

a、读写请求打散

  • 查询场景:对时间范围查询添加随机偏移(如查询近 7 天数据时,按用户 ID 分片查询);
  • 写入场景:对高频写入表添加中间缓存(如 Redis),批量写入 TiDB(减少瞬时流量冲击)。

b、冷热数据分离

  • 将高频访问的热数据与历史冷数据分表存储:
    • 热数据表:采用高效分片策略,部署在高性能节点;
    • 冷数据表:降低资源配置,或归档至 TiDB 归档集群(如使用 TiDB-Dump + 离线导入)。

5)主键、分区与二级索引

三者协同的本质是:通过主键打散物理存储,分区管理逻辑分组,索引优化访问路径,从 “存储分布→数据管理→查询效率” 形成闭环。

  • 主键:决定数据在分布式集群中的基础分布(全局打散);
  • 分区:在主键分片基础上,进一步按业务维度(如时间、地域)分组,降低单分区数据规模;
  • 二级索引:优化查询路径,避免因索引键倾斜导致的访问集中。

问题1:region 和分区关系?

可以把分区表看作 “文件夹”,**Region **看作 “文件夹里的文件块”:

  1. 一个数据库表被分成多个 “文件夹”(分区 p2024、p2025),每个文件夹存放特定时间的数据。
  2. 每个文件夹里的文件(数据)被进一步拆分成多个 “文件块”(Region),每个文件块分散存储在不同硬盘(TiKV 节点)上。
  3. 主键打散相当于给每个文件块编号(MOD 运算),让文件块均匀分布在硬盘上,避免单个硬盘过载。
  4. 二级索引相当于每个文件夹的 “目录”,查询时直接查对应文件夹的目录,快速定位文件块。

a、主键

主键:分布式存储的 “基石”—— 决定数据打散粒度

案例:电商订单表

  • 反模式:使用自增主键 order_id,新订单全部写入最后一个 Region,导致写入倾斜;

  • 优化设计

    -- 主键 = 业务主键 + 哈希分片键
    PRIMARY KEY (order_id, MOD(order_id, 100))  
    
  • 协同逻辑

    • 哈希分片键(MOD(order_id, 100))将数据分散到 100 个逻辑桶中,每个桶对应独立 Region,避免写入集中;
    • 分区与索引基于此主键分布进一步优化(如按桶分区、对分片键创建索引)。

b、分区

分区:在主键分片上 “逻辑分组”—— 缩小倾斜影响范围

场景:按时间维度分区

  • 表结构设计

    CREATE TABLE orders (
      order_id BIGINT,
      create_time DATETIME,
      -- 其他字段
      PRIMARY KEY (order_id, MOD(order_id, 100)),  -- 主键打散
      KEY idx_create_time (create_time)  -- 二级索引
    )
    PARTITION BY RANGE COLUMNS(create_time) (  -- 按时间分区
      PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
      PARTITION p2025 VALUES LESS THAN ('2026-01-01')
    );
    
  • 协同逻辑

    • 主键打散:每个时间分区内的数据通过 MOD(order_id, 100) 分散到 100Region,避免单分区内写入倾斜;
    • 分区隔离:历史分区(如 p2024)与当前分区(p2025)物理隔离,热点仅影响当前分区,不波及全量数据;
    • 索引优化:二级索引 idx_create_time 在每个分区内独立维护,查询时按分区过滤,减少跨分区扫描。

c、二级索引

二级索引:优化访问路径 —— 避免索引键导致的查询倾斜

场景:用户表按 status 字段高频查询

  • 问题:若 status 取值分布不均(如 90%active),索引查询会集中访问少数 Region

  • 协同优化方案

    CREATE TABLE users (
      user_id BIGINT,
      status VARCHAR(20),
      rand_suffix INT,
      -- 主键与分区
      PRIMARY KEY (user_id, MOD(user_id, 50)),  -- 主键打散
      -- 带随机后缀的二级索引 (user_id 避免回表)
      KEY idx_status (status, rand_suffix, user_id)  
    )
    PARTITION BY HASH(user_id) PARTITIONS 10;  -- 按用户 ID 哈希分区
    
  • 协同逻辑

    • 主键与分区user_id 哈希分片确保用户数据均匀分布在 50Region10 个分区中;
    • 索引优化status 索引添加随机后缀 rand_suffix,将同类 status 的查询分散到不同 Region(如 status='active' 的查询会访问 rand_suffix 0-99 的多个 Region);
    • 查询改写:业务查询时添加随机后缀条件(如 WHERE status='active' AND rand_suffix = FLOOR(RAND()*100)),进一步打散流量。

ContactAuthor