TiDB 介绍、安装与使用

1. 简介

TiDB 是 PingCAP 公司设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。TiDB 的目标是为 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 场景提供一站式的解决方案。

特性:

  • 高度兼容 MySQL
    大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移。

  • 水平弹性扩展
    通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景。

  • 分布式事务
    TiDB 100% 支持标准的 ACID 事务。

  • 真正金融级高可用
    相比于传统主从 (M-S) 复制方案,基于 Raft 的多数派选举协议可以提供金融级的 100% 数据强一致性保证,且在不丢失大多数副本的前提下,可以实现故障的自动恢复 (auto-failover),无需人工介入。

  • 一站式 HTAP 解决方案
    TiDB 作为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP 解决方案,一份存储同时处理 OLTP & OLAP,无需传统繁琐的 ETL 过程。

  • 云原生 SQL 数据库
    TiDB 是为云而设计的数据库,支持公有云、私有云和混合云,配合 TiDB Operator 项目 可实现自动化运维,使部署、配置和维护变得十分简单。

TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。

TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。

2. 整体架构

TiDB 集群主要包括三个核心组件:TiDB Server,PD Server 和 TiKV Server。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件和简化云上部署管理的 TiDB Operator 组件。
TiDB 整体架构
TiDB Server
TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。

PD Server
Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。
PD 通过 Raft 协议保证数据的安全性。Raft 的 leader server 负责处理所有操作,其余的 PD server 仅用于保证高可用。建议部署奇数个 PD 节点。

TiKV Server
TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

TiSpark
TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,并融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。

TiDB Operator
TiDB Operator 提供在主流云基础设施(Kubernetes)上部署管理 TiDB 集群的能力。它结合云原生社区的容器编排最佳实践与 TiDB 的专业运维知识,集成一键部署、多集群混部、自动运维、故障自愈等能力,极大地降低了用户使用和管理 TiDB 的门槛与成本。

3. 核心特性

两大核心特性:水平扩展与高可用。

3.1 水平扩展

无限水平扩展是 TiDB 的一大特点,这里说的水平扩展包括两方面:计算能力和存储能力。TiDB Server 负责处理 SQL 请求,随着业务的增长,可以简单的添加 TiDB Server 节点,提高整体的处理能力,提供更高的吞吐。TiKV 负责存储数据,随着数据量的增长,可以部署更多的 TiKV Server 节点解决数据 Scale 的问题。PD 会在 TiKV 节点之间以 Region 为单位做调度,将部分数据迁移到新加的节点上。所以在业务的早期,可以只部署少量的服务实例(推荐至少部署 3 个 TiKV, 3 个 PD,2 个 TiDB),随着业务量的增长,按照需求添加 TiKV 或者 TiDB 实例。

3.2 高可用

高可用是 TiDB 的另一大特点,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。下面分别说明这三个组件的可用性、单个实例失效后的后果以及如何恢复。

TiDB
TiDB 是无状态的,推荐至少部署两个实例,前端通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,从应用的角度看,会出现单次请求失败的情况,重新连接后即可继续获得服务。单个实例失效后,可以重启这个实例或者部署一个新的实例。

PD
PD 是一个集群,通过 Raft 协议保持数据的一致性,单个实例失效时,如果这个实例不是 Raft 的 leader,那么服务完全不受影响;如果这个实例是 Raft 的 leader,会重新选出新的 Raft leader,自动恢复服务。PD 在选举的过程中无法对外提供服务,这个时间大约是3秒钟。推荐至少部署三个 PD 实例,单个实例失效后,重启这个实例或者添加新的实例。

TiKV
TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 节点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 30 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。

4. 技术内幕

官方写了三篇文章介绍了技术内幕:
说存储
说计算
谈调度

4.1 说存储

