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。提供丰富的数据迁移工具帮助应用便捷完成数据迁移。
二、四大核心应用场景
- 对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高的金融行业属性的场景
众所周知,金融行业对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高。传统的解决方案是同城两个机房提供服务、异地一个机房提供数据容灾能力但不提供服务,此解决方案存在以下缺点:资源利用率低、维护成本高、RTO (Recovery Time Objective) 及 RPO (Recovery Point Objective) 无法真实达到企业所期望的值。TiDB 采用多副本 + Multi-Raft 协议的方式将数据调度到不同的机房、机架、机器,当部分机器出现故障时系统可自动进行切换,确保系统的 RTO <= 30s 及 RPO = 0。
- 对存储容量、可扩展性、并发要求较高的海量数据及高并发的 OLTP 场景
随着业务的高速发展,数据呈现爆炸性的增长,传统的单机数据库无法满足因数据爆炸性的增长对数据库的容量要求,可行方案是采用分库分表的中间件产品或者 NewSQL 数据库替代、采用高端的存储设备等,其中性价比最大的是 NewSQL 数据库,例如:TiDB。TiDB 采用计算、存储分离的架构,可对计算、存储分别进行扩容和缩容,计算最大支持 512 节点,每个节点最大支持 1000 并发,集群容量最大支持 PB 级别。
- Real-time HTAP 场景
随着 5G、物联网、人工智能的高速发展,企业所生产的数据会越来越多,其规模可能达到数百 TB 甚至 PB 级别,传统的解决方案是通过 OLTP 型数据库处理在线联机交易业务,通过 ETL 工具将数据同步到 OLAP 型数据库进行数据分析,这种处理方案存在存储成本高、实时性差等多方面的问题。TiDB 在 4.0 版本中引入列存储引擎 TiFlash 结合行存储引擎 TiKV 构建真正的 HTAP 数据库,在增加少量存储成本的情况下,可以在同一个系统中做联机交易处理、实时数据分析,极大地节省企业的成本。
- 数据汇聚、二次加工处理的场景
当前绝大部分企业的业务数据都分散在不同的系统中,没有一个统一的汇总,随着业务的发展,企业的决策层需要了解整个公司的业务状况以便及时做出决策,故需要将分散在各个系统的数据汇聚在同一个系统并进行二次加工处理生成 T+0 或 T+1 的报表。传统常见的解决方案是采用 ETL + Hadoop 来完成,但 Hadoop 体系太复杂,运维、存储成本太高无法满足用户的需求。与 Hadoop 相比,TiDB 就简单得多,业务通过 ETL 工具或者 TiDB 的同步工具将数据同步到 TiDB,在 TiDB 中可通过 SQL 直接生成报表。
三、TiDB 整体架构
1、TiDB优势
与传统的单机数据库相比,TiDB 具有以下优势:
- 纯分布式架构,拥有良好的扩展性,支持弹性的扩缩容。
- 支持 SQL,对外暴露 MySQL 的网络协议,并兼容大多数 MySQL 的语法,在大多数场景下可以直接替换 MySQL。
- 默认支持高可用,在少数副本失效的情况下,数据库本身能够自动进行数据修复和故障转移,对业务透明。
- 支持 ACID 事务,对于一些有强一致需求的场景友好,例如:银行转账。
- 具有丰富的工具链生态,覆盖数据迁移、同步、备份等多种场景。
2、TiDB模块组成
在内核设计上,TiDB 分布式数据库将整体架构拆分成了多个模块,各模块之间互相通信,组成完整的 TiDB 系统。
- TiDB Server:SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。TiDB Server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储节点 TiKV(或 TiFlash)。
- PD (Placement Driver) Server:整个 TiDB 集群的元信息管理模块,负责存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构,提供 TiDB Dashboard 管控界面,并为分布式事务分配事务 ID。PD 不仅存储元信息,同时还会根据 TiKV 节点实时上报的数据分布状态,下发数据调度命令给具体的 TiKV 节点,可以说是整个集群的“大脑”。此外,PD 本身也是由至少 3 个节点构成,拥有高可用的能力。建议部署奇数个 PD 节点。
-
存储节点
- 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:TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。
3、TiDB数据库的存储
TiKV设计思路及关键概念
3.1、Key-Value Pairs(键值对)
作为保存数据的系统,首先要决定的是数据的存储模型,也就是数据以什么样的形式保存下来。TiKV 的选择是 Key-Value 模型,并且提供有序遍历方法。
TiKV 数据存储的两个关键点:
- 这是一个巨大的 Map(可以类比一下 C++ 的 std::map),也就是存储的是 Key-Value Pairs(键值对)
- 这个 Map 中的 Key-Value pair 按照 Key 的二进制顺序有序,也就是可以 Seek 到某一个 Key 的位置,然后不断地调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。
注意,本文所说的 TiKV 的 KV 存储模型和 SQL 中的 Table 无关。本文不讨论 SQL 中的任何概念,专注于讨论如何实现 TiKV 这样一个高性能、高可靠性、分布式的 Key-Value 存储
3.2、本地存储 (RocksDB)
任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 也不例外。但是 TiKV 没有选择直接向磁盘上写数据,而是把数据保存在 RocksDB 中,具体的数据落地由 RocksDB 负责。这个选择的原因是开发一个单机存储引擎工作量很大,特别是要做一个高性能的单机引擎,需要做各种细致的优化,而 RocksDB 是由 Facebook 开源的一个非常优秀的单机 KV 存储引擎,可以满足 TiKV 对单机引擎的各种要求。这里可以简单的认为 RocksDB 是一个单机的持久化 Key-Value Map。
3.3、Raft 协议
接下来 TiKV 的实现面临一件更难的事情:如何保证单机失效的情况下,数据不丢失,不出错?
简单来说,需要想办法把数据复制到多台机器上,这样一台机器无法服务了,其他的机器上的副本还能提供服务;复杂来说,还需要这个数据复制方案是可靠和高效的,并且能处理副本失效的情况。TiKV 选择了 Raft 算法。Raft 是一个一致性协议,本文只会对 Raft 做一个简要的介绍,细节问题可以参考它的论文。Raft 提供几个重要的功能:
- Leader(主副本)选举
- 成员变更(如添加副本、删除副本、转移 Leader 等操作)
- 日志复制
TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft 的日志复制功能,将数据安全可靠地同步到复制组的每一个节点中。不过在实际写入中,根据 Raft 的协议,只需要同步复制到多数节点,即可安全地认为数据写入成功。
总结一下,通过单机的 RocksDB,TiKV 可以将数据快速地存储在磁盘上;通过 Raft,将数据复制到多台机器上,以防单机失效。数据的写入是通过 Raft 这一层的接口写入,而不是直接写 RocksDB。通过实现 Raft,TiKV 变成了一个分布式的 Key-Value 存储,少数几台机器宕机也能通过原生的 Raft 协议自动把副本补全,可以做到对业务无感知。
3.4、Region
首先,为了便于理解,在此节,假设所有的数据都只有一个副本。前面提到,TiKV 可以看做是一个巨大的有序的 KV Map,那么为了实现存储的水平扩展,数据将被分散在多台机器上。对于一个 KV 系统,将数据分散在多台机器上有两种比较典型的方案:
- Hash:按照 Key 做 Hash,根据 Hash 值选择对应的存储节点。
- Range:按照 Key 分 Range,某一段连续的 Key 都保存在一个存储节点上。
TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,并且会尽量保持每个 Region 中保存的数据不超过一定的大小,目前在 TiKV 中默认是 96MB。每一个 Region 都可以用 [StartKey,EndKey) 这样一个左闭右开区间来描述。
注意,这里的 Region 还是和 SQL 中的表没什么关系。 这里的讨论依然不涉及 SQL,只和 KV 有关。
将数据划分成 Region 后,TiKV 将会做两件重要的事情:
- 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的 Region 数量差不多。
- 以 Region 为单位做 Raft 的复制和成员管理。
这两点非常重要:
- 先看第一点,数据按照 Key 切分成很多 Region,每个 Region 的数据只会保存在一个节点上面(暂不考虑多副本)。TiDB 系统会有一个组件 (PD) 来负责将 Region 尽可能均匀的散布在集群中所有的节点上,这样一方面实现了存储容量的水平扩展(增加新的节点后,会自动将其他节点上的 Region 调度过来),另一方面也实现了负载均衡(不会出现某个节点有很多数据,其他节点上没什么数据的情况)。同时为了保证上层客户端能够访问所需要的数据,系统中也会有一个组件 (PD) 记录 Region 在节点上面的分布情况,也就是通过任意一个 Key 就能查询到这个 Key 在哪个 Region 中,以及这个 Region 目前在哪个节点上(即 Key 的位置路由信息)。至于负责这两项重要工作的组件 (PD),会在后续介绍。
- 对于第二点,TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本,TiKV 将每一个副本叫做一个 Replica。Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft Group。其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。默认情况下,所有的读和写都是通过 Leader 进行,读操作在 Leader 上即可完成,而写操作再由 Leader 复制给 Follower。
大家理解了 Region 之后,应该可以理解下面这张图:
以 Region 为单位做数据的分散和复制,TiKV 就成为了一个分布式的具备一定容灾能力的 KeyValue 系统,不用再担心数据存不下,或者是磁盘故障丢失数据的问题。
3.5、MVCC
很多数据库都会实现多版本并发控制 (MVCC),TiKV 也不例外。设想这样的场景:两个客户端同时去修改一个 Key 的 Value,如果没有数据的多版本控制,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。TiKV 的 MVCC 实现是通过在 Key 后面添加版本号来实现,简单来说,没有 MVCC 之前,可以把 TiKV 看做这样的:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
有了 MVCC 之后,TiKV 的 Key 排列是这样的:
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 的时候,可以通过 Key 和 Version 构造出 MVCC 的 Key,也就是 Key_Version。然后可以直接通过 RocksDB 的 SeekPrefix(Key_Version) API,定位到第一个大于等于这个 Key_Version 的位置。
3.6、分布式 ACID 事务
TiKV 的事务采用的是 Google 在 BigTable 中使用的事务模型:Percolator ,TiKV 根据这篇论文实现,并做了大量的优化。详细介绍参见事务概览。
4、TiDB 数据库的计算
TiDB 在 TiKV 提供的分布式存储能力基础上,构建了兼具优异的交易处理能力与良好的数据分析能力的计算引擎。本文首先从数据映射算法入手介绍 TiDB 如何将库表中的数据映射到 TiKV 中的 (Key, Value) 键值对,然后描述 TiDB 元信息管理方式,最后介绍 TiDB SQL 层的主要架构。
对于计算层依赖的存储方案,本文只介绍基于 TiKV 的行存储结构。针对分析型业务的特点,TiDB 推出了作为 TiKV 扩展的列存储方案 TiFlash。
4.1、表数据与 Key-Value 的映射关系
本小节介绍 TiDB 中数据到 (Key, Value) 键值对的映射方案。这里的数据主要包括以下两个方面:
- 表中每一行的数据,以下简称表数据
- 表中所有索引的数据,以下简称索引数据
4.1.1、表数据与 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 空间内区分其他数据。其具体值在后面的小结中给出。4.1.2、索引数据和 Key-Value 的映射关系
TiDB 同时支持主键和二级索引(包括唯一索引和非唯一索引)。与表数据映射方案类似,TiDB 为表中每个索引分配了一个索引 ID,用
IndexID 表示。对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID
对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null
4.1.3、映射关系小结
上述所有编码规则中的
tablePrefix、recordPrefixSep 和 indexPrefixSep 都是字符串常量,用于在 Key 空间内区分其他数据,定义如下:tablePrefix = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep = []byte{'i'}
另外请注意,上述方案中,无论是表数据还是索引数据的 Key 编码方案,一个表内所有的行都有相同的 Key 前缀,一个索引的所有数据也都有相同的前缀。这样具有相同的前缀的数据,在 TiKV 的 Key 空间内,是排列在一起的。因此只要小心地设计后缀部分的编码方案,保证编码前和编码后的比较关系不变,就可以将表数据或者索引数据有序地保存在 TiKV 中。采用这种编码后,一个表的所有行数据会按照
RowID 顺序地排列在 TiKV 的 Key 空间中,某一个索引的数据也会按照索引数据的具体的值(编码方案中的 indexedColumnsValue)顺序地排列在 Key 空间内。4.1.4、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", 102, "TiKV", "KV Engine", 203, "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 中关系模型到 Key-Value 模型的映射规则,以及选择该方案背后的考量。
4.2、元信息管理
TiDB 中每个
Database 和 Table 都有元信息,也就是其定义以及各项属性。这些信息也需要持久化,TiDB 将这些信息也存储在了 TiKV 中。每个
Database/Table 都被分配了一个唯一的 ID,这个 ID 作为唯一标识,并且在编码为 Key-Value 时,这个 ID 都会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 Key,Value 中存储的是序列化后的元信息。除此之外,TiDB 还用一个专门的 (Key, Value) 键值对存储当前所有表结构信息的最新版本号。这个键值对是全局的,每次 DDL 操作的状态改变时其版本号都会加 1。目前,TiDB 把这个键值对持久化存储在 PD Server 中,其 Key 是 "/tidb/ddl/global_schema_version",Value 是类型为 int64 的版本号值。TiDB 采用 Online Schema 变更算法,有一个后台线程在不断地检查 PD Server 中存储的表结构信息的版本号是否发生变化,并且保证在一定时间内一定能够获取版本的变化。
4.3、SQL 层简介
TiDB 的 SQL 层,即 TiDB Server,负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层 TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。
这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。
4.3.1、SQL 运算
最简单的方案就是通过上一节所述的表数据与 Key-Value 的映射关系方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。
比如
select count(*) from user where name = "TiDB" 这样一个 SQL 语句,它需要读取表中所有的数据,然后检查 name 字段是否是 TiDB,如果是的话,则返回这一行。具体流程如下:- 构造出 Key Range:一个表中所有的
RowID都在[0, MaxInt64)这个范围内,使用0和MaxInt64根据行数据的Key编码规则,就能构造出一个[StartKey, EndKey)的左闭右开区间。
- 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据。
- 过滤数据:对于读到的每一行数据,计算
name = "TiDB"这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据。
- 计算
Count(*):对符合要求的每一行,累计到Count(*)的结果上面。
整个流程示意图如下:
这个方案是直观且可行的,但是在分布式数据库的场景下有一些显而易见的问题:
- 在扫描数据的时候,每一行都要通过 KV 操作从 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大。
- 并不是所有的行都满足过滤条件
name = "TiDB",如果不满足条件,其实可以不读取出来。
- 此查询只要求返回符合要求行的数量,不要求返回这些行的值。
4.3.2、分布式 SQL 运算
为了解决上述问题,计算应该需要尽量靠近存储节点,以避免大量的 RPC 调用。首先,SQL 中的谓词条件
name = "TiDB" 应被下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。然后,聚合函数 Count(*) 也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个 Count(*) 的结果即可,再由 SQL 层将各个节点返回的 Count(*) 的结果累加求和。以下是数据逐层返回的示意图:
4.3.3、SQL 层架构
通过上面的例子,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL 层要复杂得多,模块以及层次非常多,下图列出了重要的模块以及调用关系:
用户的 SQL 请求会直接或者通过
Load Balancer 发送到 TiDB Server,TiDB Server 会解析 MySQL Protocol Packet,获取请求内容,对 SQL 进行语法解析和语义分析,制定和优化查询计划,执行查询计划并获取和处理数据。数据全部存储在 TiKV 集群中,所以在这个过程中 TiDB Server 需要和 TiKV 交互,获取数据。最后 TiDB Server 需要将查询结果返回给用户。5、TiDB 数据库的调度
PD (Placement Driver) 是 TiDB 集群的管理模块,同时也负责集群数据的实时调度。本文档介绍一下 PD 的设计思想和关键概念。
5.1、场景描述
TiKV 集群是 TiDB 数据库的分布式 KV 存储引擎,数据以 Region 为单位进行复制和管理,每个 Region 会有多个副本 (Replica),这些副本会分布在不同的 TiKV 节点上,其中 Leader 负责读/写,Follower 负责同步 Leader 发来的 Raft log。
需要考虑以下场景:
- 为了提高集群的空间利用率,需要根据 Region 的空间占用对副本进行合理的分布。
- 集群进行跨机房部署的时候,要保证一个机房掉线,不会丢失 Raft Group 的多个副本。
- 添加一个节点进入 TiKV 集群之后,需要合理地将集群中其他节点上的数据搬到新增节点。
-
当一个节点掉线时,需要考虑快速稳定地进行容灾。
-
从节点的恢复时间来看
- 如果节点只是短暂掉线(重启服务),是否需要进行调度。
- 如果节点是长时间掉线(磁盘故障,数据全部丢失),如何进行调度。
-
假设集群需要每个 Raft Group 有 N 个副本,从单个 Raft Group 的副本个数来看
- 副本数量不够(例如节点掉线,失去副本),需要选择适当的机器的进行补充。
- 副本数量过多(例如掉线的节点又恢复正常,自动加入集群),需要合理的删除多余的副本。
-
- 读/写通过 Leader 进行,Leader 的分布只集中在少量几个节点会对集群造成影响。
- 并不是所有的 Region 都被频繁的访问,可能访问热点只在少数几个 Region,需要通过调度进行负载均衡。
- 集群在做负载均衡的时候,往往需要搬迁数据,这种数据的迁移可能会占用大量的网络带宽、磁盘 IO 以及 CPU,进而影响在线服务。
以上问题和场景如果多个同时出现,就不太容易解决,因为需要考虑全局信息。同时整个系统也是在动态变化的,因此需要一个中心节点,来对系统的整体状况进行把控和调整,所以有了 PD 这个模块。
5.2、调度的需求
对以上的问题和场景进行分类和整理,可归为以下两类:
第一类:作为一个分布式高可用存储系统,必须满足的需求,包括几种
- 副本数量不能多也不能少
- 副本需要根据拓扑结构分布在不同属性的机器上
- 节点宕机或异常能够自动合理快速地进行容灾
第二类:作为一个良好的分布式系统,需要考虑的地方包括
- 维持整个集群的 Leader 分布均匀
- 维持每个节点的储存容量均匀
- 维持访问热点分布均匀
- 控制负载均衡的速度,避免影响在线服务
- 管理节点状态,包括手动上线/下线节点
满足第一类需求后,整个系统将具备强大的容灾功能。满足第二类需求后,可以使得系统整体的资源利用率更高且合理,具备良好的扩展性。
为了满足这些需求,首先需要收集足够的信息,比如每个节点的状态、每个 Raft Group 的信息、业务访问操作的统计等;其次需要设置一些策略,PD 根据这些信息以及调度的策略,制定出尽量满足前面所述需求的调度计划;最后需要一些基本的操作,来完成调度计划。
5.3、调度的基本操作
调度的基本操作指的是为了满足调度的策略。上述调度需求可整理为以下三个操作:
- 增加一个副本
- 删除一个副本
- 将 Leader 角色在一个 Raft Group 的不同副本之间 transfer(迁移)
刚好 Raft 协议通过
AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。5.4、信息收集
调度依赖于整个集群信息的收集,简单来说,调度需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息,TiKV 节点信息和 Region 信息:
每个 TiKV 节点会定期向 PD 汇报节点的状态信息
TiKV 节点 (Store) 与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也会携带这个 Store 的状态信息,主要包括:
- 总磁盘容量
- 可用磁盘容量
- 承载的 Region 数量
- 数据写入/读取速度
- 发送/接受的 Snapshot 数量(副本之间可能会通过 Snapshot 同步数据)
- 是否过载
- labels 标签信息(标签是具备层级关系的一系列 Tag,能够感知拓扑信息)
通过使用
pd-ctl 可以查看到 TiKV Store 的状态信息。TiKV Store 的状态具体分为 Up,Disconnect,Offline,Down,Tombstone。各状态的关系如下:- Up:表示当前的 TiKV Store 处于提供服务的状态。
- Disconnect:当 PD 和 TiKV Store 的心跳信息丢失超过 20 秒后,该 Store 的状态会变为 Disconnect 状态,当时间超过
max-store-down-time指定的时间后,该 Store 会变为 Down 状态。
- Down:表示该 TiKV Store 与集群失去连接的时间已经超过了
max-store-down-time指定的时间,默认 30 分钟。超过该时间后,对应的 Store 会变为 Down,并且开始在存活的 Store 上补足各个 Region 的副本。
- Offline:当对某个 TiKV Store 通过 PD Control 进行手动下线操作,该 Store 会变为 Offline 状态。该状态只是 Store 下线的中间状态,处于该状态的 Store 会将其上的所有 Region 搬离至其它满足搬迁条件的 Up 状态 Store。当该 Store 的
leader_count和region_count(在 PD Control 中获取) 均显示为 0 后,该 Store 会由 Offline 状态变为 Tombstone 状态。在 Offline 状态下,禁止关闭该 Store 服务以及其所在的物理服务器。下线过程中,如果集群里不存在满足搬迁条件的其它目标 Store(例如没有足够的 Store 能够继续满足集群的副本数量要求),该 Store 将一直处于 Offline 状态。
- Tombstone:表示该 TiKV Store 已处于完全下线状态,可以使用
remove-tombstone接口安全地清理该状态的 TiKV。
每个 Raft Group 的 Leader 会定期向 PD 汇报 Region 的状态信息
每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 Region 的状态,主要包括下面几点信息:
- Leader 的位置
- Followers 的位置
- 掉线副本的个数
- 数据写入/读取的速度
PD 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。
除此之外,PD 还可以通过扩展的接口接受额外的信息,用来做更准确的决策。比如当某个 Store 的心跳包中断的时候,PD 并不能判断这个节点是临时失效还是永久失效,只能经过一段时间的等待(默认是 30 分钟),如果一直没有心跳包,就认为该 Store 已经下线,再决定需要将这个 Store 上面的 Region 都调度走。
但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知 PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。
5.5、调度的策略
PD 收集了这些信息后,还需要一些策略来制定具体的调度计划。
一个 Region 的副本数量正确
当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的副本数量不满足要求时,需要通过 Add/Remove Replica 操作调整副本数量。出现这种情况的可能原因是:
- 某个节点掉线,上面的数据全部丢失,导致一些 Region 的副本数量不足
- 某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了副本的 Region 的副本数量过多,需要删除某个副本
- 管理员调整副本策略,修改了 max-replicas 的配置
一个 Raft Group 中的多个副本不在同一个位置
注意这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个副本不落在一个节点上,以避免单个节点失效导致多个副本丢失。在实际部署中,还可能出现下面这些需求:
- 多个节点部署在同一台物理机器上
- TiKV 节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性
- TiKV 节点分布在多个 IDC 中,希望单个机房掉电时,也能保证系统可用性
这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的『容错单元』,希望这个单元内部不会存在一个 Region 的多个副本。这个时候,可以给节点配置 labels 并且通过在 PD 上配置 location-labels 来指名哪些 label 是位置标识,需要在副本分配的时候尽量保证一个 Region 的多个副本不会分布在具有相同的位置标识的节点上。
副本在 Store 之间的分布均匀分配
由于每个 Region 的副本中存储的数据容量上限是固定的,通过维持每个节点上面副本数量的均衡,使得各节点间承载的数据更均衡。
Leader 数量在 Store 之间均匀分配
Raft 协议要求读取和写入都通过 Leader 进行,所以计算的负载主要在 Leader 上面,PD 会尽可能将 Leader 在节点间分散开。
访问热点数量在 Store 之间均匀分配
每个 Store 以及 Region Leader 在上报信息时携带了当前访问负载的信息,比如 Key 的读取/写入速度。PD 会检测出访问热点,且将其在节点之间分散开。
各个 Store 的存储空间占用大致相等
每个 Store 启动的时候都会指定一个
Capacity 参数,表明这个 Store 的存储空间上限,PD 在做调度的时候,会考虑节点的存储空间剩余量。控制调度速度,避免影响在线服务
调度操作需要耗费 CPU、内存、磁盘 IO 以及网络带宽,需要避免对线上服务造成太大影响。PD 会对当前正在进行的操作数量进行控制,默认的速度控制是比较保守的,如果希望加快调度(比如停服务升级或者增加新节点,希望尽快调度),那么可以通过调节 PD 参数动态加快调度速度。
5.6、调度的实现
PD 不断地通过 Store 或者 Leader 的心跳包收集整个集群信息,并且根据这些信息以及调度策略生成调度操作序列。每次收到 Region Leader 发来的心跳包时,PD 都会检查这个 Region 是否有待进行的操作,然后通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。
注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 根据当前自身状态来定。
四、与 MySQL 兼容性对比
1、TiDB与MySQL对比
- TiDB 高度兼容 MySQL 5.7 协议、MySQL 5.7 常用的功能及语法。MySQL 5.7 生态中的系统工具 (PHPMyAdmin、Navicat、MySQL Workbench、mysqldump、Mydumper/Myloader)、客户端等均适用于 TiDB。
-
但 TiDB 尚未支持一些 MySQL 功能,可能的原因如下:
- 有更好的解决方案,例如 JSON 取代 XML 函数。
- 目前对这些功能的需求度不高,例如存储流程和函数。
- 一些功能在分布式系统上的实现难度较大。
-
除此以外,TiDB 不支持 MySQL 复制协议,但提供了专用工具用于与 MySQL 复制数据
- 从 MySQL 复制:TiDB Data Migration (DM) 是将 MySQL/MariaDB 数据迁移到 TiDB 的工具,可用于增量数据的复制。
- 向 MySQL 复制:TiCDC 是一款通过拉取 TiKV 变更日志实现的 TiDB 增量数据同步工具,可通过 MySQL sink 将 TiDB 增量数据复制到 MySQL。
2、不支持的功能特性
- 存储过程与函数
- 触发器
- 事件
- 自定义函数
- 外键约束 #18209
- 全文语法与索引 #1793
- 空间类型的函数(即
GIS/GEOMETRY)、数据类型和索引 #6347
- 非
ascii、latin1、binary、utf8、utf8mb4、gbk的字符集
- SYS schema
- MySQL 追踪优化器
- XML 函数
- X-Protocol #1109
- Savepoints #6840
- 列级权限 #9766
XA语法(TiDB 内部使用两阶段提交,但并没有通过 SQL 接口公开)
CREATE TABLE tblName AS SELECT stmt语法 #4754
CHECK TABLE语法 #4673
CHECKSUM TABLE语法 #1895
REPAIR TABLE语法
OPTIMIZE TABLE语法
HANDLER语句
CREATE TABLESPACE语句
3、与 MySQL 有差异的特性详细说明
3.1、自增 ID
- TiDB 的自增列既能保证唯一,也能保证在单个 TiDB server 中自增,但不保证多个 TiDB server 中自增,不保证自动分配的值的连续性。不建议将缺省值和自定义值混用,若混用可能会收到
Duplicated Error的错误信息。
- TiDB 可通过
tidb_allow_remove_auto_inc系统变量开启或者关闭允许移除列的AUTO_INCREMENT属性。删除列属性的语法是:ALTER TABLE MODIFY或ALTER TABLE CHANGE。
- TiDB 不支持添加列的
AUTO_INCREMENT属性,移除该属性后不可恢复。
自增 ID 详情可参阅 AUTO_INCREMENT。
若创建表时没有指定主键时,TiDB 会使用
_tidb_rowid 来标识行,该数值的分配会和自增列(如果存在的话)共用一个分配器。如果指定了自增列为主键,则 TiDB 会用该列来标识行。因此会有以下的示例情况:mysql> CREATE TABLE t(id INT UNIQUE KEY AUTO_INCREMENT);
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO t VALUES(),(),();
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> SELECT _tidb_rowid, id FROM t;
+-------------+------+
| _tidb_rowid | id |
+-------------+------+
| 4 | 1 |
| 5 | 2 |
| 6 | 3 |
+-------------+------+
3 rows in set (0.01 sec)
3.2、Performance schema
TiDB 主要使用 Prometheus 和 Grafana 来存储及查询相关的性能监控指标,所以 Performance schema 部分表是空表。
3.3、查询计划
TiDB 中,执行计划(
EXPLAIN 和 EXPLAIN FOR)在输出格式、内容、权限设置方面与 MySQL 有较大差别。MySQL 系统变量
optimizer_switch 在 TiDB 中是只读的,对查询计划没有影响。你还可以在 optimizer hints 中使用与 MySQL 类似的语法,但可用的 hint 和实现原理可能会有所不同。详情参见理解 TiDB 执行计划。
3.4、内建函数
支持常用的 MySQL 内建函数,有部分函数并未支持。可通过执行
SHOW BUILTINS 语句查看可用的内建函数。参考 SQL 语法文档。3.5、DDL 的限制
TiDB 中,所有支持的 DDL 变更操作都是在线执行的。与 MySQL 相比,TiDB 中的 DDL 存在以下限制:
- 不能在单条
ALTER TABLE语句中完成多个操作。例如,不能在单个语句中添加多个列或索引,否则,可能会输出Unsupported multi schema change的错误。
ALTER TABLE不支持少部分类型的变更。比如,TiDB 不支持从DECIMAL到DATE的变更。当遇到不支持的类型变更时,TiDB 将会报Unsupported modify column: type %d not match origin %d的错误。更多细节,请参考ALTER TABLE。
- TiDB 中,
ALGORITHM={INSTANT,INPLACE,COPY}语法只作为一种指定,并不更改ALTER算法,详情参阅ALTER TABLE。
- 不支持添加或删除
CLUSTERED类型的主键。要了解关于CLUSTERED主键的详细信息,请参考聚簇索引。
- 不支持指定不同类型的索引 (
HASH|BTREE|RTREE|FULLTEXT)。若指定了不同类型的索引,TiDB 会解析并忽略这些索引。
- 分区表支持
HASH、RANGE和LIST分区类型。对于不支持的分区类型,TiDB 可能会报Warning: Unsupported partition type %s, treat as normal table错误,其中%s为不支持的具体分区类型。
-
分区表还支持
ADD、DROP、TRUNCATE操作。其他分区操作会被忽略。TiDB 不支持以下分区表语法:PARTITION BY KEYSUBPARTITION{CHECK|TRUNCATE|OPTIMIZE|REPAIR|IMPORT|DISCARD|REBUILD|REORGANIZE|COALESCE} PARTITION
- 更多详情,请参考分区表文档。
3.6、ANALYZE TABLE
TiDB 中的信息统计与 MySQL 中的有所不同:TiDB 中的信息统计会完全重构表的统计数据,语句执行过程较长,但在 MySQL/InnoDB 中,它是一个轻量级语句,执行过程较短。
更多信息统计的差异请参阅
ANALYZE TABLE。3.7、SELECT的限制
- 不支持
SELECT ... INTO @变量语法。
- 不支持
SELECT ... GROUP BY ... WITH ROLLUP语法。
- TiDB 中的
SELECT .. GROUP BY expr的返回结果与 MySQL 5.7 并不一致。MySQL 5.7 的结果等价于GROUP BY expr ORDER BY expr。
详情参见
SELECT。3.8、UPDATE语句
详情参见
UPDATE。3.9、视图
TiDB 中的视图不可更新,不支持
UPDATE、INSERT、DELETE 等写入操作。3.10、临时表
3.11、字符集和排序规则
- 关于 TiDB 对字符集和排序规则的支持情况,详见字符集和排序规则。
- 关于 GBK 字符集与 MySQL 的兼容情况,详见 GBK 兼容情况。
- TiDB 继承表中使用的字符集作为国家字符集。
3.12、存储引擎
- 仅在语法上兼容创建表时指定存储引擎,实际上 TiDB 会将元信息统一描述为 InnoDB 存储引擎。TiDB 支持类似 MySQL 的存储引擎抽象,但需要在系统启动时通过
--store配置项来指定存储引擎。
3.13、SQL 模式
TiDB 支持大部分 SQL 模式。不支持的 SQL 模式如下:
- 不支持兼容模式,例如:
Oracle和PostgreSQL(TiDB 解析但会忽略这两个兼容模式),MySQL 5.7 已弃用兼容模式,MySQL 8.0 已移除兼容模式。
- TiDB 的
ONLY_FULL_GROUP_BY模式与 MySQL 5.7 相比有细微的语义差别。
NO_DIR_IN_CREATE和NO_ENGINE_SUBSTITUTION仅用于解决与 MySQL 的兼容性问题,并不适用于 TiDB。
3.14、默认设置
-
字符集:
- TiDB 默认:
utf8mb4。 - MySQL 5.7 默认:
latin1。 - MySQL 8.0 默认:
utf8mb4。
- TiDB 默认:
-
排序规则:
- TiDB 中
utf8mb4字符集默认:utf8mb4_bin。 - MySQL 5.7 中
utf8mb4字符集默认:utf8mb4_general_ci。 - MySQL 8.0 中
utf8mb4字符集默认:utf8mb4_0900_ai_ci。
- TiDB 中
-
foreign_key_checks:- TiDB 默认:
OFF,且仅支持设置该值为OFF。 - MySQL 5.7 默认:
ON。
- TiDB 默认:
-
SQL mode:
- TiDB 默认:
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION。 - MySQL 5.7 默认与 TiDB 相同。
- MySQL 8.0 默认
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION。
- TiDB 默认:
-
lower_case_table_names:- TiDB 默认:
2,且仅支持设置该值为2。 -
MySQL 默认如下:
- Linux 系统中该值为
0 - Windows 系统中该值为
1 - macOS 系统中该值为
2
- Linux 系统中该值为
- TiDB 默认:
-
explicit_defaults_for_timestamp:- TiDB 默认:
ON,且仅支持设置该值为ON。 - MySQL 5.7 默认:
OFF。 - MySQL 8.0 默认:
ON。
- TiDB 默认:
3.15、日期时间处理的区别
时区
- TiDB 采用系统当前安装的所有时区规则进行计算(一般为
tzdata包),不需要导入时区表数据就能使用所有时区名称,无法通过导入时区表数据的形式修改计算规则。
- MySQL 默认使用本地时区,依赖于系统内置的当前的时区规则(例如什么时候开始夏令时等)进行计算;且在未导入时区表数据的情况下不能通过时区名称来指定时区。
类型系统
- 不支持 FLOAT4/FLOAT8。
- 不支持
SQL_TSI_*(包括SQL_TSI_MONTH、SQL_TSI_WEEK、SQL_TSI_DAY、SQL_TSI_HOUR、SQL_TSI_MINUTE和SQL_TSI_SECOND,但不包括SQL_TSI_YEAR)。
3.16、MySQL 弃用功能导致的不兼容问题
TiDB 不支持 MySQL 中标记为弃用的功能,包括:
- 指定浮点类型的精度。MySQL 8.0 弃用了此功能,建议改用
DECIMAL类型。
ZEROFILL属性。 MySQL 8.0 弃用了此功能,建议在业务应用中填充数字值。
五、使用限制
TiDB 中常见的使用限制,包括:标识符长度,最大支持的数据库、表、索引、分区表、序列等的个数等限制。
1、标识符长度限制
| 标识符类型 | 最大长度(字符) |
| Database | 64 |
| Table | 64 |
| Column | 64 |
| Index | 64 |
| View | 64 |
| Sequence | 64 |
2、Databases、Tables、Views、Connections 总个数限制
| 标识符类型 | 最大个数 |
| Databases | unlimited |
| Tables | unlimited |
| Views | unlimited |
| Connections | unlimited |
3、单个 Database 的限制
| 类型 | 最大限制 |
| Tables | unlimited |
4、单个 Table 的限制
| 类型 | 最大限制(默认值) |
| Columns | 默认为 1017,最大可调至 4096 |
| Indexs | 默认为 64,最大可调至 512 |
| Rows | 无限制 |
| Size | 无限制 |
| Partitions | 8192 |
- Columns 的最大限制可通过
table-column-count-limit修改。
- Indexs 的最大限制可通过
index-limit修改。
5、单行的限制
| 类型 | 最大限制 |
| Size | 默认为 6MB,可通过 txn-entry-size-limit 配置项调整 |
6、单列的限制
| 类型 | 最大限制 |
| Size | 6MB |
7、字符串类型限制
| 类型 | 最大限制 |
| CHAR | 256 字符 |
| BINARY | 256 字节 |
| VARBINARY | 65535 字节 |
| VARCHAR | 16383 字符 |
| TEXT | 6MB 字节 |
| BLOB | 6MB 字节 |
8、SQL Statements 的限制
| 类型 | 最大限制 |
| 单个事务最大语句数 | 在使用乐观事务并开启事务重试的情况下,默认限制 5000,可通过 stmt-count-limit 调整 |
本文来自博客园,作者:up~up,转载请注明原文链接:https://www.cnblogs.com/soft-engineer/articles/16599088.html
浙公网安备 33010602011771号