TiDB集群方案与Replication原理

TiDB集群方案与Replication原理

一 TiKV Server实现原理

TiKV Server实现了数据的强一致和高可用,为了实现CAP中的这两点,它的存储数据的基本单位是 Region,并使用了Raft共识算法提升系统的容错性。

1.1 Region

TiKV 可以看做是一个巨大的有序的 KV Map,那么为了实现存储的水平扩展,数据将被分散在多台机器上。

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

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

TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,并且会尽量保持每个 Region 中保存的数据不超过一定的大小,目前在TiKV 中默认是 96MB。每一个 Region 都可以用[StartKey,EndKey) 这样一个左闭右开区间来描述。

将数据划分成 Region 后,TiKV 将会做两件重要的事情:

  1. 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的Region 数量差不多。这是 PD Server 的主要工作;
  2. 以 Region 为单位做 Raft 的复制和成员管理。复制过程中 Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft Group。其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。默认情况下,所有的读和写都是通过 Leader 进行,读操作在 Leader 上即可完成,而写操作再由Leader 复制给 Follower。

1.2 Raft 共识算法

分布式一致性算法中比较有代表性的有 Raft 和 Paxos;其中 ZooKeeper 是基于 Paxos(zab paxos的变种);Raft 共识算法提供了几个重要的功能:

  1. Leader(主副本) 选举
  2. 成员变更(如添加副本、删除副本、转移 Leader 等操作)(增加物理节点)
  3. 日志复制

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

Raft算法示意图如下所示:
在这里插入图片描述
Raft算法简介:
任意一个节点都有三个状态:follower、candidate、leader。
每个节点手里只有一张票。
任期 原leader被刺杀,下一个leader任期递增,第一次是1,每次增1。
最开始的时候,系统中每个节点都只要共有多少给节点,以及它们的地址。

简单模型(大体流程):

  1. leader 选举
    任意一个节点都有三个状态:follower、candidate、leader。最开始的状态是follower。follower在一段内没有收到leader的消息,就会变成candidate,并向其他节点拉票,得票多的candidate会变成leader。
  2. 日志复制
    DML操作首先会写到leader节点的Log中,然后leader节点会将Log复制给其他的节点,然后等待它们的回复。当leader节点接收到大多数节点的回复后,这个条目就被commit。commit后,leader会通知其他节点已经commit,其他节点也会commit。这时cluster就会达成共识,然后leader就会返回客户端,已经完成命令。

实现细节:

  1. leader选举
    两个超时:选举超时,心跳超时。选举超时的时间设定是节点自己随机决定的,范围在150ms到300ms。当一个节点选举超时后,就会从follower变为candidate。
    当一个节点变为candidate,会做:

    1. 任期加1(初始为0);
    2. 给自己投一票;
    3. 向其他follower拉票;
    4. 重随机选举超时。

    当其他follower收到拉票信息时:

    1. 重置选举超时;
    2. 任期加一;
    3. 投票给第一个拉票的candidate。

得到大多数票的candidate变成leader。leader会按时发送附加条目,leader和其他节点之间的同步都是通过附加条目进行的。发送两个附加条目之间的时间间隔是心跳时间,。每当心跳超时后,leader就会向其他节点发送附加条目。其他节点收到心跳后,就会重置自己的选举超时,并回复心跳。

如果两个节点都成为candidate,可能出现平票,然后节点等待新的选举超时。
2. 日志复制
一旦leader被改变,leader需要把该变内容同步给其他节点上去。同步是通过附件消息同步的。附加消息是通过附加到心跳包上进行同步的。
当DML操作leader节点后,leader节点会先写Log,然后通过下一次心跳包广播到其他节点上去。其他节点在接收到这个心跳包后,也会将

网络分区,由于节点数少的分区的leader不会获得大多数节点对Log的回应,因此永远不会commit,所以不会产生脏数据。

任期用来解决网络分区的问题。任期短的leader遇到任期长的leader时,前者会自动变成任follower。出问题的部分,会回滚之前未commit的日志,复制其他已经提交的节点的日志,并复制它们的任期。

