tidb架构

注:本文东拼西凑来源于tidb官方,仅供学习使用

 

Hello,欢迎来到tidb的世界!

 

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

官方文档地址: https://www.pingcap.com/docs-cn/overview/

Github项目地址: https://github.com/pingcap

 

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

 

: tidb最大的对手是cockroachDB,都是源于googleF1论文,基于leveldb,设计很相似,可以对比学习,附上cockroachDB官网,https://www.cockroachlabs.com/

 

TiDB 架构

Tidb的架构如图1-1

tidb架构

                          1-1 tidb架构图解

TiDB 集群主要包括三个核心组件:TiDB ServerPD Server TiKV Server。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件。

 

TiDB Server

TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVSHAProxy F5)对外提供统一的接入地址。

 

PD Server

Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 IDPD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署 3 个节点。

 

TiKV Server

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

 

TiSpark

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

 

TiDB 具备如下特性:

  1. 高度兼容 MySQL: 支持mysql协议,目前仅支持mysql协议。
  2. 水平弹性扩展: 包括计算能力和存储能力。TiDB Server 负责处理 SQL 请求,随着业务的增长,可以简单的添加 TiDB Server 节点,提高整体的处理能力,提供更高的吞吐。TiKV 负责存储数据,随着数据量的增长,可以部署更多的 TiKV Server 节点解决数据 Scale 的问题。PD 会在 TiKV 节点之间以 Region 为单位做调度,将部分数据迁移到新加的节点上。所以在业务的早期,可以只部署少量的服务实例(推荐至少部署 3 TiKV 3 PD2 TiDB),随着业务量的增长,按照需求添加 TiKV 或者 TiDB 实例。计算节点和存储节点分离,单独部署。
  3. 分布式事务 : 支持ACID事务
  4. 高可用: 高可用是 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 节点上。

 

技术介绍

TIKV

TIKV 使用 Key-Value 模型,并且提供有序遍历方法。TiKV 会将数据存储到 RocksDB

TIKV 的项目源代码:https://github.com/tikv/tikv

TIKVRaft

Tikvraft的实现,如图1-2所示,这里每个store表示一个tikv节点,而tikv的数据最小存储单位为region,将数据按照key的范围打散为多个region,并且尽量保证每个节点上服务的 Region 数量差不多(PD实现调度),以 Region 为单位做 Raft 的复制和成员管理。

tidb架构

1-2  tikv raft实现

如图1-2所示一个raft group里有三个region(此处是三副本模式,也可以5)raft协议强烈建议看下http://thesecretlivesofdata.com/raft/,由于tikv是强一致性的数据库,采用的是主读主写,这里的主指的是由region组成raft groupleader,即每个store里都有存在leader,再由 Leader 复制给 Follower

tidb架构

1-3 raft实现

Raft实现如下:

TiKV 使用 Raft 一致性算法来保证数据的安全,默认提供的是三个副本支持,这三个副本形成了一个 Raft Group

Client 需要写入某个数据的时候,Client 会将操作发送给 Raft Leader,这个在 TiKV 里面我们叫做 ProposeLeader 会将操作编码成一个 entry,写入到自己的 Raft Log 里面,这个我们叫做 Append

Leader 也会通过 Raft 算法将 entry 复制到其他的 Follower 上面,这个我们叫做 ReplicateFollower 收到这个 entry 之后也会同样进行 Append 操作,顺带告诉 Leader Append 成功。

Leader 发现这个 entry 已经被大多数节点 Append,就认为这个 entry 已经是 Committed 的了,然后就可以将 entry 里面的操作解码出来,执行并且应用到状态机里面,这个我们叫做 Apply

TiKV 里面,提供了 Lease Read,对于 Read 请求,会直接发给 Leader,如果 Leader 确定自己的 lease 没有过期,那么就会直接提供 Read 服务,这样就不用走一次 Raft 了。如果 Leader 发现 lease 过期了,就会强制走一次 Raft 进行续租,然后在提供 Read 服务。

TIKVraft的优化

Batch and Pipeline

使用 batch 能明显提升性能,譬如对于 RocksDB 的写入来说,我们通常不会每次写入一个值,而是会用一个 WriteBatch 缓存一批修改,然后在整个写入。 对于 Raft 来说,Leader 可以一次收集多个 requests,然后一批发送给 Follower。当然,我们也需要有一个最大发送 size 来限制每次最多可以发送多少数据。

 

