TiDB原理与特性

TiDB原理和特性

来源于《TiDB in action》

1TiDB 整体架构

近年来,随着移动互联网、云计算、大数据和人工智能等技术的飞速发展,给各行业带来了深刻的影响和变革,使得企业的

数据量越来越庞大,应用的规模也越来越复杂。在这个背景之下,传统的单机数据库已经在很多场景下表现的力不从心,为

了解决海量数据平台的扩展性的问题,TiDB 分布式数据库应运而生。

TiDB 有以下的一些优势:

  • 纯分布式架构,拥有良好的扩展性,支持弹性的扩缩容
  • 支持 SQL,对外暴露 MySQL 的网络协议,并兼容大多数 MySQL 的语法,在大多数场景下可以直接替换 MySQL
  • 默认支持高可用,在少数副本失效的情况下,数据库本身能够自动进行数据修复和故障转移,对业务透明
  • 支持 ACID 事务对于一些有强一致需求的场景友好,例如:银行转账
  • 具有丰富的工具链生态,覆盖数据迁移、同步、备份等多种场景

TiDB 分布式数据库最初的设计受到 Google 内部开发的知名分布式数据库 Spanner 和 F1 的启发,在内核设计上将整体的架构

拆分成多个大的模块,大的模块之间互相通信,组成完整的 TiDB 系统。大的架构如下:

请添加图片描述

这三个大模块相互通信,每个模块都是分布式的架构,在 TiDB 中,对应的这几个模块叫做:

请添加图片描述

  1. TiDB: SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,客户端的连接可以均匀的分摊在多个 TiDB 实例上以达到负载均衡的效果。tidb-server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储层
  2. TiKV : 分布式 KV 存储,类似 NoSQL 数据库,作为 TiDB 的默认分布式存储引擎,支持完全弹性的扩容和缩容,数据分布在多个 TiKV 存储节点中,系统会动态且自动地进行均衡,绝大多数情况下不需要人工介入。与普通的 NoSQL 系统不一样的是,TiKV 的 API 能够在 KV 键值对层面提供对分布式事务的原生支持,默认提供了 SI (Snapshot Isolation)的隔离级别,这也是 TiDB 在 SQL 层面支持分布式事务的核心,上面提到的TiDB SQL 层做完 SQL 解析后,会将 SQL 的执行计划转换为实际对 TiKV API 的调用。所以实际上数据都是存储在 TiKV中。另外,TiKV 中的数据都会自动维护多副本(默认为 3),天然支持高可用和自动故障转移。TiFlash 是一类特殊的存储节点,和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。
  3. Placement Driver: 整个 TiDB 集群的元信息管理模块,负责存储每个TiKV 节点实时的数据分布情况和集群的整体拓扑结构,提供 Dashboard 管控界面,并为分布式事务分配事务 ID。PD 不仅仅是单纯的元信息存储,同时 PD 会根据 TiKV 节点实时上报的数据分布状态,下发数据调度命令给具体的 TiKV 节点,可以说是整个集群的「大脑」,另外 PD 本身也是由至少 3 个对等节点构成,拥有高可用的能力。

2TiDB 的存储

2.1 Key-Value Pairs (键值对)

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

  1. 这是一个巨大的 Map(可以类比一下 C++ 的 std::map),也就是存储的是 Key-Value Pairs(键值对)

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

强调:

TiKVKV 存储模型和 SQL 中的 Table 无关!