Key-Value
作为保存数据的系统,首先要决定的是数据的存储模型,也就是数据以什么样的形式保存下来。TiKV 的选择是 Key-Value 模型,并且提供有序遍历方法。简单来讲,可以将 TiKV 看做一个巨大的 Map,其中 Key 和 Value 都是原始的 Byte 数组,在这个 Map 中,Key 按照 Byte 数组总的原始二进制比特位比较顺序排列。

大家这里需要对 TiKV 记住两点:

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

现在让我们忘记 SQL 中的任何概念,专注于讨论如何实现 TiKV 这样一个高性能高可靠性的巨大的(分布式的) Map。

RocksDB
任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 也不例外。但是 TiKV 没有选择直接向磁盘上写数据,而是把数据保存在 RocksDB 中,具体的数据落地由 RocksDB 负责。这个选择的原因是开发一个单机存储引擎工作量很大,特别是要做一个高性能的单机引擎,需要做各种细致的优化,而 RocksDB 是一个非常优秀的开源的单机存储引擎,可以满足我们对单机引擎的各种要求,而且还有 Facebook 的团队在做持续的优化,这样我们只投入很少的精力,就能享受到一个十分强大且在不断进步的单机引擎。当然,我们也为 RocksDB 贡献了一些代码,希望这个项目能越做越好。这里可以简单的认为 RocksDB 是一个单机的 Key-Value Map。

Raft
Raft是一个一致性协议,提供几个重要的功能:

  1. Leader 选举
  2. 成员变更
  3. 日志复制

TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft 的日志复制功能,将数据安全可靠地同步到 Group 的多数节点中。
raft
通过单机的 RocksDB,我们可以将数据快速地存储在磁盘上;通过 Raft,我们可以将数据复制到多台机器上,以防单机失效。数据的写入是通过 Raft 这一层的接口写入,而不是直接写 RocksDB。通过实现 Raft,我们拥有了一个分布式的 KV,现在再也不用担心某台机器挂掉了。

Region
讲到这里,我们可以提到一个 非常重要的概念:Region。

前面提到,我们将 TiKV 看做一个巨大的有序的 KV Map,那么为了实现存储的水平扩展,我们需要将数据分散在多台机器上。这里提到的数据分散在多台机器上和 Raft 的数据复制不是一个概念,在这一节我们先忘记 Raft,假设所有的数据都只有一个副本,这样更容易理解。

对于一个 KV 系统,将数据分散在多台机器上有两种比较典型的方案:

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

TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,我们将每一段叫做一个 Region,并且我们会尽量保持每个 Region 中保存的数据不超过一定的大小(这个大小可以配置,目前默认是 64mb)。每一个 Region 都可以用 StartKey 到 EndKey 这样一个左闭右开区间来描述。
tikv
注意,这里的 Region 还是和 SQL 中的表没什么关系! 请各位继续忘记 SQL,只谈 KV。 将数据划分成 Region 后,我们将会做 两件重要的事情:

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

先看第一点,数据按照 Key 切分成很多 Region,每个 Region 的数据只会保存在一个节点上面。我们的系统会有一个组件来负责将 Region 尽可能均匀的散布在集群中所有的节点上,这样一方面实现了存储容量的水平扩展(增加新的结点后,会自动将其他节点上的 Region 调度过来),另一方面也实现了负载均衡(不会出现某个节点有很多数据,其他节点上没什么数据的情况)。同时为了保证上层客户端能够访问所需要的数据,我们的系统中也会有一个组件记录 Region 在节点上面的分布情况,也就是通过任意一个 Key 就能查询到这个 Key 在哪个 Region 中,以及这个 Region 目前在哪个节点上。至于是哪个组件负责这两项工作,会在后续介绍。

对于第二点,TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本,我们将每一个副本叫做一个 Replica。Replica 之间是通过 Raft 来保持数据的一致(终于提到了 Raft),一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft Group。其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。所有的读和写都是通过 Leader 进行,再由 Leader 复制给 Follower。 大家理解了 Region 之后,应该可以理解下面这张图:

raft group
以 Region 为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的 KeyValue 系统,不用再担心数据存不下,或者是磁盘故障丢失数据的问题。这已经很 Cool,但是还不够完美,我们需要更多的功能。