如果只是用 batchLeader 还是需要等待 Follower 返回才能继续后面的流程,我们这里还可以使用 Pipeline 来进行加速。大家知道,Leader 会维护一个 NextIndex 的变量来表示下一个给 Follower 发送的 log 位置,通常情况下面,只要 Leader Follower 建立起了连接,我们都会认为网络是稳定互通的。所以当 Leader Follower 发送了一批 log 之后,它可以直接更新 NextIndex,并且立刻发送后面的 log,不需要等待 Follower 的返回。如果网络出现了错误,或者 Follower 返回一些错误,Leader 就需要重新调整 NextIndex,然后重新发送 log 了。

 

Append Log Parallelly

对于上面提到的一次 request 简易 Raft 流程来说, Leader 可以先并行的将 log 发送给 Followers,然后再将 log append。为什么可以这么做,主要是因为在 Raft 里面,如果一个 log 被大多数的节点append,我们就可以认为这个 log 是被 committed 了,所以即使 Leader 再给 Follower 发送 log 之后,自己 append log 失败 panic 了,只要 N / 2 + 1 Follower 能接收到这个 log 并成功 append,我们仍然可以认为这个 log 是被 committed 了,被 committed log 后续就一定能被成功 apply

 

那为什么我们要这么做呢?主要是因为 append log 会涉及到落盘,有开销,所以我们完全可以在 Leader 落盘的同时让 Follower 也尽快的收到 log append

 

这里我们还需要注意,虽然 Leader 能在 append log 之前给 Follower log,但是 Follower 却不能在 append log 之前告诉 Leader 已经成功 append 这个 log。如果 Follower 提前告诉 Leader 说已经成功 append,但实际后面 append log 的时候失败了,Leader 仍然会认为这个 log 是被 committed 了,这样系统就有丢失数据的风险了。

 

Asynchronous Apply

上面提到,当一个 log 被大部分节点 append 之后,我们就可以认为这个 log committed 了,被 committed log 在什么时候被 apply 都不会再影响数据的一致性。所以当一个 log committed 之后,我们可以用另一个线程去异步的 apply 这个 log

 

所以整个 Raft 流程就可以变成:

Leader 接受一个 client 发送的 request

Leader 将对应的 log 发送给其他 follower 并本地 append

Leader 继续接受其他 client requests,持续进行步骤 2

Leader 发现 log 已经被 committed,在另一个线程 apply

Leader 异步 apply log 之后,返回结果给对应的 client

使用 asychronous apply 的好处在于我们现在可以完全的并行处理 append log apply log,虽然对于一个 client 来说,它的一次 request 仍然要走完完整的 Raft 流程,但对于多个 clients 来说,整体的并发和吞吐量是上去了。

 

SST Snapshot

Raft 里面,如果 Follower 落后 Leader 太多,Leader 就可能会给 Follower 直接发送 snapshot。在 TiKVPD 也有时候会直接将一个 Raft Group 里面的一些副本调度到其他机器上面。上面这些都会涉及到 Snapshot 的处理。

在现在的实现中,一个 Snapshot 流程是这样的:

Leader scan 一个 region 的所有数据,生成一个 snapshot file

Leader 发送 snapshot file Follower

Follower 接受到 snapshot file,读取,并且分批次的写入到 RocksDB

如果一个节点上面同时有多个 Raft Group Follower 在处理 snapshot fileRocksDB 的写入压力会非常的大,然后极易引起 RocksDB 因为 compaction 处理不过来导致的整体写入 slow 或者 stall

幸运的是,RocksDB 提供了 SST 机制,我们可以直接生成一个 SST snapshot file,然后 Follower 通过 injest 接口直接将 SST file load 进入 RocksDB

 

Asynchronous Lease Read

TiKV 使用 ReadIndex Lease Read 优化了 Raft Read 操作,但这两个操作现在仍然是在 Raft 自己线程里面处理的,也就是跟 Raft append log 流程在一个线程。无论 append log 写入 RocksDB 有多么的快,这个流程仍然会 delay Lease Read 操作。

使用一个线程异步实现 Lease Read。也就是我们会将 Leader Lease 的判断移到另一个线程异步进行,Raft 这边的线程会定期的通过消息去更新 Lease,这样我们就能保证 Raft write 流程不会影响到 read

 

事务的Percolator模型

对于同一个 Region 来说,通过 Raft 一致性协议,我们能保证里面的 key 操作的一致性,但如果我们要同时操作多个数据,而这些数据落在不同的 Region 上面,为了保证操作的一致性,我们就需要分布式事务。

譬如我们需要同时将 a = 1b = 2 修改成功,而 a b 属于不同的 Region,那么当操作结束之后,一定只能出现 a b 要么都修改成功,要么都没有修改成功,不能出现 a 修改了,但 b 没有修改,或者 b 修改了,a 没有修改这样的情况。

最通常的分布式事务的做法就是使用 two-phase commit,也就是俗称的 2PC,但传统的 2PC 需要有一个协调者,而我们也需要有机制来保证协调者的高可用。这里,TiKV 参考了 Google Percolator,对 2PC 进行了优化,来提供分布式事务支持。

