TiDB内核解密:揭秘其底层KV存储引擎如何玩转键值对

一、简介        

        TiDB是开源的分布式数据库,是一款同时支持在线事务处理与在线分析处理的融合型分布式数据库产品,具备水平扩容或缩容、金融级高可用、实时HTAP、云原生分布式数据库、兼容MySQL5.7 协议和 MySQL 生态等重要特性。目标是为用户提供一站式 OLTP、OLAP、HTAP 解决方案。

        TiDB有诸多特性,核心特性如下:

  • 一键水平扩容或缩容:得益于 TiDB 存储计算分离的架构设计,可按需对计算、存储分别进行在线扩容或缩容。扩容或者缩容过程中对运维人员透明。
  • 金融级高可用:数据采用多副本存储,数据副本通过 Multi-Raft 协议同步事务日志,多数写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本的地理位置、副本数量等策略满足不同容灾级别。
  • 实时HTAP:提供行存储引擎 TiKV、列存储引擎 TiFlash 两种存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎 TiFlash 之间数据强一致性。TiKV、TiFlash 可按需部署在不同机器,解决HTAP资源隔离问题。
  • 云原生分布式数据库:专为云设计的分布式数据库,通过 TiDB Operator 可在公有云、私有云、混合云中实现部署工具化、自动化。
  • 兼容MySQL 5.7协议和MySQL生态:兼容 MySQL 5.7 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到 TiDB

二、TiDB整体架构

        在内核设计上,TiDB分布式数据库将整体架构拆成了多个模块,各模块之间相互通信,组成完整的TiDB系统。

        下面先介绍下 TiDB 各个组件的功能。

        TiDB Server

        SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。

        TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果,TiDB server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层存储节点 TiKV 或TiFlash。

        PD(Placement Driver) Server

        整合TiDB集群原信息管理模块,负责存储每个 TiKV 节点实时数据分布情况和集群的整体拓扑结构,提供 TiDB Dashboard 管控界面,并为分布式事务提供事务 ID。

        PD 不仅存储原信息,同时还会根据 TiKV 节点实时上报的数据分布状态,下发数据调度命令给具体的 TiKV 节点,可以说是整个集群的大脑。

        此外,PD 本身也是由至少3个节点构成,拥有高可用能力。

        存储节点

        TiKV 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 中的数据都会自动维护多副本(默认为三副本),天然支持高可用和自动故障转移。

        TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。

三、Key-Value Pairs(键值对)

        作为保存数据的系统,首先要确定的是数据的存储模型,也就是数据以什么样的形式保存下来。TiKV 选择的是 Key-Value 模型,并且提供有序遍历方法。

        TiKV数据存储的两个关键点。

  1. 这是一个巨大的 Map,也就是存储的 Key-Value Pairs(键值对)。
  2. 这个 Map 中的 Key-Value pair 按照 Key 的二进制顺序排序,也就是可以 Seek 到某个 Key的位置,然后不断地调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。

        TiKV 可以看做是一个巨大的有序的 KV Map,那么为了实现存储的水平扩展,数据将被分散在多台机器上。对于一个 KV 系统,将数据分散在多台机器上有两种比较典型的方案:

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

        TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一些列连续的 Key,将每一段叫做一个 Region,可以用 [StartKey, EndKey) 这样一个左闭右开区间来描述。每个 Region 中保存的数据量默认维持在 96MiB (可通过配置修改)。TiDB内核解密:揭秘其底层KV存储引擎如何玩转键值对_第1张图片

        注意,这里的 Region 还是和 SQL 中表没什么关系。这里的讨论依然不涉及 SQL,这和 KV有关。

        将数据划分成 Region 后,TiKV 将会做两件重要的事情:

  • 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的 Region 数量差不多。
  • 以 Region 为单位做 Raft 的复制和成员管理。

        表数据与Key-Value的映射数据

        在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个(Key, Value)键值对,需要考虑如何构造Key。

        首先,OLTP场景下有大量针对单行或者多行的增删改查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一的 ID(显示或隐示的ID),以方便快速定位。

        其次,很多OLAP型查询需要进行全表扫描。如果能够将一个表中所有行的Key编码到一个区间内,就可以通过范围查询高效完成全表扫描任务。

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

  • 为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用 TableID表示,表 ID 是一个整数在整个集群内唯一。
  • TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TIDB 会使用主键值当做这一行数据的行 ID。 
  • 每行数据按照如下规则编码成(Key, Value)键值对:

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

        其中 tablePrefix 和 recordPrefixSep 都是特定的字符串常量,用于在 Key 空间内区分其他数据。

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

        TiDB 同时支持主键和二级索引(包括唯一索引和非唯一索引)。与表数据映射方案类似,TiDB 为表中每个索引分配了一个索引 ID,用 IndexID 表示。TiDB 创建二级索引为在线操作,不会阻塞表中的数据读写。

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

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

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

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

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

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

        另外请注意,上述方案中,无论是表数据还是索引数据的 Key 编码方案,一个表内所有的行都有相同的 Key 前缀,一个索引的所有数据也都有相同的前缀。这样具有相同的前缀的数据,在 TiKV 的 Key 空间内,是排列在一起的。因此只要小心地设计后缀部分的编码方案,保证编码前和编码后的比较关系不变,就可以将表数据或者索引数据有序地保存在 TiKV 中。采用这种编码后,一个表的所有行数据会按照 RowID 顺序地排列在 TiKV 的 Key 空间中,某一个索引的数据也会按照索引数据的具体的值(编码方案中的 indexedColumnsValue)顺序地排列在 Key 空间内。

        Key-Value 映射关系示例

        通过一个简单的例子,来理解 TiDB 的 Key-Value 映射关系。假设 TiDB 中有如下这个表:

CREATE TABLE User (
    ID int,
    Name varchar(20),
    Role varchar(20),
    Age int,
    PRIMARY KEY (ID),
    KEY idxAge (Age)
);

        假设该表中有 3 行数据:

1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30

        首先每行数据都会映射为一个 (Key, Value) 键值对,同时该表有一个 int 类型的主键,所以 RowID 的值即为该主键的值。假设该表的 TableID 为 10,则其存储在 TiKV 上的表数据为:

t10_r1 --> ["TiDB", "SQL Layer", 10]
t10_r2 --> ["TiKV", "KV Engine", 20]
t10_r3 --> ["PD", "Manager", 30]

        除了主键外,该表还有一个非唯一的普通二级索引 idxAge,假设这个索引的 IndexID 为 1,则其存储在 TiKV 上的索引数据为:

t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null

        TiDB Server,负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层 TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。

        对于数据修改(INSERT, UPDATE, DELETE)操作,TiDB 需要根据事务上下文生成对应的键值对,并且每个键都包含了表名、主键信息以及可能的列值或版本号等信息。

        对于 SELECT 查询,TiDB 根据查询条件生成对相应 KV 范围的扫描请求。如果使用了索引,那么生成的键将会是索引键;否则可能是基于聚簇索引的范围扫描。

        总之,在 TiDB 中,SQL 查询并不直接操作数据库文件,而是通过 SQL 层将高级查询语句分解为一系列底层的 Key-Value 操作,并借助 TiKV 分布式存储引擎实现最终的数据存取。这一过程确保了 SQL 语义在分布式环境中的正确性和高效性。

你可能感兴趣的:(tidb,TiDB,KV操作细节,分布式数据库)