MVCC
很多数据库都会实现多版本控制(MVCC),TiKV 也不例外。设想这样的场景,两个 Client 同时去修改一个 Key 的 Value,如果没有 MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。 TiKV 的 MVCC 实现是通过在 Key 后面添加 Version 来实现,简单来说,没有 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。然后可以直接 Seek(Key-Version),定位到第一个大于等于这个 Key-Version 的位置。

事务
TiKV 的事务采用的是 Percolator 模型,并且做了大量的优化。

TiKV 的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方会尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,就是计数器,多个客户端同时修改少量行,导致冲突严重的,造成大量的无效重试。

4.2 说计算

关系模型到 Key-Value 模型的映射
在这我们将关系模型简单理解为 Table 和 SQL 语句,那么问题变为如何在 KV 结构上保存 Table 以及如何在 KV 结构上运行 SQL 语句。 假设我们有这样一个表的定义:

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

SQL 和 KV 结构之间存在巨大的区别,那么如何能够方便高效地进行映射,就成为一个很重要的问题。一个好的映射方案必须有利于对数据操作的需求。那么我们先看一下对数据的操作有哪些需求,分别有哪些特点。

对于一个 Table 来说,需要存储的数据包括三部分:

  1. 表的元信息
  2. Table 中的 Row
  3. 索引数据

表的元信息我们暂时不讨论,会有专门的章节来介绍。 对于 Row,可以选择行存或者列存,这两种各有优缺点。TiDB 面向的首要目标是 OLTP 业务,这类业务需要支持快速地读取、保存、修改、删除一行数据,所以采用行存是比较合适的。

对于 Index,TiDB 不止需要支持 Primary Index,还需要支持 Secondary Index。Index 的作用的辅助查询,提升查询性能,以及保证某些 Constraint。查询的时候有两种模式,一种是点查,比如通过 Primary Key 或者 Unique Key 的等值条件进行查询,如 select name from user where id=1; ,这种需要通过索引快速定位到某一行数据;另一种是 Range 查询,如 select name from user where age > 30 and age < 35;,这个时候需要通过idxAge索引查询 age 在 30 和 35 之间的那些数据。Index 还分为 Unique Index 和 非 Unique Index,这两种都需要支持。

分析完需要存储的数据的特点,我们再看看对这些数据的操作需求,主要考虑 Insert/Update/Delete/Select 这四种语句。

对于 Insert 语句,需要将 Row 写入 KV,并且建立好索引数据。

对于 Update 语句,需要将 Row 更新的同时,更新索引数据(如果有必要)。

对于 Delete 语句,需要在删除 Row 的同时,将索引也删除。

上面三个语句处理起来都很简单。对于 Select 语句,情况会复杂一些。首先我们需要能够简单快速地读取一行数据,所以每个 Row 需要有一个 ID (显示或隐式的 ID)。其次可能会读取连续多行数据,比如 Select * from user;。最后还有通过索引读取数据的需求,对索引的使用可能是点查或者是范围查询。

大致的需求已经分析完了,现在让我们看看手里有什么可以用的:一个全局有序的分布式 Key-Value 引擎。全局有序这一点重要,可以帮助我们解决不少问题。比如对于快速获取一行数据,假设我们能够构造出某一个或者某几个 Key,定位到这一行,我们就能利用 TiKV 提供的 Seek 方法快速定位到这一行数据所在位置。再比如对于扫描全表的需求,如果能够映射为一个 Key 的 Range,从 StartKey 扫描到 EndKey,那么就可以简单的通过这种方式获得全表数据。操作 Index 数据也是类似的思路。接下来让我们看看 TiDB 是如何做的。

TiDB 对每个表分配一个 TableID,每一个索引都会分配一个 IndexID,每一行分配一个 RowID(如果表有整数型的 Primary Key,那么会用 Primary Key 的值当做 RowID),其中 TableID 在整个集群内唯一,IndexID/RowID 在表内唯一,这些 ID 都是 int64 类型。