TiFlash是learner,只能复制,没有选举权。

二 分布式事务

TiDB 中分布式事务采用的是 Percolator 的模型。Percolator是一个标准的 2PC(2Phase Commit),2PC(2Phase Commit)是一个经典的分布式事务的算法。但是 2PC 一般来说最大的问题是事务管理器(Transaction Manager)。

二阶段提交是将事务的提交过程分成了两个阶段来进行处理,能够非常方便地完成所有分布式事务参与者的协调,统一决定事务的提交或回滚。

  • 阶段一:提交事务请求
    1. 事务询问
      所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应;(和raft不同,这个是全部都要一直)。
    2. 执行事务
      各参与者节点执行事务操作,并将 “undo” 和 “redo” 信息记录在事务日志中;
    3. 各参与者向协调者反馈事务询问的响应
      执行成功,返回 yes 响应;执行失败,返回 no 响应;
  • 阶段二:执行事务提交(根据反馈决定是否进行事务提交)
    • 执行事务提交(全是 yes 响应)
      1. 发送提交请求
        协调者向所有参与者节点发出 Commit 请求;
      2. 事务提交
        参与者收到 Commit 请求后,会正式执行事务提交操作,并在提交操作之后释放在整个事务执行期间占用的资源;
      3. 反馈事务提交结果
        参与者完成事务后,向协调者发送 ack 消息;
      4. 完成事务
        协调者接收到所有参与者反馈的 ack 消息后,完成事务;
    • 中断事务(只要有一个 no 响应)
      1. 发送回滚请求
        协调者向所有参与者节点发出 Rollback 请求;
      2. 事务回滚
        参与者接收到 Rollback 请求后,会利用 “Undo” 信息执行事务回滚,并在回滚后释放整个事务执行期间占用的资源;
      3. 反馈事务回滚结果
        参与者在完成事务回滚之后,向协调者发送 ack 消息;
      4. 中断事务
        协调者接收到所有参与者反馈的 ack 消息后,完成事务中断;

总的来说,二阶段提交将一个事务的处理过程分为了投票和执行两个阶段;核心是对每个事务都采用先尝试后提交的处理方式。

在分布式的场景下,有可能会出现第一阶段后某个参与者与协调者的连接中断,此时这个参与者并不清楚这个事务到底最终是提交了还是被回滚了,因为理论上来说,协调者在第一阶段结束后,如果确认收到所有参与者都已经将数据落盘,那么即可标注这个事务提交成功。然后进入第二阶段,但是第二阶段如果某参与者没有收到 COMMIT 消息,那么在这个参与者复活以后,它需要到一个地方去确认本地这个事务后来到底有没有成功被提交,此时就需要事务管理器(协调器)的介入。这个事务管理器在整个系统中是个单点,即使参与者,协调者都可以扩展,但是事务管理器需要原子的维护事务的提交和回滚状态。

Percolator 模型则取消了事务管理器,

Percolator 模型的写事务流程:

每当事务开始,协调者(在 TiDB 内部的 tidb server充当这个角色)会从 PD leader 上获取一个timestamp,然后使用这个 ts 作为标记这个事务的唯一 id。标准的 Percolator 模型采用的是乐观事务模型,在提交之前,会收集所有参与修改的行(key-value pairs),从里面随机选一行,作为这个事务的 Primary row,剩下的行自动作为 secondary rows,这里注意,primary 是随机的,具体是哪行完全不重要,primary 的唯一意义就是负责标记这个事务的完成状态。在选出 Primary row 后, 开始走正常的两阶段提交,

第一阶段是上锁+写入新的版本,所谓的上锁,其实就是写一个 lock key。假设我们这个事务要 Update (A, B, C, Version 4) ,第一阶段,我们选出的 Primary row 是A,那么第一阶段后,数据库的 Layout 会变成:
在这里插入图片描述

