4.1.TiDB-SQL操作
(1)创建、查看和删除数据库
要创建一个名为 samp_db 的数据库,可使用以下语句:
CREATE DATABASE IF NOT EXISTS samp_db;
使用 SHOW DATABASES 语句查看数据库:
SHOW DATABASES;
使用 DROP DATABASE 语句删除数据库,例如:
DROP DATABASE samp_db;
再次查看数据库:
SHOW DATABASES;
(2)创建、查看和删除表
先创建一个库
CREATE DATABASE IF NOT EXISTS samp_db;
USE samp_db;
使用 SHOW TABLES 语句查看数据库中的所有表。例如:
SHOW TABLES FROM samp_db;
使用 CREATE TABLE 语句创建表。
如果表已存在,添加 IF NOT EXISTS 可防止发生错误:
CREATE TABLE IF NOT EXISTS person (
number INT(11),
name VARCHAR(255),
birthday DATE
);
使用 SHOW CREATE 语句查看建表语句。例如:
SHOW CREATE table person;
使用 SHOW FULL COLUMNS 语句查看表的列。 例如:
SHOW FULL COLUMNS FROM person;
使用 DROP TABLE 语句删除表。例如:
DROP TABLE person或者DROP TABLE IF EXISTS person;
(3)创建、查看和删除索引
先创建一张表:
CREATE TABLE IF NOT EXISTS person (
number INT(11),
name VARCHAR(255),
birthday DATE
);
对于值不唯一的列,可使用 CREATE INDEX 或 ALTER TABLE 语句。例如:
CREATE INDEX person_num ON person (number) 或者 ALTER TABLE person ADD INDEX person_num (number);
使用 SHOW INDEX 语句查看表内所有索引:
SHOW INDEX from person;
使用 ALTER TABLE 或 DROP INDEX 语句来删除索引。与 CREATE INDEX 语句类似,DROP INDEX 也可以嵌入 ALTER TABLE 语句。例如:
DROP INDEX person_num ON person;
ALTER TABLE person DROP INDEX person_num;
对于值唯一的列,可以创建唯一索引。例如:
CREATE UNIQUE INDEX person_num ON person (number) 或者 ALTER TABLE person ADD UNIQUE person_num (number);
(4)增删改查数据
使用 INSERT 语句向表内插入数据。例如:
INSERT INTO person VALUES(“1”,“tom”,“20170912”);
使用 SELECT 语句检索表内数据。例如:
SELECT * FROM person;
使用 UPDATE 语句修改表内数据。例如:
UPDATE person SET birthday=‘20200202’ WHERE name=‘tom’;
SELECT * FROM person;
使用 DELETE 语句删除表内数据:
DELETE FROM person WHERE number=1;
SELECT * FROM person;
(5)创建、授权和删除用户
使用 CREATE USER 语句创建一个用户 tiuser,密码为 123456:
CREATE USER ‘tiuser’@‘localhost’ IDENTIFIED BY ‘123456’;
授权用户 tiuser 可检索数据库 samp_db 内的表:
GRANT SELECT ON samp_db.* TO ‘tiuser’@‘localhost’;
查询用户 tiuser 的权限:
SHOW GRANTS for tiuser@localhost;
删除用户 tiuser:
DROP USER ‘tiuser’@‘localhost’;
查看所有权限
SHOW GRANTS;
4.2.TiDB-读取历史数据
(1)功能说明
TiDB 实现了通过标准 SQL 接口读取历史数据功能,无需特殊的 client 或者 driver。当数据被更新、删除后,依然可以通过 SQL 接口将更新/删除前的数据读取出来。
另外即使在更新数据之后,表结构发生了变化,TiDB 依旧能用旧的表结构将数据读取出来。
(2)操作流程
为支持读取历史版本数据, 引入了一个新的 system variable: tidb_snapshot ,这个变量是 Session 范围有效,可以通过标准的 Set 语句修改其值。其值为文本,能够存储 TSO 和日期时间。TSO 即是全局授时的时间戳,是从 PD 端获取的; 日期时间的格式可以为: “2020-10-08 16:45:26.999”,一般来说可以只写到秒,比如”2020-10-08 16:45:26”。 当这个变量被设置时,TiDB 会用这个时间戳建立 Snapshot(没有开销,只是创建数据结构),随后所有的 Select 操作都会在这个 Snapshot 上读取数据。
注意:
TiDB 的事务是通过 PD 进行全局授时,所以存储的数据版本也是以 PD 所授时间戳作为版本号。在生成 Snapshot 时,是以 tidb_snapshot 变量的值作为版本号,如果 TiDB Server 所在机器和 PD Server 所在机器的本地时间相差较大,需要以 PD 的时间为准。
当读取历史版本操作结束后,可以结束当前 Session 或者是通过 Set 语句将 tidb_snapshot 变量的值设为 “",即可读取最新版本的数据。
(3)历史数据保留策略
TiDB 使用 MVCC 管理版本,当更新/删除数据时,不会做真正的数据删除,只会添加一个新版本数据,所以可以保留历史数据。历史数据不会全部保留,超过一定时间的历史数据会被彻底删除,以减小空间占用以及避免历史版本过多引入的性能开销。
TiDB 使用周期性运行的 GC(Garbage Collection,垃圾回收)来进行清理,关于 GC 的详细介绍参见 TiDB 垃圾回收 (GC)。
这里需要重点关注的是 tikv_gc_life_time 和 tikv_gc_safe_point 这条。tikv_gc_life_time 用于配置历史版本保留时间,可以手动修改;tikv_gc_safe_point 记录了当前的 safePoint,用户可以安全地使用大于 safePoint 的时间戳创建 snapshot 读取历史版本。safePoint 在每次 GC 开始运行时自动更新。
(4)案例演示
查看当前表数据
记录时间
修改数据
设置历史时间戳,读取历史数据
4.3.TiDB整合Spark-TiSpark
(1)准备数据,向集群中插入样本数据
由于docker-compose安装已经帮我们整合好tispark组件,所以我们无须再次部署。
进入集群内部:docker-compose exec tispark-master bash
进入到tispark目录:cd /opt/spark/data/tispark-sample-data
执行脚本:mysql -h tidb -P 4000 -u root < dss.ddl
(2)启动Spark shell
先退出容器内部执行命令:docker-compose exec tispark-master /opt/spark/bin/spark-shell
(3)执行Spark代码
scala> import org.apache.spark.sql.TiContext ... scala> val ti = new TiContext(spark) ... scala> ti.tidbMapDatabase("TPCH_001") ... scala> spark.sql("select count(*) from lineitem").show +--------+ |count(1)| +--------+ | 60175| +--------+
也可以通过 Python 或 R 来访问 Spark: docker-compose exec tispark-master /opt/spark/bin/pyspark && docker-compose exec tispark-master /opt/spark/bin/sparkR
执行另一个复杂一点的 Spark SQL: scala> spark.sql( """select | l_returnflag, | l_linestatus, | sum(l_quantity) as sum_qty, | sum(l_extendedprice) as sum_base_price, | sum(l_extendedprice * (1 - l_discount)) as sum_disc_price, | sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge, | avg(l_quantity) as avg_qty, | avg(l_extendedprice) as avg_price, | avg(l_discount) as avg_disc, | count(*) as count_order |from | lineitem |where | l_shipdate <= date '1998-12-01' - interval '90' day |group by | l_returnflag, | l_linestatus |order by | l_returnflag, | l_linestatus """.stripMargin).show +------------+------------+---------+--------------+--------------+ |l_returnflag|l_linestatus| sum_qty|sum_base_price|sum_disc_price| +------------+------------+---------+--------------+--------------+ | A| F|380456.00| 532348211.65|505822441.4861| | N| F| 8971.00| 12384801.37| 11798257.2080| | N| O|742802.00| 1041502841.45|989737518.6346| | R| F|381449.00| 534594445.35|507996454.4067| +------------+------------+---------+--------------+--------------+ -----------------+---------+------------+--------+-----------+ sum_charge| avg_qty| avg_price|avg_disc|count_order| -----------------+---------+------------+--------+-----------+ 526165934.000839|25.575155|35785.709307|0.050081| 14876| 12282485.056933|25.778736|35588.509684|0.047759| 348| 1029418531.523350|25.454988|35691.129209|0.049931| 29181| 528524219.358903|25.597168|35874.006533|0.049828| 14902| -----------------+---------+------------+--------+-----------+
4.4.数据迁移-TiDB Lightning
(1)TiDB Lightning介绍
TiDB Lightning是一个将全量数据高速导入到TIDB集群的工具,目前支持Mydumper或CSV输出格式的数据源。你可以在以下两种场景下使用Lightning:
迅速导入大量新数据
备份恢复所有数据
TiDB Lightning主要包含两个部分:
tidb-lightning(“前端”):主要完成适配工作,通过读取数据源,在下游TiDB集群建表、将数据转换成键、值对(KV对)发送到tikv-importer、检查数据完整性等。
tikv-importer(“后端”):主要完成对数据导入TiKV集群的工作,把tidb-lightning写入的KV对缓存排序、切分并导入到TiKV集群。
(2)下载迁移工具
wget https://download.pingcap.org/tidb-enterprise-tools-latest-linux-amd64.tar.gz wget https://download.pingcap.org/tidb-toolkit-latest-linux-amd64.tar.gz
(3)准备mysql数据
5.TiDB技术原理
5.1.TiDB存储原理
(1)key-value
作为保存数据的系统,首先要决定的是数据的存储模型,就是数据以什么样的方式保存下来。TiKV的选择是Key-Value模型,并且提供有序遍历方法。简单来说,可以将TiDB看做一个巨大的Map,其中Key和Value都是原始的Byte数组,在这个Map中。Key按照Byte数组总的原始二进制比特位比较顺序排序。
对于TiDB的理解只需要记住两点:
这是一个巨大的Map,也就是存储的是Key-Value pair。
这个Map中的Key-Value pair按照Key的二进制顺序有序,也就是我们可以Seek到某一个Key的位置,然后不断的调用Next方法以递增的顺序获取比这个Key大的Key-Value。
(2)RocksDB
任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV也不例外,但是TiKV没有直接选择向磁盘上写数据,而是把数据保存在RocksDB中,具体的数据落地由RocksDB负责。这个选择的原因是开发一个单机存储引擎工作量很大,高性能的单机存储引擎。这里可以简单的认为RocksDB是一个单机的Key-Value Map。
底层LSM树将对数据的修改增量保存在内存中,达到指定大小后批量把数据flush到磁盘中。
(3)Raft
RocksDB保证单机的高性能,但是怎么保证数据的可靠性呢,Raft是一个一致性协议,提供几个重要功能:
Leader选举
成员变更
日志复制
TiKV利用Raft来做数据复制,每个数据变更都会落地为一条Raft日志,通过Raft的日志复制功能,将数据安全可靠的同步到Group的多数节点上。
RocksDB解决数据快速的存储在磁盘上,通过Raft,我们可以将数据复制到多台机器上,以防止单机失效。数据的写入是通过Raft这一层的接口写入,而不是直接写RocksDB。通过实现Raft,我们拥有一个分
(4)Region
TiKV相当于一个巨大的有序的KV Map,为了实现存储的水平扩展,我们需要将数据分散在多台机器上。Region分散有两种方案:一种是按照Key做Hash,根据Hash值选择对应的存储节点,另一种是分Range,某一段连续的Key都保存在一个存储节点上。TiKV选择了第二种方式,将整个key-value空间分成很多段,每一段是一些列连续的key,我们将每一段叫做一个Region,并且我们会尽量保持每个Region都可以用到StartKey到EndKey这样一个左闭右开的区间来描述。
将数据划分成Region后,我们将会做两件事:
以Region为单位,将数据分散在集群中所有节点上,并且尽量保证每个节点上服务的Region数量差不多。
以Region为单位做Raft的复制和成员管理。
先看第一点,数据按照Key切分成很多Region,每个Region的数据只会保存在一个节点上面。我们的系统会有一个组件将Region尽可能均匀的散布在及群众所有节点上,这样一方面实现了存储容量的水平扩展,另一方面也实现了负载均衡。通过任意一个key就能查到这个key在哪个Region中,以及这个Region目前在哪个节点上。
第二点,TiKV是以Region为单位做数据的复制,也就是一个Region的数据会保存多个副本,我们将一个副本叫做一个Replica。Replica之间是通过Raft来保持数据的一致,一个Region的多个Replica会保存在不同的节点上,构成一个Raft Group。其中一个Replica回座位这个Group的Leader,其他的Replica作为Follower。所有的读和写都是通过Leader进行,再由Leader复制给Follower。
我们以Region为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的KeyValue系统,不用担心数据存不下,或者磁盘故障丢失数据的问题。
(5)MVCC
很多数据库都会实现多版本控制(MVCC),TiKV也不例外。假设这样的场景,两个Client同事去修改一个Key,如果没有MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁的问题。TiKV的MVCC实现是通过在Key后面添加Version来实现。
注意,对于同一个Key的多个版本,我们把版本号较大的放在前面,版本号小的放在后面,这样当用户通过一个Key+Version来获取value的时候,可以将Key和Version构造出MVCC的Key,也就是Key-Version。然后可以直接Seek(Key-Version),定位到第一个大于等于这个Key-Version的位置。
(6)事务
TiKV的事务采用的是Percolator模型,并且做了大量的优化。TiKV的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方面尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,多个客户端修改少量行,导致冲突严重,造成大量的无效重试。
5.2.TiDB计算原理
(1)关系模型到 Key-Value 模型的映射
我们将关系模型简单的理解为Table和SQL语句,那么问题变为如何在KV结构上保存Table以及如何在KV结构上运行SQL语句。
SQL和KV结构之间存在巨大的区别,那么如何能够方便高效的进行映射,就成为一个很重要的问题。一个好的映射方案必须有利于对数据操作的需求。
对于一个Table来说,需要存储的数据包括三部分:
表的元数据
Table中的Row
索引数据
对于Row可以选择行存或者列存,这两种各有优缺点。TiDB面向的首要目标是OLTP业务,这类业务需要快速地读取、保存、修改、删除一行数据,所以采用行存是比较合适的。
对于Index,TiDB不止需要支持Primary Index,还需要支持Secondary Index。Index的作用的辅助查询,提升查询性能,以及保证某些Constraint。
查询的方式分为两种:点查和范围查询。
一个全局有序的分布式Key-value引擎。全局有序这一点重要,可以帮助我们解决不少问题。比如对于快速获取一行数据,假设我们能够构造出某一个或者几个Key,定位这一行,我们就利用TiKV提供的Seek方法快速定位到这一行数据所在的位置。在比如对于扫描全表的需求,如果能够映射为一个Key的Range,从StartKey扫描到EndKey,那么就可以简单的通过这种方式获取全表数据。操作Index数据也是类似的思路。
TiDB对每一个表分配一个TableID,每一个索引都会分配一个IndexID,每一行分配一个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}_indexedColumsValue
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
假设表中有 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
(2)原信息管理
Database/Table都有元信息,也就是其定义以及各项属性,这些信息也需要持久化,我们也将这些信息存储在TiKV中。每个Database/Table都被分配一个唯一的ID,这个ID作为唯一标识,并且在编码为Key-Value时,这个ID都会编码到Key中,再加上m_前缀。这样就可以构造出一个Key,Value中存储的是序列化后的元信息。
除此之外,还有一个专门的Key-Value存储当前Schema版本信息。TiDB使用Google F1的Online Schema变更算法,有一个后台线程不断地检查TiKV上面存储的Schema版本是否发生变化,并且保证在一定时间内一定能够获取版本的变化。
(3)SQL on KV 架构
TiKV Cluster主要作用是作为KV引擎存储数据,这里主要介绍SQL层,也就是TiDB Servers这一层,这一层的节点都是无状态的节点,本身并不存储数据,节点之间完全对等。TiDB Server这一层最重要的工作是处理用户请求,执行SQL运算逻辑。
(3)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 的很好,原因是显而易见的:
在扫描数据的时候,每一行都要通过 KV 操作同 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大
并不是所有的行都有用,如果不满足条件,其实可以不读取出来
符合要求的行的值并没有什么意义,实际上这里只需要有几行数据这个信息就行
(4)分布式 SQL 运算
如何避免上述缺陷也是显而易见的,首先我们需要将计算尽量靠近存储节点,以避免大量的 RPC 调用。其次,我们需要将 Filter 也下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。最后,我们可以将聚合函数、GroupBy 也下推到存储节点,进行预聚合,每个节点只需要返回一个 Count 值即可,再由 tidb-server 将 Count 值 Sum 起来。
这里有一个数据逐层返回的示意图:
(5)SQL 层架构
上面几节简要介绍了 SQL 层的一些功能,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL 层要复杂的多,模块以及层次非常多,下面这个图列出了重要的模块以及调用关系:
用户的 SQL 请求会直接或者通过 Load Balancer 发送到 tidb-server,tidb-server 会解析 MySQL Protocol Packet,获取请求内容,然后做语法解析、查询计划制定和优化、执行查询计划获取和处理数据。
数据全部存储在 TiKV 集群中,所以在这个过程中 tidb-server 需要和 tikv-server 交互,获取数据。
最后 tidb-server 需要将查询结果返回给用户。
5.3.TiDB调度原理
(1)调度的基本操作
调度无非是下面三件事:
增加一个 Replica
删除一个 Replica
将 Leader 角色在一个 Raft Group 的不同 Replica 之间 transfer
刚好 Raft 协议能够满足这三种需求,通过 AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。
(2)信息收集
调度依赖于整个集群信息的收集,简单来说,我们需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息:
每个 TiKV 节点会定期向 PD 汇报节点的整体信息
TiKV 节点(Store)与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也会携带这个 Store 的状态信息,主要包括:
总磁盘容量
可用磁盘容量
承载的 Region 数量
数据写入速度
发送/接受的 Snapshot 数量(Replica 之间可能会通过 Snapshot 同步数据)
是否过载
标签信息(标签是具备层级关系的一系列 Tag)
每个 Raft Group 的 Leader 会定期向 PD 汇报信息
每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 Region 的状态,主要包括下面几点信息:
Leader 的位置
Followers 的位置
掉线 Replica 的个数
数据写入/读取的速度
PD 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。除此之外,PD 还可以通过管理接口接受额外的信息,用来做更准确的决策。比如当某个 Store 的心跳包中断的时候,PD 并不能判断这个节点是临时失效还是永久失效,只能经过一段时间的等待(默认是 30 分钟),如果一直没有心跳包,就认为是 Store 已经下线,再决定需要将这个 Store 上面的 Region 都调度走。但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知 PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。
(3)调度的策略
PD 收集了这些信息后,还需要一些策略来制定具体的调度计划。
1.一个 Region的 Replica数量正确
当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的 Replica 数量不满足要求时,需要通过 Add/Remove Replica 操作调整 Replica 数量。出现这种情况的可能原因是:
某个节点掉线,上面的数据全部丢失,导致一些 Region 的 Replica 数量不足
某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了 Replica 的 Region 的 Replica 数量多过,需要删除某个 Replica
管理员调整了副本策略,修改了 max-replicas 的配置
2.一个 Raft Group中的多个 Replica不在同一个位置
注意第二点,『一个 Raft Group 中的多个 Replica 不在同一个位置』,这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个 Replica 不落在一个节点上,以避免单个节点失效导致多个 Replica 丢失。在实际部署中,还可能出现下面这些需求:
多个节点部署在同一台物理机器上
TiKV 节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性
TiKV 节点分布在多个 IDC 中,希望单个机房掉电时,也能保证系统可用
这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的容错单元,我们希望这个单元内部不会存在一个 Region 的多个 Replica。这个时候,可以给节点配置 lables 并且通过在 PD 上配置 location-labels 来指明哪些 lable 是位置标识,需要在 Replica 分配的时候尽量保证不会有一个 Region 的多个 Replica 所在结点有相同的位置标识。
3.副本在 Store 之间的分布均匀分配
前面说过,每个副本中存储的数据容量上限是固定的,所以我们维持每个节点上面,副本数量的均衡,会使得总体的负载更均衡。
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-ctl 手动加快调度速度。
8.支持手动下线节点
当通过 pd-ctl 手动下线节点后,PD 会在一定的速率控制下,将节点上的数据调度走。当调度完成后,就会将这个节点置为下线状态。
(4)调度的实现
了解了上面这些信息后,接下来我们看一下整个调度的流程。
PD 不断的通过 Store 或者 Leader 的心跳包收集信息,获得整个集群的详细数据,并且根据这些信息以及调度策略生成调度操作序列,每次收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 自己根据当前自身状态来定。