每行数据按照如下规则进行编码成 Key-Value pair:

Key: tablePrefix{tableID}_recordPrefixSep{rowID}
Value: [col1, col2, col3, col4]

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

对于 Index 数据,会按照如下规则编码成 Key-Value pair:

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

Index 数据还需要考虑 Unique Index 和非 Unique Index 两种情况,对于 Unique Index,可以按照上述编码规则。但是对于非 Unique Index,通过这种编码并不能构造出唯一的 Key,因为同一个 Index 的 tablePrefix{tableID}_indexPrefixSep{indexID} 都一样,可能有多行数据的 ColumnsValue 是一样的,所以对于非 Unique Index 的编码做了一点调整:

Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
Value: null

这样能够对索引中的每行数据构造出唯一的 Key。 注意上述编码规则中的 Key 里面的各种 xxPrefix 都是字符串常量,作用都是区分命名空间,以免不同类型的数据之间相互冲突,定义如下:

var(
	tablePrefix     = []byte{'t'}
	recordPrefixSep = []byte("_r")
	indexPrefixSep  = []byte("_i")
)

另外请大家注意,上述方案中,无论是 Row 还是 Index 的 Key 编码方案,一个 Table 内部所有的 Row 都有相同的前缀,一个 Index 的数据也都有相同的前缀。这样具体相同的前缀的数据,在 TiKV 的 Key 空间内,是排列在一起。同时只要我们小心地设计后缀部分的编码方案,保证编码前和编码后的比较关系不变,那么就可以将 Row 或者 Index 数据有序地保存在 TiKV 中。这种保证编码前和编码后的比较关系不变 的方案我们称为 Memcomparable,对于任何类型的值,两个对象编码前的原始类型比较结果,和编码成 byte 数组后(注意,TiKV 中的 Key 和 Value 都是原始的 byte 数组)的比较结果保持一致。具体的编码方案参见 TiDB 的 codec 包。采用这种编码后,一个表的所有 Row 数据就会按照 RowID 的顺序排列在 TiKV 的 Key 空间中,某一个 Index 的数据也会按照 Index 的 ColumnValue 顺序排列在 Key 空间内。

现在我们结合开始提到的需求以及 TiDB 的映射方案来看一下,这个方案是否能满足需求。首先我们通过这个映射方案,将 Row 和 Index 数据都转换为 Key-Value 数据,且每一行、每一条索引数据都是有唯一的 Key。其次,这种映射方案对于点查、范围查询都很友好,我们可以很容易地构造出某行、某条索引所对应的 Key,或者是某一块相邻的行、相邻的索引值所对应的 Key 范围。最后,在保证表中的一些 Constraint 的时候,可以通过构造并检查某个 Key 是否存在来判断是否能够满足相应的 Constraint。

至此我们已经聊完了如何将 Table 映射到 KV 上面,这里再举个简单的例子,便于大家理解,还是以上面的表结构为例。假设表中有 3 行数据:

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

那么首先每行数据都会映射为一个 Key-Value pair,注意这个表有一个 Int 类型的 Primary Key,所以 RowID 的值即为这个 Primary Key 的值。假设这个表的 Table ID 为 10,其 Row 的数据为:

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

除了 Primary Key 之外,这个表还有一个 Index,假设这个 Index 的 ID 为 1,则其数据为:

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

元信息管理
上节介绍了表中的数据和索引是如何映射为 KV,本节介绍一下元信息的存储。Database/Table 都有元信息,也就是其定义以及各项属性,这些信息也需要持久化,我们也将这些信息存储在 TiKV 中。每个 Database/Table 都被分配了一个唯一的 ID,这个 ID 作为唯一标识,并且在编码为 Key-Value 时,这个 ID 都会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 Key,Value 中存储的是序列化后的元信息。 除此之外,还有一个专门的 Key-Value 存储当前 Schema 信息的版本。TiDB 使用 Google F1 的 Online Schema 变更算法,有一个后台线程在不断的检查 TiKV 上面存储的 Schema 版本是否发生变化,并且保证在一定时间内一定能够获取版本的变化(如果确实发生了变化)。这部分的具体实现参见 TiDB 的异步 schema 变更实现一文。

