PCTP 考试学习笔记之一:深入 TIDB 体系架构(上)
作者: OnTheRoad 原文来源:https://tidb.net/blog/f52b44cf
本系列学习笔记根据官方课程《TiDB 高级系统管理 [TiDB v5]》整理,感谢官方精心制作的视频课程。相关课程介绍,详见官方课程链接:https://learn.pingcap.com/learner/course/120005
1. 第一章 深入 TiDB 体系架构
1.1. TiDB 数据库架构概述
1.1.1. TiDB Server
TiDB Server 是 TiDB 数据库的计算节点,主要提供如下功能:
处理客户端的连接(MySQL 协议);
SQL 语句的解析、编译;
关系型数据与 KV 的转化;
SQL 语句的执行
在线 DDL 语句执行;
MVCC(多版本并发控制)垃圾回收;
【注意】
TiDB Server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储节点 TiKV(或 TiFlash)。
TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址,客户端的连接可以均匀地分布在多个 TiDB 实例上以达到负载均衡的效果。
1.1.2. TiKV
TiKV 是 TiDB 数据库的存储节点,主要提供如下功能:
数据持久化。
TiKV 在内部选择了基于 LSM-Tree 结构的 RocksDB 引擎(是由 Facebook 开源的一个非常优秀的单机 KV 存储引擎)作为数据存储引擎。可以简单的认为 RocksDB 是一个单机的持久化 Key-Value Map。
分布式事务支持。
TiKV 的事务采用的是 Google 在 BigTable 中使用的两阶段提交事务模型:Percolator。在每个 TiKV 节点上会单独分配存储锁的空间,称为 CF Lock。
副本的强一致性和高可用性。
采用 Multi-Raft 机制实现了多副本的强一致和高可用。TiKV 中存储的 Region 会自动分裂与合并,实现动态扩展,并借助 PD 节点可使 Region 在 TiKV 节点中灵活调度。
MVCC(多版本并发控制)。
Coprocessor(协同处理器)。
将 SQL 中的一部分计算利用协同处理器,下推到 TiKV 节点中完成(即算子下推)。充分利用 TiKV 节点的计算资源实现并行计算,从而减轻 TiDB Server 节点的计算压力。TiKV 节点将初步计算后的数据返回给 TiDB Server 节点,减少了网络带宽的占用。
1.1.3. PD(Placement Driver)
PD 是整个 TiDB 集群的控制中枢,是 TiDB 数据库的元信息管理模块。提供如下功能:
TiKV 的元数据存储(即 Region 分布),方便 TiDB 知道数据位于哪个 TiKV 节点;
集群整体拓扑结构的存储;
分配全局 ID 和事务 ID。
生成全局 TSO 时间戳;
收集集群信息进行调度;
提供 TiDB Dashboard 服务。
此外,PD 本身也是由至少 3 个节点构成,拥有高可用的能力。建议部署奇数个 PD 节点。
1.1.4. TiFlash
TiFlash 是 TIDB 数据库的列式存储节点,提供如下功能:
列式存储,提高分析查询效率;
支持强一致性和实时性
业务隔离,即 OLTP 与 OLAP 隔离
智能选择
TiFlash 节点以 Leaner 角色加入到 Multi-Raft 体系中,从而实现了对 TiKV 数据的准实时更新和强一致性。
1.2. TiDB Server
1.2.1. TiDB Server 架构
Protocol Layer、Parse、Compile
Protocol Layer 提供 MySQL 兼容的协议,处理客户端的连接;将接收到的 SQL 语句交由 Parse 模块进行解析后,并交由 Compile 模块进行编译,并生成执行计划。
Executor、DistSQL
Executor、DistSQL 负责 SQL 执行计划的执行;对于可直接返回结果的 SQL 语句(点查),由 Executor 和 KV 直接执行;而复杂的 SQL 语句由 Executor 交给 DistSQL 来执行。
Transaction
Transaction 则负责与事务相关的任务。
PD Client、TiKV Client
PD Client 负责与 PD 实例的交互;TiKV Client 则负责与 TiKV 实例的交互。
Schema load、Worker、Start job
schema load、worker、start job 则负责在线 ddl。
GC
GC 负责 MVCC 的垃圾回收
1.2.2. SQL 语句的解析和编译
1.2.2.1. SQL Parse 解析
如图 1.3 所示。首先,通过 lex(词法分析)将 SQL 语句解析成多个 token;再根据 token,通过 yacc(语法分析)生成抽象语法树(AST),以便于 compile 模块进行优化。
1.2.2.2. SQL Compile 编译
Compile 接收到 AST 后,如图 1.4 所示。首先根据元数据进行合法性验证(如对象是否存在),然后进行逻辑优化(即 SQL 语句优化,如等价改写),再结合统计信息进行物理优化(如是否走索引等),最后生成执行计划。
1.2.3. 关系型数据与 KV 的转化
1.2.3.1. 聚簇表的 KV 转化
聚簇表(Clustered Table)中,行数据的物理存储顺序与主键的存储顺序一致,访问主键时可直接获取到行记录。表的主键即为 KV 映射中 Key 的一部分。如图 1.5 所示,以 “编号” 列为主键的聚簇表为例,简单介绍关系性数据与 KV 的转化:
【知识补充】
聚簇表的特点:
表中行数据的物理存储顺序与主键的存储顺序一致;
通过主键访问行数据时,可以直接获取行数据,即只需 1 次 I/O;
创建方法:“CREATE TABLE t(a BIGINT PRIMARY KEY CLUSTERED , b VARCHAR(255);”
聚簇表(Clustered Table)在 TiDB 文档中又称聚簇索引表(index-organized tables),在 Oracle 中称为索引组织表(IOT,Index-Organized Tables)。
首先,提取聚簇表主键列“编号”为 KEY 值;
为了使 KEY 值在 TiDB 数据库中全局唯一,由 TiDB 自动为 KEY 追加“TableID”2 前缀;
TableID
与主键列共同构成 KEY 值,而表行的其它字段则成为该 KEY 值对应的 VALUE 值。如 KEY 值“T24_r1”对应的 VALUE 值为“Tom,1982-09-28,1390811212,78”。聚簇表的 KV 映射规则:
Key:tablePrefix{TableID}_recordPrefixSep{Col1}。即假设 Col1 为 Cluster Index,Key 为 TableID 与索引键组合而成。
Value:[col2,col3,col4]。即 Value 由行数据中除了主键列的其它列组成。
1.2.3.2. 非聚簇表 3 的 KV 转化
非聚簇表(Non-clustered Table)中,行数据的物理存储顺序与主键(主键本质上是唯一索引)的存储顺序无关。在对非聚簇表做 KV 转化时,TiDB 会隐式为每行数据生成 RowID(是一个 Int64 整型)。TableID 与 RowID 组合成 KV 键值对的 KEY 值。具体的 KV 转化流程(如图 1.6 所示)如下:
首先,TiDB 隐式为非聚簇表的每行生成一个 int64 类型的 RowID,并将该 RowID 作为行数据的 KEY 值;若表中含有整型列的主键,则 TiDB 默认将主键列作为每行的 RowID;
为了使 KEY 值在 TiDB 数据库中全局唯一,由 TiDB 自动为 KEY 追加“TableID”前缀;
TableID
与RowID
共同构成 KEY 值,而表行的所有字段都成为该 KEY 值对应的 VALUE 值。如 KEY 值 ‘’T24_xx1
’’ 对应的 VALUE 值为 “1,Tom,1982-09-28,1390811212,78
”。非聚簇表的 KV 映射规则:
Key:tablePrefix{
TableID
}_recordPrefixSep{_Tidb_RowID
}。即 Key 由 TableID 与 RowID 组合而成。Value:[
col1,col2,col3,col4
]。即 Value 为真实的行数据。
【知识补充】
TableID:TableID 在 TiDB 数据库中全局唯一,可理解为 Oracle 的 ObjectID 非聚簇表的特点:
表中行数据的物理存储顺序与主键(主键本质上是唯一索引)的存储顺序无关;
通过主键访问非聚簇表的行数据时,无法直接获取行数据,需要 2 次 I/O(第一次扫主键索引,获取数据行的 RowID;第二次通过 RowID 回表获取行数据);
创建方法:
CREATE TABLE t(a BIGINT PRIMARY KEY NONCLUSTERED ,b VARCHAR(255));
1.2.3.3. Region 分裂
Region 是 TiDB 数据存储管理的基本单位。多组连续的“KV 键值对”构成一个 Region,存储在 TiKV 实例的 RocksDB 中。
每个 Region 默认大小为 96MB。当一个 Region 达到 144MB 时,会自动分裂成两个 Region(如下图所示);
每个 Region 按左闭右开的区间划分数据存储范围,如 [a d)、[d g);
1.2.4. SQL 读写相关模块
如图 1.8 所示,SQL 读写相关模块的协作流程如下:
Executor 拿到执行计划后,根据 SQL 语句的类型,分别交给不同模块执行;
当 SQL 语句为简单的点查(PointGet,根据主键或索引,只查 1 行或 0 行),直接交给 KV 执行;
当 SQL 语句为复杂的查询(如多表连结、嵌套查询等),则交给 DistSQL 来执行;DistSQL 模块将多表查询拆分成多个单表查询的操作。
若 SQL 语句包含事务,则 Executor 将启动 Transaction 模块。Transaction 模块负责两阶段提交(PreWrite、Commit)和锁管理等。
Transaction 模块通过 PD Client 向 PD 节点申请全局事务 ID、TSO(全局时间戳)类型的事务开始时间戳 start_ts 和提交时间戳 commit_ts,再调用 KV 模块或 DistSQL 模块。
最终的数据读写,通过调用 TiKV Client 模块,来完成与 TiKV 节点的读写交互。
1.2.5. 在线 DDL 相关模块
如图 1.9 所示,在线 DDL 相关模块的协作流程如下:
start job 模块负责接受 ddl 请求,并将其存入 TiKV 节点的 job queue 队列中;
workers 模块负责读取 TiKV 节点的 job queue 队列,按序执行队列中的 ddl 操作,并将执行完毕的 ddl 存入 history queue 队列中;
schema load 模块负责加载 schema 元数据。
多个 TiDB Server 中的 start job 可同时接受多个 ddl 请求。但是,只有角色为 Owner 的 TiDBServer 中的 workers 模块可以执行 ddl。
同一时刻,只有一个 TiDB Server 角色为 Owner;Owner 角色定期在多个 TIDB Server 节点中轮换(重选举)。
成为 Owner 的 TiDB Server 节点,首先会通过 schema load 模块来加载 schema 元数据。
1.2.6. GC 机制与相关模块
TiDB 的事务的实现采用了 MVCC(多版本并发控制)机制,当新写入的数据覆盖旧数据时,旧数据不会被替换掉,而是与新写入的数据同时保留,并以时间戳来区分版本,GC 的任务便是周期性地清理不再需要的旧数据。
1.2.6.1. GC Leader
TiDB 集群会从众多 TiDB Server 实例中选举一个作为 GC Leader,而 GC 的工作由 GC Leader 中的 GC Worker 模块来处理 4。其他 TiDB Server 上的 GC Worker 是不工作的。
【知识补充】
GC Worker 模块:维护 GC 状态并向所有的 TiKV Region leader 发送 GC 命令。
【注意】
选举 GC Leader 的方式为 GC Worker 每分钟 Tick 时,如果发现没有 Leader 或 Leader 失效,就把自己写进去,成为 GC Leader。
1.2.6.2. GC 流程
每一轮 GC 分为以下三个步骤(串行执行):
Resolve Locks:该阶段会对所有 Region 扫描 Safepoint 5 之前的锁,并清理这些锁。
Delete Ranges:该阶段快速地删除由于 DROP TABLE/DROP INDEX 等操作产生的整区间(Region)的废弃数据。
Do GC:该阶段每个 TiKV 节点将会各自扫描该节点上的数据,并对每一个 key 删除其不再需要的旧版本。
【知识补充】
Safepoint:每次 GC 时,首先 TiDB 会计算一个称为 Safepoint 的时间戳,接下来 TiDB 会在保证 Safepoint 之后的快照全部拥有正确数据的前提下,删除更早的过期数据。
【注意】
如果一轮 GC 运行时间太久,上轮 GC 还在前两个阶段,下轮 GC 又开始了,下一轮 GC 会忽略,GC Leader 会报“there’s already a gc job running,skipped”:
默认配置下,GC 每 10 分钟触发一次,每次 GC 会保留最近 10 分钟内的数据(即默认 GC LifeTime 为 10 分钟)。
【注意】
为了使持续时间较长的事务能在超过 GC Life Time 之后仍然可以正常运行,Safepoint 不会大于正在执行中的事务的开始时间 (start_ts)。
1.2.6.3. GC 实现细节
1.2.6.3.1. Resolve Locks(清理锁)
TiDB 的事务是基于 Google Percolator 模型实现的,事务的提交是一个两阶段提交的过程。第一阶段完成时,所有涉及的 key 都会上锁,其中一个锁会被选为 Primary,其余的锁 ( Secondary ) 则会存储一个指向 Primary 的指针;第二阶段会将 Primary 锁所在的 key 加上一个 Write 记录,并去除锁。如果因为某些原因(如发生故障等),这些 Secondary 锁没有完成替换、残留了下来,那么也可以根据锁中的信息找到 Primary,并根据 Primary 是否提交来判断整个事务是否提交。但是,如果 Primary 的信息在 GC 中被删除了,而该事务又存在未成功提交的 Secondary 锁,那么就永远无法得知该锁是否可以提交。这样,数据的正确性就无法保证。Resolve Locks 这一步的任务即对 Safepoint 之前的锁进行清理。即如果一个锁对应的 Primary 已经提交,那么该锁也应该被提交;反之,则应该回滚。而如果 Primary 仍然是上锁的状态(没有提交也没有回滚),则应当将该事务视为超时失败而回滚。Resolve Locks 的执行方式是由 GC leader 对所有的 Region 发送请求扫描过期的锁,并对扫到的锁查询 Primary 的状态,再发送请求对其进行提交或回滚。从 3.0 版本开始,Resolve Locks 实现了并行,把所有 Region 分配给各个线程,所有线程并行的向各个 Region 的 Leader 发送请求:
并发线程数若 tikv_gc_auto_concurrency = 1,则每个 TiKV 自动一个线程。若 tikv_gc_auto_concurrency = 0,则由 tikv_gc_concurrency 决定总线程数,但每个 tikv 最多一个线程。
实际清锁的操作,是调用了 RocksDB 的 Delete ,RocksDB 的内部实现原理是写一个删除标记,需要等 RocksDB 执行 Compaction 回收空间,通常这步骤涉及的数据非常少。
1.2.6.3.2. Delete Ranges(删除区间)
在执行 Drop/Truncate Table ,Drop Index 等操作时,会有大量连续的数据被删除。如果对每个 key 都进行删除操作、再对每个 key 进行 GC 的话,那么执行效率和空间回收速度都可能非常的低下。事实上,这种时候 TiDB 并不会对每个 key 进行删除操作,而是将这些待删除的区间及删除操作的时间戳记录下来。Delete Ranges 会将这些时间戳在 Safepoint 之前的区间进行快速的物理删除,而普通 DML 的多版本不在这个阶段回收。
TiKV 默认使用 RocksDB 的 UnsafeDestroyRange 接口
Drop/Truncate Table ,Drop Index 会先把 Ranges 写进 TiDB 系统表(mysql.gc_delete_range),TiDB 的 GC worker 定期查看是否过了 Safepoint,然后拿出这些 Ranges,并发的给 TiKV 去删除 sst 文件,并发数和 concurrency 无关,而是直接发给各个 TiKV。删除是直接删除,不需要等 compact 。完成 Delete Ranges 后,会记录在 TiDB 系统表 mysql.gc_delete_range_done,表中的内容过 24 小时后会清除:
1.2.6.3.3. Do GC(进行 GC 清理)
此步操作主要是清理因 DML 操作而产生的过期版本数据。为了保证 Safepoint 之后的任何时间戳都具有一致的快照,这一步删除 Safepoint 之前提交的数据,但是会对每个 key 保留 Safepoint 前的最后一次写入(除非最后一次写入是删除)。
在进行这一步时,TiDB 只需将 Safepoint 发送给 PD,即可结束整轮 GC。TiKV 会每分钟自行检测是否 Safepoint 发生了更新,然后会对当前节点上所有 Region Leader 进行 GC。与此同时,GC Leader 可以继续触发下一轮 GC。详细流程如下:
调用 RocksDB 的 Delete 接口,打一个删除标记。
每一轮 GC 都会扫所有的 Region,但会根据 sst 上的元信息初步判断是否有较多的历史数据,进而来判断是否可以跳过。如果增量数据比较大,表示要打标记的老版本较多,会大幅增加耗时。
GC 打完标记后,不会立即释放空间,最终通过 RocksDB Compaction 来真正回收空间。
如果这时 TiKV 进程挂掉了,重启后,需要等下一轮 GC 开始继续。
并行度。3.0 开始,默认设置 tikv_gc_mode = distributed,无需 TiDB 通过对 TiKV 发送请求的方式来驱动,而是 TiDB 只需在每个 GC 周期发送 safepoint 到 PD 就可以结束整轮 GC,每台 TiKV 会自行去 PD 获取 safepoint 后分布式处理。
由于通常 Do GC 比较慢,下一轮 interval 到来时,上一轮 GC 还没有跑完。3.0 开始,如果没有执行完,下一轮 GC 将新的 safepoint 更新到 PD 后,TiKV 每隔 1 分钟到 PD 获取新的 safepoint,获取后会使用新的 safepoint 将剩余的 Region 完成扫描,并尽量回头完成 100% 的 Region (TiKV 会将执行到的 Region 位置在内存中,并按 Region 顺序扫描所有 Region )。
1.2.6.4. GC 相关的配置项
其中,tikv_gc_run_interval,tikv_gc_life_time,tikv_gc_auto_concurrency 这三条记录可以手动配置。其余带有 tikv_gc 前缀的记录为当前运行状态的记录,TiDB 会自动更新这些记录,请勿手动修改。
1.2.7. TiDB Server 的缓存
TiDB Server 缓存组成: SQL 结果、线程缓存、元数据、统计信息。
TiDB Server 缓存相关的参数:
mem-quota-query
单条 SQL 语句可以占用的最大内存阈值,单位为字节。该值作为系统变量 tidb_mem_quota_query 的初始值。超过该值的请求会被 oom-action 定义的行为所处理。
tidb_mem_quota_query
这个变量用来设置一条查询语句的内存使用阈值。如果一条查询语句执行过程中使用的内存空间超过该阈值,会触发 TiDB 启动配置文件中 OOMAc-tion 项所指定的行为。该变量的初始值由配置项 mem-quota-query 配置。
oom-action
当 TiDB 中单条 SQL 的内存使用超出 mem-quota-query 限制且不能再利用临时磁盘时的行为。目前合法的选项为”log”、”cancel”(默认值)。设置为”log” 时,仅输出日志。设置为”cancel” 时,取消执行该 SQL 操作,并输出日志。其他详细的配置参数,详见“第六章 TiDB 数据库系统优化”。
1.3. PD (Placement Driver)
1.3.1. PD(Placement Driver)架构
Placement Driver (简称 PD) 是 TiDB 集群数据库的总控节点,负责整个集群的调度,负责全局 ID 的生成,以及全局时间戳 TSO 的生成等。PD 还保存着整个集群 TiKV 的元信息,负责给 client 提供路由功能。
作为总控节点,PD 集群由多个(通常为 3 个)PD 实例构成,其中的 Leader 实例对外提供服务。PD 通过集成 etcd ,实现 PD 高可用和元数据存储,自动支持 auto failover,无需担心单点故障问题。同时,PD 通过 Raft 协议,保证了多个 PD 实例 etcd 数据的强一致性,不用担心数据丢失的问题。
在架构层面,PD 所有的数据都是通过 TiKV 主动上报获知。同时,PD 对整个 TiKV 集群的调度等操作,也只会在 TiKV 发送 Heartbeat 命令的结果里面返回相关的命令,让 TiKV 自行处理,而不是主动给 TiKV 发送命令。可以认为 PD 是一个无状态的服务。
PD(Placement Driver)主要功能
存储集群的元信息(metadata),即某个 Key 存储在哪个 TiKV 实例的哪个 Region;
分配全局 ID(如 tableid、indexid 等)和事务 ID
生成全局时间戳 TSO,如事务的开始时间与结束时间
收集集群信息进行调度,即收集各个 TiKV 实例中 Region 的分布情况,并对其进行调度。
提供 label 功能,实现相关的高可用,即通过 label,使 Region 可实现更合理的高可用隔离性。
提供 TiDB Dashboard 服务
1.3.2. 路由功能
PD 存储了整个集群 TiKV 的元数据(即 Region 的分布),因此可为 TiDB Server 的 Executor 提供 Region 的路由功能(如图 1.29 所示):
TiDB Server 实例的 Executor 执行器将执行计划的请求发送给 TiKV Client 模块;
TiKV Client 模块通过 PD Client 模块,到 PD Server 中获取 Region 的元数据信息;
PD Server 将 Region 的元数据信息,通过 TiDB Server 的 PD Client 模块返回给 TiKV Client 模块;
TiKV Client 模块根据接收到的 Region 元数据信息,到 TiKV 实例中获取数据。
为了减少与 PD Server 的网络交互,TiKV Client 将从 PD Server 获取的元数据信息缓存到本地 Region Cache 中,以便后续的读取请求可直接从 Region Cache 中获取该 Region 的元数据信息。当 TiKV 的 Region 的 Leader 发生切换或分裂,TiKV Client 还是按照 Region Cache 缓冲的元数据进行请求时,会发生二次请求 29,这种现象则成为 back off。导致 back off 的主要原因是 Region Cache 的信息过旧;back off 事件越多,读写的延迟就越高。
1.3.3. TSO 授时
TiDB 中的时间服务(TSO)由 PD 提供,使用的是中心式的混合逻辑时钟。其使用 64 位(int64)表示一个时间戳,其中低 18 位代表逻辑时钟(Logical Time)部分,剩余部分代表物理时钟(Physical Time)部分,其结构如下图 1.30 所示。物理时钟是当前 Unix Time 物理时钟的毫秒时间。由于逻辑时钟部分为 18 位,因此理论上,每毫秒内 PD 最多可以分配 218=262144 个时间戳(TSO)。
下面从“校时”、“授时”、“推进”三部分,对 TSO 进行讲解:
1.3.3.1. 校时
当新的 Leader 节点被选出时,其并不知道当前系统的时间已经推进到了哪里,因此首选要对 Leader 的时间进行校对。首先,新 Leader 会读取上一个 Leader 保存到 etcd 中的时间,这个时间是上一个 Leader 申请的物理时间(ms)的最大值 Tlast。通过读取 Tlast,便可知道上一个 Leader 分配的时间戳是小于(因为是预分配)Tlast 的。获得 Tlast 后,会将本地物理时间 Tnow 与 Tlast 进行比较,如果 Tnow - Tlast < 1ms,那么当前的物理时间 Tnext = Tlast + 1,否则 Tnext = Tnow。至此,校时完成。
1.3.3.2. 授时
TSO 的授时流程如图 1.31 所示。
TSO 请求者向 TiDB Server 实例的 PD Client 模块发送“TSO 请求”;
PD Client 将“TSO 请求”转发至 PD Leader;
PD 接收到“TSO 请求”后,因为无法立刻分配 TSO。于是,先为 PD Client 返回一个异步对象 tsFuture,表示 “我已经收到请求了,这个 tsFuture 你先拿着,待会儿给你分配 TSO,到时候你需用这个 tsFuture 来领取 TSO”;
PD Client 将 PD 分配的 tsFuture 转发给 TSO 请求者,TSO 请求者收到并存储 tsFuture。
PD 为 TSO 请求者分配 TSO(TSO 中会携带 tsFuture 信息),PD Client 将分配的 TSO 转交给 TSO 请求者;
TSO 请求者接收到 PD Client 发送过来的 TSO 后,将其中携带的 tsFuture 信息与自己在第 4 步中已收到的 tsFuture 相比对,以确保是分配给自己的 TSO。
为了保证当前 Leader 宕机后,新 Leader 能校时成功,需要在每次授时之后,都要对 Tlast 进行持久化,保存到 etcd 中。若每次授时之后,都持久化,性能会大大降低。PD 采取的优化策略是预申请一个可分配的“时间窗口 Tx”,默认 Tx = 3s。因此,在授时开始之前,PD Leader 首先将 Tlast = Tnext +Tx 存储到 etcd 中;然后,PD Leader 便可在内存中直接分配 [Tnext , Tnext + Tx) 之内的所有时间戳,避免了频繁写入 etcd 带来的性能问题。但是,如果 Leader crash,会浪费一些时间戳。
TSO 中的物理时钟部分便是校时之后的 Tlast,而逻辑时钟部分便随着请求而原子递增。如果逻辑时钟部分超过了最大值(218=262144),则会睡眠 50ms 来等待物理时间被推进,物理时间被推进后,如果有时间戳可以被分配,则会继续分配时间戳。
由于 TSO 请求是跨网络的,所以为减少网络开销,PD 的 TSO 服务支持批量请求时间戳。客户端可以一次申请 N 个时间戳,减少网络开销。
1.3.3.3. 推进
授时阶段,只能通过逻辑时钟部分自增来分配时间戳,当逻辑时钟部分到达上限(218=262144)后,则无法继续分配,则需要对物理时间进行推进。
PD 会每 50ms 检测当前的时钟,进行时钟推进。首先计算 jetLag = Tnow - Tlast,如果 jetLag >1ms,则说明混合逻辑时钟的物理时钟部分落后于物理时钟,则需要更新混合逻辑时钟的物理时钟部分:Tnext = Tnow。与此同时,为了防止授时阶段由于逻辑时钟达到阈值导致的等待,在推进阶段,当发现当前的逻辑时钟已经大于逻辑时钟的最大值的一半时,也会增加混合逻辑时钟的物理时钟部分。一旦混合逻辑时钟的物理时钟部分增长,则逻辑时钟部分会被重置为 0。
当 Tlast - Tnext <= 1ms 时,说明上次申请的时间窗口已经用完了,需要申请下一个时间窗口。此时,同样将 Tlast = Tnext + Tx 存储到 etcd 中,然后继续在新的时间窗口内进行时间分配。
中心式的解决方案实现简单,但是跨区域的性能损耗大,因此实际部署时,会将 PD 集群部署在同一个区域,避免跨区域的性能损耗;
1.3.4. 调度
宏观上看,PD 的调度流程主要包括如下 3 个部分(如图 1.32 所示):
信息收集 TiKV 节点周期性地主动向 PD 上报 StoreHeartbeat 和 RegionHeartbeat 两种心跳消息。其中,StoreHeartbeat 包含了 Store 的基本信息,容量,剩余空间,读写流量等数据,RegionHeartbeat 包含了 Region 的范围,副本分布,副本状态,数据量,读写流量等数据。注意,对于 RegionHeartbeat,只有 Leader peer 才会上报心跳,Follower peer 是不会上报的。PD 收到 StoreHeartbeat 和 RegionHeartbeat 后,首先将其缓存到 cache 中。若 PD 发现 Region 的 epoch 30 有变化,会将这个 Region 的信息也保存到 etcd 中。
生成调度
PD 中的调度器(Scheduler31)根据接收到的心跳信息,考虑各种限制和约束后生成待执行的 Operator。生成调度(Operator)时,主要关注如下这些点:
Balance(负载均衡调度):Region 负载均衡调度主要依赖 balance-leader-scheduler 和 balance-region-scheduler 这两个调度器,balance-leader-scheduler 目的是保持不同 TiKV 节点中 Leader 的均衡,分散读写压力;balance-region-scheduler 目的是保持不同 TiKV 节点中 Peer 的均衡,分散存储压力。
Hot Region(热点调度):热点调度对应的调度器是 hot-region-scheduler,保持不同 TiKV 节点读写热点 Region 的均衡。热点调度会尝试将热点 Region 打散成多个 Region,使其分布在不同的 TiKV 实例中。
集群拓扑感知:让 PD 感知不同节点分布的拓扑是为了通过调度使不同 Region 的各个副本尽可能分散,保证高可用和容灾。例如集群有 3 个数据中心,最安全的调度方式就是把 Region 的 3 个 Peer 分别放置在不同的数据中心,这样任意一个数据中心故障时,都能继续提供服务。PD 的 replicaChecker . 会在后台不断扫描所有 Region,当发现 Region 的分布不是当前的最优化状态时,会生成调度替换 Peer,将 Region 调整至最佳状态。
缩容及故障恢复:缩容是指预备将某个 Store(即 TiKV)下线,通过命令将该 Store 标记为 Offline 状态。此时,PD 通过 evict-leader-store-id 调度器,将此节点上的 Region 迁移至其他节点。故障恢复是指当有 Store 发生故障且无法恢复时,有 Peer 分布在对应 Store 上的 Region 会产生缺少副本的状况,此时 . 需要在其他节点上为这些 Region 补副本。二者处理过程基本一致,由 replicaChecker 检查到 Region 存在异常状态的 Peer,然后生成调度在健康的 Store 创建新的副本替换掉异常的 Peer。
Region Merge:Region merge 由 mergeChecker 负责,其过程与 replicaChecker 类似,也是在后台遍历,发现连续的小 Region 后发起调度,将相邻的小的 Region 进行合并(Merge)。
执行调度生成的 Operator 不会立即开始执行,而是首先会进入一个由 OperatorController 管理的一个等待队列。OperatorController 会根据配置以一定的并发,从等待队列中取出 Operator 进行执行。执行的过程就是依次把每个 Operator Step33 通过 Regionhearbeat Response 下发给对应 Region 的 Leader。让 TiKV 自己去处理,待处理完成之后,通过下一个 Heartbeat 重新上报,PD 就能知道是否调度成功
【补充知识】epoch:在 Region 的 epoch 里面,有 conf_ver 和 version,分别表示这个 Region 不同的版本状态。若一个 Region 发生了 membership changes,即新增或删除了 peer,conf_ver 会加 1,若 Region 发生了 split 或者 merge,则 version 加 1。
1.3.5. Label 与高可用
为了提升 TiDB 集群的高可用性和数据容灾能力,TiDB 推荐让 TiKV 节点尽可能在物理层面上分散,例如让 TiKV 节点分布在不同的机架甚至不同的机房。PD 调度器根据 TiKV 的拓扑信息,会自动在后台通过调度使得 Region 的各个副本尽可能隔离,从而使得数据容灾能力最大化。
而 PD 调度器,默认因无法感知 TiDB 集群的拓扑结构,只能保证一个 Raft Group 中的多个副本(Peer)分布在不同的 TiKV 实例中,而无法保证其分布在不同的机房或机柜中。如图 1.33 的调度示例中,当机柜 Rack4 故障后,则 Region3 将无法读取;当数据中心 DC1 故障后,则 Region2 将无法读取;只有 Region1,无论单个机柜或单个数据中心故障,都可正常提供服务。
可通过为 TiKV 设置 labels(用于描述集群的拓扑信息,特别是 TiKV 的位置),并将其上报给 PD。以便于 PD 调度器可通过 TiKV 上报的 labels 信息,感知到 TiDB 的拓扑结构,从而隔离各个 Region 副本,提高容灾能力。
1.3.5.1. 根据集群拓扑配置 labels
可根据集群拓扑配置 labels,配置流程如下:
设置 TiKV 的 labels 标签
TiKV 支持在命令行参数或者配置文件中以键值对的形式绑定一些属性,我们把这些属性叫做标签 (label)。TiKV 在启动后,会将自身的标签上报给 PD,因此我们可以使用标签来标识 TiKV 节点的地理位置。
比如集群的拓扑结构分成三层:机房 (zone) -> 机架 (rack) -> 主机 (host),就可以使用这 3 个标签来描述 TiKV 的位置。
使用命令行参数的方式启动一个 TiKV 实例:
使用配置文件的方式:
设置 PD 的 location-labels 配置
根据前面的描述,labels 标签可以是用来描述 TiKV 属性的任意键值对,但 PD 无从得知哪些 labels 标签是用来标识地理位置的,而且也无从得知这些标签的层次关系。因此,PD 也需要一些配置来使得 PD 可以理解 TiKV 节点的拓扑结构。
PD 上的配置叫做 location-labels,是一个字符串数组。该配置的每一项与 TiKV labels 的 key 是对应的,而且其中每个 key 的顺序代表不同标签的层级关系(从左到右,隔离级别依次递减)。
location-labels 没有默认值,你可以根据具体需求来设置该值,包括 zone、rack、host 等等。同时,location-labels 对标签级别的数量也没有限制,只要其级别与 TiKV 服务器的标签匹配,即可配置成功。
【注意】
必须同时配置 PD 的 location-labels 和 TiKV 的 labels 参数,否则 PD 不会根据拓扑结构进行调度。
如果你使用 Placement Rules in SQL,只需要配置 TiKV 的 labels 即可。Placement Rules in SQL 目前不兼容 PD location-labels 设置,会忽略该设置。不建议 location-labels 与 Placement Rules in SQL 混用,否则可能产生非预期的结果。
可以根据集群当前状态,来选择不同的配置方式:
在集群初始化之前,可以通过 PD 的配置文件进行配置:
在 PD 集群初始化完成后进行配置,则需要使用 pd-ctl 工具进行在线更改:
设置 PD 的 isolation-level 配置
在配置了 location-labels 的前提下,通过 isolation-level 配置来进一步加强对 TiKV 集群的拓扑隔离要求。假设按照上面的说明通过 location-labels 将集群的拓扑结构分成三层:机房 (zone) -> 机架 (rack) -> 主机 (host),并对 isolation-level 作如下配置:
当 PD 集群初始化完成后,需要使用 pd-ctl 工具进行在线更改:
其中,isolation-level 配置是一个字符串,需要与 location-labels 的其中一个 key 对应。该参数限制 TiKV 拓扑集群的最小且强制隔离级别要求,比如设置 isolation-level=”zone”,则可确保 Raft Group 中的多个副本分布在不同的 zone 区域中。
【注意】
isolation-level 默认情况下为空,即不进行强制隔离级别限制,若要对其进行设置,必须先配置 PD 的 location-labels 参数,同时保证 isolation-level 的值一定为 location-labels 中的一个。
1.3.5.2. 使用 TiUP 进行配置(推荐)
如果使用 TiUP 部署集群,可以在初始化配置文件中统一进行 location 相关配置。TiUP 会负责在部署时生成对应的 TiKV 和 PD 配置文件。
下面的例子定义了 zone/host 两层拓扑结构。集群的 TiKV 分布在三个 zone,每个 zone 内有两台主机,其中 z1 每台主机部署两个 TiKV 实例,z2 和 z3 每台主机部署 1 个实例。以下例子中 tikv-n 代表第 n 个 TiKV 节点的 IP 地址。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/998530ada5b0e781b8000c25b】。文章转载请联系作者。
评论