Percolator 的原理是比较复杂的,需要关注几点:

首先,Percolator 需要一个服务 timestamp oracle (TSO) 来分配全局的 timestamp,这个 timestamp 是按照时间单调递增的,而且全局唯一。任何事务在开始的时候会先拿一个 start timestamp (startTS),然后在事务提交的时候会拿一个 commit timestamp (commitTS)。时间戳均由PD统一分配,由于PD是个集群,主节点读写,保证了时间戳的唯一性。

Percolator 提供三个 column family (CF)LockData Write,当写入一个 key-value 的时候,会将这个 key lock 放到 Lock CF 里面,会将实际的 value 放到 Data CF 里面,如果这次写入 commit 成功,则会将对应的 commit 信息放到入 Write CF 里面。

Key Data CF Write CF 里面存放的时候,会把对应的时间戳给加到 Key 的后面。在 Data CF 里面,添加的是 startTS,而在 Write CF 里面,则是 commitCF

假设我们需要写入 a = 1,首先从 TSO 上面拿到一个 startTS,譬如 10,然后我们进入 Percolator PreWrite 阶段,在 Lock Data CF 上面写入数据,如下:

Lock CF: W a = lock

Data CF: W a_10 = value

后面我们会用 W 表示 WriteR 表示 Read D 表示 DeleteS 表示 Seek

PreWrite 成功之后,就会进入 Commit 阶段,会从 TSO 拿一个 commitTS,譬如 11,然后写入:

Lock CF: D a

Write CF: W a_11 = 10

Commit 成功之后,对于一个 key-value 来说,它就会在 Data CF Write CF 里面都有记录,在 Data CF 里面会记录实际的数据, Write CF 里面则会记录对应的 startTS

当我们要读取数据的时候,也会先从 TSO 拿到一个 startTS,譬如 12,然后进行读:

 

Lock CF: R a

Write CF: S a_12 -> a_11 = 10

Data CF: R a_10

Read 流程里面,首先我们看 Lock CF 里面是否有 lock,如果有,那么读取就失败了。如果没有,我们就会在 Write CF 里面 seek 最新的一个提交版本,这里我们会找到 11,然后拿到对应的 startTS,这里就是 10,然后将 key startTS 组合在 Data CF 里面读取对应的数据。

注:tidb使用乐观锁即在commit阶段判定事务是否冲突,当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,就是计数器,多个客户端同时修改少量行,导致冲突严重的,造成大量的无效重试。mysql默认使用悲观锁,事务开始时进行加锁。

 

 

MVCC

对于同一个 Key 的多个版本,我们把版本号较大的放在前面,版本号小的放在后面。这样当用户通过一个 Key + Version 来获取 Value 的时候,可以将 Key Version 构造出 MVCC Key,也就是 Key-Version。然后可以直接 Seek(Key-Version),定位到第一个大于等于这个 Key-Version 的位置。

 

TIKV RocksDB

RocksDB 是一个 key-value 存储系统,所以对于 TiKV 来说,任何的数据都最终会转换成一个或者多个 key-value 存放到 RocksDB 里面。 一个 TiKV 会有多个 Regions,我们在 Raft RocksDB 里面会使用 Region ID 作为 key 的前缀,然后再带上 Raft Log ID 来唯一标识一条 Raft Log。譬如,假设现在有两个 RegionID 分别为 12,那么 Raft Log RocksDB 里面类似如下存放:

1_1 -> Log {a = 1}

1_2 -> Log {a = 2}

1_N -> Log {a = N}

2_1 -> Log {b = 2}

2_2 -> Log {b = 3}

2_N -> Log {b = N}

因为我们是按照 range key 进行的切分,那么在 KV RocksDB 里面,我们直接使用 key 来进行保存,类似如下:

a -> N

b -> N

里面存放了两个 keya b,但并没有使用任何前缀进行区分。

RocksDB 支持 Column Family,所以能直接跟 Percolator 里面的 CF 对应,在 TiKV 里面,我们在 RocksDB 使用 Default CF 直接对应 Percolator Data CF,另外使用了相同名字的 Lock Write

 

TIDB

数据在tikv上是使用KV存储的,那sql是如何转化为kv的。是在tidb层实现的。

对于一个 Table 来说,需要存储的数据包括三部分:表的元信息、Table 中的 Row、索引数据。对于 Insert 语句,需要将 Row 写入 KV,并且建立好索引数据。对于 Update 语句,需要将 Row 更新的同时,更新索引数据(如果有必要)。对于 Delete 语句,需要在删除 Row 的同时,将索引也删除。