SQL on KV 架构
TiDB 的整体架构如下图所示
SQL on KV 架构
TiKV Cluster 主要作用是作为 KV 引擎存储数据。SQL 层,也就是 TiDB Servers 这一层,这一层的节点都是无状态的节点,本身并不存储数据,节点之间完全对等。TiDB Server 这一层最重要的工作是处理用户请求,执行 SQL 运算逻辑,接下来我们做一些简单的介绍。

SQL 运算
理解了 SQL 到 KV 的映射方案之后,我们可以理解关系数据是如何保存的,接下来我们要理解如何使用这些数据来满足用户的查询需求,也就是一个查询语句是如何操作底层存储的数据。 能想到的最简单的方案就是通过上一节所述的映射方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。

比如 Select count(*) from user where name=“TiDB”; 这样一个语句,我们需要读取表中所有的数据,然后检查 Name 字段是否是 TiDB,如果是的话,则返回这一行。这样一个操作流程转换为 KV 操作流程:

  • 构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,那么我们用 0 和 MaxInt64 根据 Row 的 Key 编码规则,就能构造出一个 [StartKey, EndKey) 的左闭右开区间
  • 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据
  • 过滤数据:对于读到的每一行数据,计算 name=“TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据
  • 计算 Count:对符合要求的每一行,累计到 Count 值上面 这个方案肯定是可以 Work 的,但是并不能 Work 的很好,原因是显而易见的:
  1. 在扫描数据的时候,每一行都要通过 KV 操作同 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大
  2. 并不是所有的行都有用,如果不满足条件,其实可以不读取出来
  3. 符合要求的行的值并没有什么意义,实际上这里只需要有几行数据这个信息就行

分布式 SQL 运算
如何避免上述缺陷也是显而易见的,首先我们需要将计算尽量靠近存储节点,以避免大量的 RPC 调用。其次,我们需要将 Filter 也下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。最后,我们可以将聚合函数、GroupBy 也下推到存储节点,进行预聚合,每个节点只需要返回一个 Count 值即可,再由 tidb-server 将 Count 值 Sum 起来。

数据逐层返回示意图:
数据逐层返回示意图
MPP and SMP in TiDB 这篇文章详细描述了 TiDB 是如何让 SQL 语句跑的更快。

SQL 层架构
上面几节简要介绍了 SQL 层的一些功能,实际上 TiDB 的 SQL 层要复杂的多,模块以及层次非常多,下面这个图列出了重要的模块以及调用关系:
SQL 层架构
用户的 SQL 请求会直接或者通过 Load Balancer 发送到 tidb-server,tidb-server 会解析 MySQL Protocol Packet,获取请求内容,然后做语法解析、查询计划制定和优化、执行查询计划获取和处理数据。数据全部存储在 TiKV 集群中,所以在这个过程中 tidb-server 需要和 tikv-server 交互,获取数据。最后 tidb-server 需要将查询结果返回给用户。

4.3 谈调度

PD 不断的通过 Store(TiKV 节点) 或者 Leader(每个 Raft Group) 的心跳包收集信息,获得整个集群的详细数据,并且根据这些信息以及调度策略生成调度操作序列,每次收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 自己根据当前自身状态来定。

5. 安装

使用 Docker Compose 在本地快速部署 TiDB 集群,该部署方式不适用于生产环境。

下载docker-compose
上传到 /usr/local/bin
/usr/local/bin:用户放置自己的可执行程序的地方,放在这里不会被系统升级而覆盖同名文件。

修改docker-compose运行权限

mv docker-compose-Linux-x86_64_2 docker-compose
sudo chmod +x docker-compose

下载 tidb-docker-compose