在第一阶段中在数据库中新写入的数据,可以注意到, A_Lock 、B_Lock 、C_Lock 这几个就是所谓的锁,大家看到 B 和 C 的锁的内容其实就是存储了这个事务的 Primary lock 是谁。在 2PC 的第二阶段,标志事务是否提交成功的关键就是对 Primary lock 的处理,如果提交 Primary row 完成(写入新版本的提交记录+清除 Primary lock),那么表示这个事务完成,反之就是失败,对于Secondary rows 的清理不需要关心,可以异步做。

Percolator 是采用了一种化整为零的思路,将集中化的事务状态信息分散在每一行的数据中(每个事务的 Primary row 里),对于未决的情况,只需要通过 lock 的信息,顺藤摸瓜找到 Primaryrow 上就能确定这个事务的状态。

三 隔离级别

最早的 ANSI SQL-92 提出了至今为止仍然是应用最广的隔离级别定义,读提交、可重复读、可序列化。MySQL就是依据它做的隔离级别。这个隔离级别定义如下:

隔离级别脏读不可重复读幻读丢失更新
Read UncommittedYesYesYesYes
Read CommittedNoYesYesYes
Repeatable ReadNoNoYesNo
SerializableNoNoNoNo

TiDB又引入了一个全新的隔离级别:Snapshot Isolation。它在Repeatable Read之下,Serializable 之上。

Snapshot Isolation

  1. 事务的读操作从 Committed 快照中读取数据,快照时间可以是事务的第一次读操作之前的任意时间,记为StartTimestamp;
  2. 事务准备提交时,获取一个 CommitTimestamp,它需要比现存的 StartTimestamp 和CommitTimestamp 都大;
  3. 事务提交时进行冲突检查,如果没有其他事务在 [StartTS, CommitTS] 区间内提交了与自己的WriteSet 有交集的数据,则本事务可以提交;这里阻止了 Lost Update 异常;
  4. SI 允许事务用很旧的 StartTS 来执行,从而不被任何的写操作阻塞,或者读一个历史数据;当然,如果用一个很旧的 CommitTS 提交,大概率是会 Abort 的;

TiDB只有写锁。

TiDB是不会被死锁的。

四 列式存储TiFlash

TiDB 是一款分布式 HTAP 数据库,它目前有两种存储节点,分别是 TiKV 和 TiFlash。TiKV 采用了行式存储,更适合 TP 类型的业务;而 TiFlash 采用列式存储,擅长 AP 类型的业务。

TiFlash 通过Raft 协议从 TiKV 节点实时同步数据,拥有毫秒级别的延迟,以及非常优秀的数据分析性能。它支持实时同步 TiKV 的数据更新,以及支持在线 DDL。我们把 TiFlash 作为 Raft Learner 融合进TiDB 的 raft 体系,将两种节点整合在一个数据库集群中,上层统一通过 TiDB 节点查询,使得TiDB 成为一款真正的 HTAP 数据库。

TiFlash 研发了新的列存引擎 Delta Tree。它可以在支持高 TPS 写入的同时,仍然能保持良好的读性能;

4.1 整体架构

Delta Tree 的架构设计充分参考了 B+ Tree 和 LSM Tree 的设计思想。从整体上看,Delta Tree将表数据按照主键进行 range 分区,切分后的数据块称为 Segment;然后 Segment 内部则采用了类似 LSM Tree 的分层结构。分区是为了减少每个区的数据量,降低复杂度。

4.2 Segment

Segment 的切分粒度通常在150 万行左右,远超传统 B+ Tree 的 Leaf Node 的大小。Segment的数量在一台机器上通常在 10 万以内,所以我们可以把 Segment 的元信息完全放在内存,这简化了工程实现的复杂度。和 B+ Tree 的叶子节点一样,Segment 支持 Split、Merge。在初始状态,一张表只存在一个 range 为 [-∞, +∞) 的 Segment。
在这里插入图片描述

4.3 Levels LSM-Tree

在 Segment 内部,通过类似 LSM Tree 的分层的方式组织数据。 因为 Segment 的数据量相对于其他 LSM Tree 实现要小的多,所以 Delta Tree 只需要固定的两层,即 Delta Layer 和 StableLayer,分别对应 LSM Tree 的 L0 和 L1。我们知道对于 LSM Tree 来说,层数越少,写放大越小。默认配置下,Delta Tree 的理论写放大(不考虑压缩)约为 19 倍。因为列式存储连续存储相同类型的数据,天然对压缩算法更加友好,在生产环境下,Delta Tree 引擎常见的实际写放大低于 5 倍。
在这里插入图片描述