TiDB 对每个表分配一个 TableID,每一个索引都会分配一个 IndexID,每一行分配一个 RowID(如果表有整数型的 Primary Key,那么会用 Primary Key 的值当做 RowID),其中 TableID 在整个集群内唯一,IndexID/RowID 在表内唯一,这些 ID 都是 int64 类型。 每行数据按照如下规则进行编码成 Key-Value pair

         Key tablePrefix_rowPrefix_tableID_rowID

         Value: [col1, col2, col3, col4]

其中 Key tablePrefix/rowPrefix 都是特定的字符串常量,用于在 KV 空间内区分其他数据。 对于 Index 数据,会按照如下规则编码成 Key-Value pair

         Key: tablePrefix_idxPrefix_tableID_indexID_indexColumnsValue

         Value: rowID

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

 

         Key: tablePrefix_idxPrefix_tableID_indexID_ColumnsValue_rowID

         Valuenull

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

         var(

                   tablePrefix     = []byte{'t'}

                   recordPrefixSep = []byte("_r")

                   indexPrefixSep  = []byte("_i")

         )

一个 Table 内部所有的 Row 都有相同的前缀,一个 Index 的数据也都有相同的前缀。这样具体相同的前缀的数据,在 TiKV Key 空间内,是排列在一起。同时只要我们小心地设计后缀部分的编码方案,保证编码前和编码后的比较关系不变,那么就可以将 Row 或者 Index 数据有序地保存在 TiKV 中。TIKVkey是按照rangeregion的,还记的吗?

例子:假设表中有 3 行数据:

TiDB”, “SQL Layer”, 10

TiKV”, “KV Engine”, 20

PD”, “Manager”, 30

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

         t_r_10_1 --> ["TiDB", "SQL Layer", 10]

         t_r_10_2 --> ["TiKV", "KV Engine", 20]

         t_r_10_3 --> ["PD", "Manager", 30]

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

         t_i_10_1_10_1 --> null

         t_i_10_1_20_2 --> null

         t_i_10_1_30_3 --> null

 

元信息管理

Database/Table 都有元信息,也就是其定义以及各项属性,这些信息也需要持久化,我们也将这些信息存储在 TiKV 中。每个 Database/Table 都被分配了一个唯一的 ID,这个 ID 作为唯一标识,并且在编码为 Key-Value 时,这个 ID 都会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 KeyValue 中存储的是序列化后的元信息。 除此之外,还有一个专门的 Key-Value 存储当前 Schema 信息的版本。TiDB 使用 Google F1 Online Schema 变更算法,有一个后台线程在不断的检查 TiKV 上面存储的 Schema 版本是否发生变化,并且保证在一定时间内一定能够获取版本的变化(如果确实发生了变化)。变更流程:https://github.com/ngaut/builddatabase/blob/master/f1/schema-change-implement.md

 

 

tidb架构

1-4 tidbSQL

SQL 运算

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

 

PD

调度的基本操作: 增加一个 Replica/删除一个 Replica/ Leader 角色在一个 Raft Group 的不同 Replica 之间 transfer

刚好 Raft 协议能够满足这三种需求,通过 AddReplicaRemoveReplicaTransferLeader 这三个命令,可以支撑上述三种基本操作。

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

 

 

信息收集

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

  1. 每个 TiKV 节点会定期向 PD 汇报节点的整体信息
  2. 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 都调度走。

 

调度的策略

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

一个 Region Replica 数量正确

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

某个节点掉线,上面的数据全部丢失,导致一些 Region Replica 数量不足

某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了 Replica Region Replica 数量多过,需要删除某个 Replica

管理员调整了副本策略,修改了 max-replicas 的配置

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

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

多个节点部署在同一台物理机器上

TiKV 节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性

TiKV 节点分布在多个 IDC 中,希望单个机房掉电时,也能保证系统可用

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

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

前面说过,每个副本中存储的数据容量上限是固定的,所以我们维持每个节点上面,副本数量的均衡,会使得总体的负载更均衡。

Leader 数量在 Store 之间均匀分配

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

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

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

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

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

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

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

支持手动下线节点

当通过 pd-ctl 手动下线节点后,PD 会在一定的速率控制下,将节点上的数据调度走。当调度完成后,就会将这个节点置为下线状态。

 

 

 

注:Tidb的技术内幕文章详见:

说存储(TIKV):  https://pingcap.com/blog-cn/tidb-internal-1/

说计算(TIDB):  https://pingcap.com/blog-cn/tidb-internal-2/

谈调度(PD):  https://pingcap.com/blog-cn/tidb-internal-3/

Tikvraft优化: 详见 https://zhuanlan.zhihu.com/p/25735592

Tikv/TIDB的源码解析: 详见pingcap博客: https://pingcap.com/blog-cn/