unzip tidb-docker-compose-master.zip
mv -f tidb-docker-compose-master /usr/local/softwares/tidb-docker-compose

运行

cd /usr/local/softwares/tidb-docker-compose && docker-compose pull && docker-compose up –d

像mysql使用一样,并且初始root用户无密码

mysql -h 127.0.0.1 -P 4000 -u root

tidb-docker-compose 已默认部署:3 个 PD,3 个 TiKV,1 个 TiDB 和监控组件 Prometheus,Pushgateway,Grafana 以及 tidb-vision。
tidb-docker-compose
监控组件默认地址:
Prometheus http://localhost:9090
Pushgateway http://localhost:9091
Grafana http://localhost:3000
tidb-vision http://localhost:8010

6. 配置

6.1 修改root密码

使用navicat连接tidb,若使用navicat修改root用户密码,会报错。

MySQL5.7后user表的密码字段应该为authentication_string,而TiDB虽然查看MySQL版本是5.7,但是user表密码字段还是Password。
navicat修改密码报错
TiDB 作为分布式数据库,在 TiDB 中修改用户密码建议使用 set password 或 alter 方法,不推荐使用 update mysql.user 的方法进行,这种方法可能会造成其它节点刷新不及时的情况。修改权限也一样,都建议采用官方的标准语法。

set password for 'root'@'%' = 'root';

创建tidb用户,可像MySQL一样直接创建数据库和表

CREATE USER 'tidb'@'%' IDENTIFIED BY 'tidb';
GRANT ALL PRIVILEGES ON *.* TO 'tidb'@'%';
SHOW GRANTS for 'tidb'@'%';

6.2 监控组件

官方在 Grafana 默认通过配置导入Prometheus时序数据库配置,不能通过UI修改该配置
Prometheus配置
所以修改hosts,添加pushgateway和prometheus映射到虚拟机ip

#tidb
192.168.110.40 pushgateway
192.168.110.40 prometheus

一开始 Grafana 在上未检测到数据,到Prometheus的web页面看到如下警告

Prometheus依赖准确的时间安装时间同步
警告!在浏览器和服务器之间检测到86398.76秒的时差。Prometheus依赖准确的时间,时间漂移可能会导致意外的查询结果。

安装时间同步ntp

yum -y install ntp 
systemctl start ntpd.service 
systemctl status ntpd.service
systemctl enable ntpd.service

Prometheus和Pushgateway正常
Prometheus
Grafana正常
Grafana

7. 使用

创建数据库

CREATE DATABASE IF NOT EXISTS tidb;

创建表

CREATE TABLE person (
	id INT(11),
	name VARCHAR(255)
);

可使用jpa,直接通过MySQL驱动连接TiDB,并成功写入5w条数据

使用 TiSpark 分析

docker exec -it tidb-docker-compose_tispark-master_1 /opt/spark/bin/spark-shell

执行

import org.apache.spark.sql.TiContext
val ti = new TiContext(spark)
ti.tidbMapDatabase("tidb")
spark.sql("select count(*) from person").show

tispark

spark.sql("select * from person where id > 20000 and id < 20050").show

tispark

8. MySQL迁移

下载 TiDB 企业版工具集 (Linux)

mkdir  /usr/local/softwares/tidb-enterprise-tools
tar -zxvf tidb-enterprise-tools-latest-linux-amd64.tar.gz -C /usr/local/softwares/tidb-enterprise-tools

8.1 全量迁移

导出

./mydumper -h xxx.xxx.xx.xx -P 3306 -u root -p root -B test -T user -t 16 -F 64 --skip-tz-utc -o ./user

-B tidb 数据库tidb
-T user 表user
-t 16 使用 16 个线程去导出数据
-F 64 将实际的 table 切分成多大的 chunk,这里就是 64MB 一个 chunk
--skip-tz-utc 添加这个参数忽略掉 MySQL 与导数据的机器之间时区设置不一致的情况,禁止自动转换。
-o ./user 导出文件夹