4.4 Pack

Segment 内部的数据管理单位是 Pack,通常一个 Pack 包含 8K 行或者更多的数据。关系型数据库的 schema 由多个列定义组成,每个列定义包括 column name,column id,column type 和default value 等。由于支持 DDL,比如加列、删列、改数据类型等操作,所以不同的 Pack schema 有可能是不一样的。Pack 的数据也由列数据(column data)组成,每个列数据其实就是一维数组。Pack 除了主键列 Primary Keys(PK) 以及 schema 包含的列之外,还额外包含version 列和 del_mark 列。version 就是事务的 commit 时间戳,通过它来实现 MVCC。del_mark 是布尔类型,表明这一行是否已经被删除。

将 Segment 数据分割成 Pack 的作用是,可以以 Pack 为 IO 单位和索引过滤单位。在数据分析场景,从存储引擎获取数据的方式都是 Scan。为了提高 Scan 的性能,通常我们的 IO 块要比较大,所以每次读 IO 可以读一个或者多个 Pack 的数据。另外通常在分析场景,传统的行级别的精确索引通常用处不大,但是我们仍然可以实现一些简单的粗糙索引,比如 Min-Max 索引,这类索引的过滤单位也是 Pack。

在 TiDB 的架构中,TiKV 的数据是以 Region 为调度单位,Region 是数据以 range 切分出来的虚拟数据块。而 Delta Tree 的 Pack 内部的数据是以 (PK, version) 组合字段按升序排序的,与TiKV 内的数据顺序一致。这样可以让 TiFlash 无缝接入 TiDB 集群,复用原来的 Region 调度机制。

4.5 Delta Layer

Delta Layer 相当于 LSM Tree 的 L0,它可以认为是对 Segment 的增量更新,所以命名为Delta。与 LSM Tree 的 MemTable 类似,最新的数据会首先被写入一个称为 Delta Cache 的数据结构,当写满之后会被刷入磁盘上的 Delta Layer。而当 Delta Layer 写满之后,会与 Stable
Layer 做一次 Merge(这个动作称为 Delta Merge)得到新的 Stable Layer。

4.6 Stable Layer

Stable Layer 相当于 LSM Tree 的 L1,是存放 Segment 的大部分数据的地方。它由一个不可修改的文件存储,称为 DTFile。一个 Segment 只有一个 DTFile。Stable Layer 同样由 Pack 组成,并且数据以 (PK, version) 组合字段按升序排序。不一样的是,Stable Layer 中的数据是全局有序,而 Delta Layer 则只保证 Pack 内有序。原因很简单,因为 Delta Layer 的数据是从 DeltaCache 写下去的,各个 Pack 之间会有重复数据;而 Stable Layer 的数据则经过了 Delta Merge动作的整理,可以实现全局有序。

当 Segment 的总数据量超过配置的容量上限,就会以 Segment range 的中点为分裂点,进行split,分裂成两个 Segment;如果相邻的两个 Segment 都很小,则可能会被 merge 在一起,变成一个 Segment。

4.7 Delta Cache

为了缓解高频写入的 IOPS 压力,我们在 Delta Tree 的 Delta Layer 中设计了内存 cache,称为Delta Cache。更新会先写入 Delta Cache,写满之后才会被 flush 到磁盘。而批量写入是不需要写 Delta Cache 的,这种类型的数据会被直接写入磁盘。Delta Tree 并没有在写入数据的之前写WAL;而是充分利用 TiDB 中使用的 Raft 协议;在 Raft 协议中,任何一个更新,会先写入 Raftlog,等待大多数副本确认之后,才会将这个更新应用到存储引擎;在这里 TiFlash 直接利用了Raft log 实现 WAL,即在 flush 之后才更新 raft log applied index。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值