2.2 本地存储(RocksDB

任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 也不例外。但是 TiKV 没有选择直接向磁盘上写数据,而是把数据保存在 RocksDB 中,具体的数据落地由 RocksDB 负责。这是引文开发一个单机存储引擎工作量很大,特别是要做一个高性能的单机引擎,需要做各种细致的优化,而 RocksDB 是由 Facebook 开源的一个非常优秀的单机 KV 存储引擎,可以满足 TiKV 对单机引擎的各种要求。这里可以简单的认为 RocksDB 是一个单机的持久化 Key-Value Map。

2.3 Raft 协议

TiKV 如何保证单机失效的情况下,数据不丢失,不出错?

简单来说,需要想办法把数据复制到多台机器上,这样一台机器挂了,其他的机器上的副本还能提供服务; 复杂来说,还需要这个数据复制方案是可靠和高效的,并且能处理副本失效的情况。TiKV 选择了 Raft 算法。Raft 是一个一致性协议,它和Multi Paxos 实现一样的功能,但是更加易于理解。Raft 提供几个重要的功能:

  1. Leader(主副本)选举

  2. 成员变更(如添加副本、删除副本、转移 Leader 等操作)

  3. 日志复制

TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft 的日志复制功能,将数据安全可靠地同步到复制组的每一个节点中。不过在实际写入中,根据 Raft 的协议,只需要同步复制到多数节点,即可安全地认为数据写入成功。

2.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 后,TiKV 将会做两件重要的事情:

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

先看第一点,数据按照 Key 切分成很多 Region,每个 Region 的数据只会保存在一个节点上面(暂不考虑多副本)。TiDB 系统会有一个组件(PD)来负责将 Region 尽可能均匀的散布在集群中所有的节点上,这样一方面实现了存储容量的水平扩展(增加新的节点后,会自动将其他节点上的 Region 调度过来),另一方面也实现了负载均衡(不会出现某个节点有很多数据,其他节点上没什么数据的情况)。同时为了保证上层客户端能够访问所需要的数据,系统中也会有一个组件(PD)记录 Region 在节点上面的分布情况,也就是通过任意一个 Key 就能查询到这个 Key 在哪个 Region 中,以及这个 Region 目前在哪个节点上(即 Key 的位置路由信息)。对于第二点,TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本,TiKV 将每一个副本叫做一个 Replica。Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个Raft Group。其中一个 Replica 会作为这个 Group的 Leader,其他的 Replica 作为 Follower。所有的读和写都是通过 Leader 进行,读操作在 Leader 上即可完成,而写操作再由 Leader 复制给 Follower。

2.5 MVCC

很多数据库都会实现多版本并发控制(MVCC),TiKV 也不例外。设想这样的场景,两个客户端同时去修改一个 Key 的Value,如果没有数据的多版本控制,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。 TiKV 的 MVCC实现是通过在 Key 后面添加版本号来实现的。

2.6 分布式 ACID 事务

TiKV 的事务采用的是 Google 在 BigTable 中使用的事务模型:Percolator,TiKV 根据这篇论文实现,并做了大量的优化。

在 TiKV 层的事务 API 的语义类似下面的伪代码:

tx = tikv.Begin()

	tx.Set(Key1, Value1)

	tx.Set(Key2, Value2)

	tx.Set(Key3, Value3)

tx.Commit()

这个事务中包含3条 Set 操作,TiKV 能保证这些操作要么全部成功,要么全部失败,不会出现中间状态或脏数据。 就如前面提到的,TiDB 的 SQL 层会将 SQL 的执行计划转换成多个 KV 操作,对于上层的同一个业务层的 SQL 事务,在底层也是对应一个 KV 层的事务,这是 TiDB 实现 MySQL 的事务语义的关键。

3章 TiDB的计算

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

本节介绍TiDB 中数据到 (Key, Value) 键值对的映射方案。这里的数据主要包括两个方面:

  1. 表中每一行的数据,以下简称表数据

  2. 表中所有索引的数据,以下简称索引数据

下面分别对这两个方面进行介绍。

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

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

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

  1. 为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用 TableID 表示。表 ID 是一个整数,在整个集群内唯一。

  2. TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID。

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

Key: tablePrefix{TableID}_recordPrefixSep{RowID}

Value: [col1, col2, col3, col4]

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

3.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
3.1.3 映射关系小结

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

tablePrefix = []byte{'t'}

recordPrefixSep = []byte{'r'}

indexPrefixSep = []byte{'i'}

3.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 内置的 etcd 中,其Key为"/tidb/ddl/global_schema_version",Value 是类型为 int64 的版本号值。 TiDB 使用 Google F1 的 Online Schema 变更算法,有一个后台线程在不断的检查 etcd 中存储的表结构信息的版本号是否发生变化,并且保证在一定时间内一定能够获取版本的变化。

3.3 SQL 层简介

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

这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。

3.3.1 SQL 运算

能想到的最简单的方案就是通过上一节所述的 表中所有数据和 Key-Value 的映射关系 映射方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。

比如

select count(*) from user where name = "TiDB" 

这样一个语句,我们需要读取表中所有的数据,然后检查 name 字段是否是 TiDB ,如果是的话,则返回这一行。具体流程是:

  1. 构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,那么我们用 0 和 MaxInt64 根据行数据的 Key 编码规则,就能构造出一个 [StartKey, EndKey) 的左闭右开区间

  2. 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据

  3. 过滤数据:对于读到的每一行数据,计算 name = “TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据

  4. 计算 Count() :对符合要求的每一行,累计到 Count() 的结果上面

这个方案不能 Work 的很好,原因是显而易见的:

  1. 在扫描数据的时候,每一行都要通过 KV 操作从 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大

  2. 并不是所有的行都有用,如果不满足条件,其实可以不读取出来

  3. 符合要求的行的值并没有什么意义,实际上这里只需要有几行数据这个信息就行

3.3.2 分布式 SQL 运算

如何避免上述缺陷也是显而易见的,我们需要将计算尽量靠近存储节点,以避免大量的 RPC 调用。首先,我们需要将 SQL中的谓词条件下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。然后,我们还可以将聚合函数Count() 也下推到存储节点,进行预聚合,每个节点只需要返回一个 Count() 的结果即可,再由 SQL 层将各个节点返回的 Count(*) 的结果累加求和。

3.3.3 SQL 层架构

通过上面的例子,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL 层要复杂的多,模块以及层次非常多,下面这个图列出了重要的模块以及调用关系:

请添加图片描述

用户的 SQL 请求会直接或者通过 Load Balancer 发送到 tidb-server,tidb-server 会解析 MySQL Protocol Packet ,获取请求内容,对 SQL 进行语法解析和语义分析,制定和优化查询计划,执行查询计划并获取和处理数据。数据全部存储在 TiKV 集群中,所以在这个过程中 tidb-server 需要和 TiKV 交互,获取数据。最后 tidb-server 需要将查询结果返回给用户。

4章 TiDB的调度

4.1 调度概述

4.1.1 为什么要进行调度

TiKV 集群是 TiDB 数据库的分布式 KV 存储引擎,数据以 Region 为单位进行复制和管理,每个 Region 会有多个 Replica(副本),这些 Replica 会分布在不同的 TiKV 节点上,其中 Leader 负责读/写,Follower 负责同步 Leader 发来的 raft log。了解了这些信息后,请思考下面这些问题:

  • 如何保证同一个 Region 的多个 Replica 分布在不同的节点上?更进一步,如果在一台机器上启动多个 TiKV 实例,会有什么问题?

  • TiKV 集群进行跨机房部署的时候,如何保证一个机房掉线,不会丢失 Raft Group 的多个 Replica?

  • 添加一个节点进入 TiKV 集群之后,如何将集群中其他节点上的数据搬过来?

  • 当一个节点掉线时,会出现什么问题?整个集群需要做什么事情?

    • ​ 从节点的恢复时间来看

      • ​ 如果节点只是短暂掉线(重启服务),如何处理?

      • ​ 如果节点是长时间掉线(磁盘故障,数据全部丢失),如何处理?

    • ​ 假设集群需要每个 Raft Group 有 N 个副本,从单个 Raft Group 的 Replica 个数来看

      • ​ Replica 数量不够(例如节点掉线,失去副本),如何处理?

      • ​ Replica 数量过多(例如掉线的节点又恢复正常,自动加入集群),如何处理?

  • 读/写都是通过 Leader 进行,如果 Leader 只集中在少量节点上,会对集群有什么影响?

  • 并不是所有的 Region 都被频繁的访问,可能访问热点只在少数几个 Region,这个时候我们需要做什么?

  • 集群在做负载均衡的时候,往往需要搬迁数据,这种数据的迁移会不会占用大量的网络带宽、磁盘 IO 以及 CPU,进而

  • 影响在线服务?

这些问题单独拿出可能都能找到简单的解决方案,但是混杂在一起,就不太好解决。有的问题貌似只需要考虑单个 Raft Group 内部的情况,比如根据副本数量是否充足来决定是否需要添加副本,但是实际上这个副本添加在哪里,是需要考虑全局信息的。同时整个系统也是在动态变化,Region 分裂、节点加入、节点失效、访问热点变化等情况会不断发生,整个调度系统也需要在动态中不断向最优状态前进,如果没有一个掌握全局信息,可以对全局进行调度,并且可以配置的组件,就很难满足这些需求。因此我们需要一个中心节点,来对系统的整体状况进行把控和调整,所以有了 PD 这个模块。

4.1.2 调度的需求

上面罗列了一大堆问题,我们先进行分类和整理。总体来看,问题有两大类:

作为一个分布式高可用存储系统,必须满足的需求,包括四种:

  • 副本数量不能多也不能少
  • 副本需要分布在不同的机器上
  • 新加节点后,可以将其他节点上的副本迁移过来
  • 自动下线失效节点,同时将该节点的数据迁移走

作为一个良好的分布式系统,需要优化的地方,包括:

  • 维持整个集群的 Leader 分布均匀
  • 维持每个节点的储存容量均匀
  • 维持访问热点分布均匀
  • 控制负载均衡的速度,避免影响在线服务
  • 管理节点状态,包括手动上线/下线节点

满足第一类需求后,整个系统将具备强大的容灾功能。满足第二类需求后,可以使得系统整体的负载更加均匀,管理更加容易方便。为了满足这些需求,首先我们需要收集足够的信息,比如每个节点的状态、每个 Raft Group 的信息、业务访问操作的统计等;其次需要设置一些策略,PD 根据这些信息以及调度的策略,制定出尽量满足前面所述需求的调度计划;最后需要一些基本的操作,来完成调度计划。

4.1.3 调度的基本操作

调度的基本操作指的是为了满足调度的策略,我们有哪些功能可以用。这是整个调度的基础,了解了手里有什么样的锤子,才知道用什么样的姿势去砸钉子。上述调度需求看似复杂,但是整理下来无非是下面三个操作:

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

刚好 Raft 协议通过 AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。

4.1.4 信息收集

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

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

TiKV 节点(Store)与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也会携带这个 Store 的状态信息,主要包括:

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

每个 Raft GroupLeader 会定期向 PD 汇报 Region 的状态信息

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

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

PD 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。除此之外,PD 还可以通过管理接口接受额外的信息,用来做更准确的决策。比如当某个 Store 的心跳包中断的时候,PD 并不能判断这个节点是临时失效还是永久失效,只能经过一段时间的等待(默认是 30 分钟),如果一直没有心跳包,就认为该 Store 已经下线,再决定需要将这个Store 上面的 Region 都调度走。但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。

4.1.5 调度的策略

PD 收集了这些信息后,还需要一些策略来制定具体的调度计划。

一个 Region 的 Replica 数量正确

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

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

一个 Raft Group 中的多个 Replica 不在同一个位置

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

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

这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的『容错单元』,我们希望这个单元内部不会存在一个Region 的多个 Replica。这个时候,可以给节点配置 labels 并且通过在 PD 上配置 location-labels 来指名哪些 label 是位置标识,需要在 Replica 分配的时候尽量保证一个 Region 的多个 Replica 不会分布在具有相同的位置标识的节点上。

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

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

Leader 数量在 Store 之间均匀分配

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

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

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

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

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

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

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

4.1.6 调度的实现

了解了上面这些信息后,接下来我们看一下整个调度的流程。

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

4.2 弹性调度

弹性调度(Elastic Schedule) 是 TiDB 在 4.0 的新特性,通过与云环境结合后提供的一系列调度策略,可以让 TiDB 具备自适应能力(Adaptive Capacity) ,即 TiDB 能根据用户的 workload 模式自动调节形态以达到资源的最大利用率。自适应能力是 TiDB 能够提供 DBaaS 服务的一项关键能力。

4.2.1 需求背景

传统上,我们一般将 TiDB 集群部署在 IDC 环境中,在这种情况下,用户通常希望各台机器的资源利率比较平均,并且各台机器需要预留足够的资源以应对高峰期,但大部分时间业务流量比较低且平均,机器的利用率相对于高峰期处在一个比较低的水平,造成了机器资源的浪费。而在云环境下,机器资源可以按需分配,并且云厂商能够支持秒级或分钟级交付,那么在平常的大部分时间里,就不需要让每台机器预留资源,而是应该尽可能地利用每台机器资源。当遇到资源利用高峰期时,可以临时扩容机器并且将一部分负载调度到新机器上,进而分散集群压力,保证性能稳定。如何在云上来实现弹性调度,这不仅需要让 TiDB 内核具备更灵活的处理方式,还需要结合 TiDB Operator 来让它在云上对业务进行自适应调节。目前 4.0 已经初步具备以下两个方面的功能:

  • 自动伸缩
  • 动态调度
4.2.2 自动伸缩

自动伸缩(Auto-Scale)包含两方面的内容,一是弹性扩缩容节点,二是在扩缩容节点后自动均衡集群负载。

和 Aurora 做法类似,弹性伸缩节点可通过对一些系统指标设置一个阈值,比如 CPU 利用率(TiDB Server 或 TiKV Server)、QPS(TiKV Server)等,当集群在平衡状态下目标指标等于或者超过阈值一段时间以后,就会自动触发水平的弹性伸缩。

  • TiDB 借助 TiDB Operator 和 PD 来实现 Auto-Scale:
  • TiDB Operator 通过 API 的方式暴露出期望的 TiDB / TiKV 节点数量
  • TiDB Operator 定期获取 TiDB / TiKV 的 metrics 信息和 PD 上的集群状态信息
  • TiDB Operator 通过内部的 Auto-Scaling 算法对 TidbCluster.Spec.Replicas 进行调整,从而实现 Auto-Scaling。

在 TiDB Operator 中,新增了 AutoScaler API 和 AutoScaler Controller,下面是一个 AutoScaler API 的例子:

apiVersion: pingcap.com/v1alpha1

kind: TidbClusterAutoScaler

metadata:

name: autoscaler

namespace: ela-demo

spec:

cluster:

name: ela-scheduling

namespace: ela-demo

metricsUrl: http://monitor-prometheus.ela-demo.svc:9090

tidb:

minReplicas: 8

maxReplicas: 8

scaleOutIntervalSeconds: 100

scaleInIntervalSeconds: 100

metricsTimeDuration: "1m"

metrics:

\- type: "Resource"

resource:

name: "cpu"

target:

type: "Utilization"

averageUtilization: 90

tikv:

minReplicas: 3

maxReplicas: 5

scaleOutIntervalSeconds: 100

scaleInIntervalSeconds: 100

metricsTimeDuration: "1m"

metrics:

\- type: "Resource"

resource:

name: "cpu"

target:

type: "Utilization"

averageUtilization: 70

其中:

  • minReplicas:最小实例数
  • maxReplicas:最大实例数
  • scaleOutIntervalSeconds:每次触发 scale-out 的间隔时间
  • scaleInIntervalSeconds: 每次触发 scale-in 的间隔时间

当集群扩缩容节点后,还需要进行快速的负载均衡。对于 TiDB 的负载均衡,需要客户端具备自动重新调整长连接的能力,使建立到 TiDB 上的连接能够重新均衡。而对于 TiKV,主要是通过 PD 发起对热点 Region 的动态调度,以达到快速分摊压力的目的,同时也能以最小的调度代价来提高弹性伸缩的速度。

4.2.3 动态调度

在上面提到了通过 TiDB Operator 扩缩容 TiKV 节点后,需要由 PD 来发起 Region 的热点调度,一般来说分为以下几种情况:

  1. 请求分布相对平均,区域广

  2. 请求分布相对平均,区域小

  3. 请求分布不平均,集中在多个点

  4. 请求分布不平均,集中在单个点

对于第一种情况,访问平均分布在集群的大部分 Region 中,目前调度不会对其做相关的特殊处理。对于第三种情况,现有的热点调度器已经能够识别并且对其进行调度。下面来介绍下对于第 2 种和第 4 种情况如何去做动态调整:

  1. 根据负载动态分裂 ( Load Base Splitting)

对于上述第二种情况,会出现小区域的热点问题。特别是在 TiDB 实践中经常遇到的热点小表问题,热点数据集中在几个Region 中,造成无法利用多台机器资源的情况。TiDB 4.0 中引入了根据负载动态分裂特性,即根据负载自动拆分 Region。其主要的思路借鉴了 CRDB 的实现,会根据设定的 QPS 阈值来进行自动的分裂。其主要原理是,若对该 Region 的请求 QPS 超过阈值则进行采样,对采样的请求分布进行判断。采样的方法是通过蓄水池采样出请求中的 20 个 key,然后统计请求在这些key 的左右区域的分布来进行判断,如果分布比较平均并能找到合适的 key 进行分裂,则自动地对该 Region 进行分裂。

  1. 热点隔离 (Isolate Frequently Access Region)

由于 TiKV 的分区是按 Range 切分的,在 TiDB 的实践中自增主建、递增的索引的写入等都会造成单一热点的情况,另外如果用户没有对 workload 进行分区,且访问是 non-uniform 的,也会造成单一热点问题。这些都是上述的第四种情况。根据过去的最佳实践经验,往往需要用户调整表结构,采用分区表,使用 shard_bits 等方式来使得单一分区变成多分区,才能进行负载均衡。而在云环境中,在用户不用调整 workload 或者表结构的情况下,TiDB 可以通过在云上弹性一个高性能的机器,并由 PD 通过识别自动将单一热点调度到该机器上,达到热点隔离的目的。该方法也特别适用于时事、新闻等突然出现爆发式业务热点的情况。

4.2.4 总结

布进行判断。采样的方法是通过蓄水池采样出请求中的 20 个 key,然后统计请求在这些key 的左右区域的分布来进行判断,如果分布比较平均并能找到合适的 key 进行分裂,则自动地对该 Region 进行分裂。

  1. 热点隔离 (Isolate Frequently Access Region)

由于 TiKV 的分区是按 Range 切分的,在 TiDB 的实践中自增主建、递增的索引的写入等都会造成单一热点的情况,另外如果用户没有对 workload 进行分区,且访问是 non-uniform 的,也会造成单一热点问题。这些都是上述的第四种情况。根据过去的最佳实践经验,往往需要用户调整表结构,采用分区表,使用 shard_bits 等方式来使得单一分区变成多分区,才能进行负载均衡。而在云环境中,在用户不用调整 workload 或者表结构的情况下,TiDB 可以通过在云上弹性一个高性能的机器,并由 PD 通过识别自动将单一热点调度到该机器上,达到热点隔离的目的。该方法也特别适用于时事、新闻等突然出现爆发式业务热点的情况。

4.2.4 总结

TiDB 4.0 是一个更加成熟,易用的版本,并且随着 TiDB Operator 的成熟以及 DBaaS 的推出,TiDB 4.0 开始成为一个拥抱云的版本。在云上,调度关注的视角也发生了改变,这使得让 TiDB 自适应 workload 去调整数据库形态变成了可能。后续弹性调度这一块, TiDB 还将有更多的玩法,比如 Follower Read 与多数据中心场景的结合,以及 TiFlash 大家族的加入。未来的TIDB,除了是一个 HTAP 的数据库,也会变成一个“智能”的数据库。

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值