导出的user文件夹下包括数据库sql,表sql,数据sql以及metadata文件

MySQL未开启binlog则内容为开始和结束备份的时间,开启binlog会多出用log的信息
导出
导入

./loader -h 127.0.0.1 -P 4000 -u tidb -p tidb -t 32 -d ./user

-t 32 使用 32 个线程去导入数据
-d ./user 导入文件夹
导入

8.2 增量复制

TiDB 提供了 syncer 工具,可以方便地将 MySQL 的数据增量的导入到 TiDB 里面。

同步前检查

1.检查数据库版本

select @@version; 

支持5.5 < MySQL 版本 < 8.0

2.检查源库 server-id

show global variables like 'server_id';

结果为空或者为 0,Syncer 无法同步数据。
Syncer server-id 与 MySQL server-id 不能相同,且必须在 MySQL cluster 中唯一。

3.检查 Binlog 相关参数

检查 MySQL 是否开启了 binlog

show global variables like 'log_bin';

binlog 格式必须为 ROW,且参数 binlog_row_image 必须设置为 FULL,可使用如下命令查看参数设置:

select variable_name, variable_value from information_schema.global_variables where variable_name in ('binlog_format','binlog_row_image');

5.7中直接执行会出错,从MySQL 5.7.6开始 information_schema.global_variables 已经开始被舍弃,为了兼容性,此时需要打开 show_compatibility_56

查看show_compatibility_56

show variables like '%show_compatibility_56%';

修改my.ini,添加后重启

show_compatibility_56 = 1

4.检查 SQL mode

必须确认上下游的 SQL mode 一致;如果不一致,则会出现数据同步的错误。

show variables like '%sql_mode%';

TiDB 的 sql_mode 与 MySQL 的 sql_mode 设置方法有一些差别,TiDB 不支持配置文件配置设置数据库的 sql_mode,而只能使用 set 命令去设置,重启后生效

set @@global.sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

5.检查权限,字符集,同步的表是否都有主键或者唯一索引

添加配置
1.获取同步 position

Mydumper 导出的数据目录里面有一个 metadata 文件,里面就包含了我们所需的 position 信息。我们将 position 相关的信息保存到一个 syncer.meta 文件里面,用于 syncer 的同步。

在user文件夹下新建syncer.meta文件

binlog-name = "mysql-bin.000002"
binlog-pos = 1065

syncer.meta 只需要第一次使用的时候配置,后续 syncer 同步新的 binlog 之后会自动将其更新到最新的 position。
如果使用 binlog position 同步则只需要配置 binlog-name binlog-pos; 使用 gtid 同步则需要设置 gtid,且启动 Syncer 时带有 --enable-gtid。

再新建syncer的配置文件 config.toml

log-level = "info"
log-file = "syncer.log"
log-rotate = "day"

server-id = 101

## meta 文件地址
meta = "./syncer.meta"
worker-count = 16
batch = 100
flavor = "mysql"

## Prometheus 可以通过该地址拉取 Syncer metrics,也是 Syncer 的 pprof 调试地址
status-addr = ":8271"

## 如果设置为 true,Syncer 遇到 DDL 语句时就会停止退出
stop-on-ddl = false

## SQL 请求由于网络异常等原因出错时的最大重试次数
max-retry = 100

[from]
host = "xxx.xxx.xx.xx"
user = "root"
password = "root"
port = 3306

[to]
host = "127.0.0.1"
user = "tidb"
password = "tidb"
port = 4000

启动syncer

../syncer -config config.toml

syncer启动
增量同步成功
源表在这里插入图片描述

参考:
TiDB 文档
使用 Docker Compose 快速构建 TiDB 集群
TIDB FAQ
MySQL 迁移
set global show_compatibility_56 = on;永久生效MySQL重启

发布了57 篇原创文章 · 获赞 11 · 访问量 9831

猜你喜欢

转载自blog.csdn.net/qq_36160730/article/details/104058705
今日推荐