TiKV 是一个分布式事务型的键值数据库,提供了 满足 ACID 约束
的分布式事务接口,并且通过 RAFT 协议保证了多副本数据一致性以及高可用。TiKV 作为 TiDB 的存储层,为用户写入 TiDB 的数据提供了持久化以及读写服务,同时还 存储了 TiDB 的统计信息数据
。
整体架构
TiKV 参考 Spanner 设计了 multi-raft-group 的副本机制。
将数据按照 key 的范围划分成大致相等的切片,统称为 Region,每一个切片会有多个副本(通常是 3 个),其中一个副本是 Leader,提供读写服务。
虽然 TiKV 将数据按照范围切割成了多个 Region,但是同一个节点的所有 Region 数据仍然是不加区分地存储于同一个 RocksDB 实例上,而用于 Raft 协议复制所需要的日志则存储于另一个 RocksDB 实例。
- 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的 Region 数量差不多
- 以 Region 为单位做 Raft 的复制和成员管理
RocksDB
RocksDB 允许用户创建多个 ColumnFamily,这些 ColumnFamily 各自拥有独立的内存跳表以及 SST 文件,但是 共享同一个 WAL 文件
,这样的好处是可以根据应用特点为不同的 ColumnFamily 选择不同的配置,但是又没有增加对 WAL 的写次数。
RocksDB 作为 TiKV 的核心存储引擎,用于存储 Raft 日志以及用户数据。
每个 TiKV 实例中有两个 RocksDB 实例,一个用于 存储 Raft 日志
(通常被称为 raftdb),另一个用于 存储用户数据以及 MVCC 信息
(通常被称为 kvdb)。
kvdb 中有四个 ColumnFamily:raft、lock、default 和 write:
-
raft 列:用于存储各个 Region 的元信息。仅占极少量空间,用户可以不必关注。
-
lock 列:用于存储悲观事务的悲观锁以及分布式事务的一阶段 Prewrite 锁。
当用户的事务提交之后,lock cf 中对应的数据会很快删除掉,因此大部分情况下 lock cf 中的数据也很少(少于 1GB)。
如果 lock cf 中的数据大量增加,说明有大量事务等待提交,系统出现了 bug 或者故障。
-
write 列:用于存储用户真实的写入数据以及 MVCC 信息(该数据所属事务的开始时间以及提交时间)。
当用户写入了一行数据时,如果该行数据长度小于 255 字节,那么会被存储 write 列中,否则的话该行数据会被存入到 default 列中。
-
default 列:用于存储超过 255 字节长度的数据。
TiKV 持久化1
分布式事务3
Raft 与 Multi Raft4
读写与 Coprocessor5
SQL 执行流程6
TiKV 持久化
TiKV 架构
RocksDB 作为 TiKV 的核心存储引擎,用于存储 Raft 日志以及用户数据。1
TiKV 作用
- 数据持久化
- 分布式一致性
- MVCC
- 分布式事务
- Coprocessor 协同处理器
数据持久化和读取
Rocksdb
单机数据存储引擎
- 高性能 key-value 数据库
- 持久化机制,保证性能和安全行
- LSM 存储引擎
- 良好的支持范围查询
- 为存储 TB 级别数据到本地 FLASH 或 RAM 的应用服务设计
- 针对中小键值优化-可以存储在 FLASH 或者直接存储在内存
- 性能随 CPU 数量线性提升,对多核系统友好
写入操作
-
先写 WAL(Write Ahead Log)预写日志,再写入内存 MemTable
WAL 关键参数:sync_log=true,避免先写操作系统缓存,直接写磁盘
-
写入数据量到达 write_buffer_size 大小限制时,MemTable 转变成 immutable MemTable(不可变)
immutable MemTable:写到磁盘 SST 文件的一个中间状态,避免直接写磁盘,减少 IO 等待
-
重启开启一个 MemTable
-
将 immutable MemTable 刷到磁盘上形成 SST 文件,该 immutable MemTable 销毁
-
immutable MemTable 达到 5 个就会触发 rocksdb 的流控 write stall,限制写入 MemTable 的速度
-
可以优化存储或者调高 immutable 数量来提高写入速度
-
WAL 中数据都持久化到 SST file 之后,会被删除
Level0:immutable Memtable 的复制,默认达到 4 个后,向下一层做 Compaction
为什么 Compaction?
用于去除相同 key 的多条记录。当我们对一个 key 进行多次 update/delete 时,RocksDB 会产生多条记录。
从 Level0~LevelN:4 个合并成 1 个,压缩并对 key 进行排序
immutable 数量达到 min-write-buffer-number-to-merge 之后就会触发 flush。
L0 层上包含的文件,是由内存中的 memtable dump 到磁盘上生成的,单个文件内部按 key 有序,文件之间无序。可能存在多个相同的 key 在 L0 层
L1~L6 层上的文件都是按照 key 有序的。也就是每层只会存在一个 key。
每一层都会切分成多个 SST 文件,每个 SST 文件都是键值对文件
对于每个文件使用二分法进行查找键值信息
删除时:不管数据在哪,直接写入 delete key
查询操作
查询性能不如 B+ 树
Block Cache:最近最常读数据的缓存
依次按照写入最新时间查找 MemTable,再按从磁盘中的 Level 0 依次往后查找到 SST 文件
根据查找的 KEY 判断是否在 SST 的 min_key 和 max_key 中间;
引入布隆过滤器 bloom filter 判断 key 在不在这个 SST 中,如果 key 不在,则查找下一个 SST 文件
如果数据在该 SST 文件,则二分法查找
Column Family 属于 RocksDb 的数据分片技术,可以将数据的键值对按照不同的属性分配给不同的 CF,可以让某些内存和 SST 文件中存的都是相同类型的数据,可以极大地增加读写的效率、提升数据压缩率;
落数的时候会自带 CF1、CF2、default 来决定落入哪个分片中;
内存和 SST 文件都按照 CF 分了,但是 WAL 没有按照 CF 区分。 ↩
RocksDB 作为 TiKV 的核心存储引擎,用于存储 Raft 日志以及用户数据。 ↩
分布式事务
TiDB 采用 Google Percolator 事务模型来解决分布式事务的问题。
Google Percolator 事务模型
Percolator 事务分为两个阶段:预写(Pre-write)和提交(Commit),本质上相当于一个加强的 2PC。
- 从 PD 组件获取事务开始时间
- 将要修改的数据读出到 TiDB Server 的内存中,进行修改,commit
- 进行两阶段提交
- 第一阶段:prewrite 会将修改的数据和锁信息写入到 TiKV 节点中;
- 第二阶段,在 Write CF 中写入数据;
- 从 PD 获取 TSO,作为事物结束时间;
- 锁清理: 往 Lock CF 中落入一条数据清理锁。
什么是列族
RocksDB 的每个键值对都与唯一一个列族(column family)结合。如果没有指定 Column Family,键值对将会结合到“default” 列族。
列族提供了一种从逻辑上给数据库分片的方法。他的一些有趣的特性包括:
- 支持跨列族原子写。意味着你可以原子执行 Write({cf1, key1, value1}, {cf2, key2, value2})。
- 跨列族的一致性视图。
- 允许对不同的列族进行不同的配置
- 即时添加/删除列族。两个操作都是非常快的。
事务的过程
假设有这样一个事务:
begin: update person set name = 'Frank' where id = 3; commit;
事务开始
在事务开始时,begin 时,TiDB 会从 PD 中获取事务开始的时间戳 TSO,假设 TSO=100。
修改数据
然后 TiDB 将需要修改的数据读取到内存中,在内存中完成数据的修改。
TiDB 在内存中修改数据的时候,不会将锁信息写入 TiKV,此时其他会话无法感知锁的存在,是乐观事务。
乐观事务与悲观事务
乐观事务模型就是直接提交,遇到冲突就回滚。
悲观事务模型就是在真正提交事务前,先尝试对需要修改的资源上锁,只有在确保事务一定能够执行成功后,才开始提交。
对于乐观事务模型来说,比较适合冲突率不高的场景,因为直接提交大概率会成功,冲突是小概率事件,但是一旦遇到事务冲突,回滚的代价会比较大。
悲观事务的好处是对于冲突率高的场景,提前上锁的代价小于事后回滚的代价,而且还能以比较低的代价解决多个并发事务互相冲突导致谁也成功不了的场景。不过悲观事务在冲突率不高的场景并没有乐观事务处理高效。
事务提交
在 commit 的时候进入两阶段提交。
预写 Prewrite
在第一阶段,TiDB 会写三个列族到 TiKV 中:
- Default 列族:记录带有事务开始时间戳标记(100)的修改后的数据
- Lock 列族:记录锁信息,在第一行数据加写锁(W),这是一把主锁(pk),并且记录下其他相关的信息
- Write 列族:预留用来存放提交信息
此时,其他会话会感知到写锁的存在,这样其他会话不会进行 id=3 的数据的读、写操作。
注意:
当用户写入的数据长度小于 255 字节时,数据会被存储在 Write 列族;
当用户写入的数据长度大于 255 字节时,数据会被存储在 Default 列族。
提交 Commit
在第二阶段,TiDB 会从 PD 中获取事务提交的时间戳 TSO,假设 TSO=110。
TiDB 在 Write 列族中写入提交信息,包括事务提交的时间戳(110)和事务开始的时间戳(100)
完成后在 Lock 列族中记录一条锁清理的数据,表示写锁已经被释放。
此时,其他会话可进行 id=3 的数据的读、写操作。
如何处理分布式事务
假设有这样一个事务:
begin: update person set name = 'Jack' where id = 1; update person set name = 'Candy' where id = 2; commit;
并且两条数据分布在 TiKV 中的两个实例上。
TiDB 会按照事务的处理过程进行处理。
在预写阶段,在事务中的第一条数据(节点 1,id=1)上加主锁(pk),在节点 2 上记录的是附加锁(@1)表示的是主锁在 id=1 的那条记录那里。
此时,节点 1 和节点 2 上都有写锁,其他会话不能对这些数据进行读、写操作。
在提交阶段,节点 1 和节点 2 都写入提交信息和清理写锁。
当节点 1 提交成功,节点 2 提交失败,那么节点 2 上的写锁不会被清除。
后续在读取数据的时候,发现有写锁存在,并且是附加锁(@1),此时需要判断事务是否提交:
根据附加锁的指向找到主锁,发现主锁已成功提交,则可判断自己在提交阶段出现了问题。
因为主锁是成功提交的,所以附加锁这里只需要补充提交即可,继续写入提交信息,清理附加锁,Default 列族中的数据变成最终数据。
MVCC
从前面的过程中知道,事务已预写、未提交的时候,数据不能进行读、写操作,这带来一个问题,就是读也会被阻塞。
MVCC 的引入是为了解决读操作被阻塞的,因为在修改中的、还未提交的数据,还不确定最后是否提交,那么读取修改前的数据应该是被允许的。
MVCC 机制下,读操作按最近一次提交记录读取,无需关心锁信息,写操作需要先检查当前是否已存在其他写锁。
假设有两个事务:
--事务1 begin (start_ts = 100) update person set name = 'Jack' where id = 1; update person set name = 'Candy' where id = 2; commit; (commit_ts = 110) --事务2 begin (start_ts = 115) update person set name = 'Tim' where id = 1; update person set name = 'Jerry' where id = 4;
此时,事务 1 已成功提交,事务 2 未提交。
假设在时间戳 TSO=120 的时候,有会话读取数据:
select * from person where id in (1,2,4);
此时 id=1 和 id=4 的记录有锁信息但无提交信息,属于在事务中的数据,如果阻塞读操作,那么此时 id=1 和 id=4 的记录都是无法读取的。
引入 MVCC 后。
读 id=1 的数据时,从 Write 列族中发现最近一次提交是 TSO=100,读操作可以读取 TSO=100 的数据。
写 id=1 的数据时,当前锁是 TSO=115,因为 TSO=115 的锁还未提交,所以写被阻塞。
读 id=2 的数据时,从 Write 列族中发现最近一次提交是 TSO=100,读操作可以读取 TSO=100 的数据。
写 id=2 的数据时,当前无锁,所以可以写。
读 id=4 的数据时,从 Write 列族中发现最近一次提交是 TSO=80,读操作可以读取 TSO=80 的数据。
写 id=4 的数据时,当前锁是 TSO=115 的附加锁,因为主锁还未提交,所以附加锁也还在事务中,写被阻塞。
↩
Raft 与 Multi Raft
raft group:region 及其副本(上图以 3 副本为例)
Multi raft:由多个 raft goup 组成
名词解释
Leader
- 集群的管理者
- 所有读写流量都是走 Leader
- Leader 会周期性向 follower 发出心跳信息
- Leader 会将写的数据以日志的方式传递给其他 follower
- 当写入的数据成员过半,就认为写入成功
Follower
- 被管理者
- 对其他的服务作出响应
- 接受 Leader 的日志
- 如果长时间没收到 Leader 的通知信息,就会将自己角色转换为后选择 candidate,发起投票,票多者升级为 Leader
Region
-
是按照 Key 排序的连续的有序集合
-
当 Region 插入达到 96MB 后会另起一个新 Region
-
初始化时,Region 内的数据是连续的,Region 中间也是连续的,左闭右开区间
region1: [1,1000), region2:[1000-2000),region3:[2000,3000)
-
随着数据的修改(例如 UPDATE 等),Region 大小会发生变化,当数据涨到 144M 的时候会自动分裂;
当 Region 过小的时候会进行 Region 的合并;(分裂和合并的大小可以自定义)
-
一个 Region 构成一个 Raft group
多个 Region 会形成多个 Raft Group--Multi Raft
-
如果一个 TiKV 中的 Region 超过 5W,会影响性能
Raft 日志复制
raft log 利用 region ID+ 日志的顺序 ID 来作为唯一标识
所有客户端的读写流量都通过 leader
Leader 日志写入的过程
- Propose,Leader 将写请求转化为 Raft Log 的形式;
- Append:日志持久化, Leader 在 Propose 后会将写入请求转换为写入日志,存到日志文件中;(日志组成:region_id + 序号 + 数据组成,日志存储在本地的 RocksDB 实例中)
- Replicate:Leader 将日志分发给 follower -> follower 收到日志后写入到本地存储中(Append)-> 返回消息给 Leader 确认;
- Commited:当多数节点都返回了 Append 成功的消息后,Leader 认为写入成功;此时可以保证 Raft rocksdb 的日志不丢失;(区别于用户的 commit)
- Apply:Leader 将 raft log(rocksdb raft 中)的操作 apply 至 rocksdb kv 中的数据,写入完成(一个 TiKV 中实际上有两个 RocksDB,一个用于存储 Raft Log,一个用于存储 KV 信息;)
ps:若大多数(超过一半的)follower 没有持久化成功(Replicate),则日志不会被 Apply
Raft- Leader 选举
leader 周期性向 follower 发出含有统治信息的心跳(参数 heartbeat time interval);若 follower 长时间收不到统治信息(参数 election timeout),某个 region 将会转换为 candidate(候选者),并发起投票重新选取 leader
election timeout:控制收不到统治信息时,发起选举的阈值;一般用于初始化时进行选举(集群刚被创建)
heartbeat time interval:控制发出心跳的周期,当收不到心跳的时间且超过 election timeout 的阈值,发起选举;一般用于集群运行中
任期的概念 term
- Raft 把时间分割成任意长度的任期(term),任期用连续的整数标记。
- 每一段任期从一次选举开始,一个或者多个 candidate 尝试成为 leader 。如果一个 candidate 赢得选举,然后他就在该任期剩下的时间里充当 leader 。在某些情况下,一次选举无法选出 leader 。在这种情况下,这一任期会以没有 leader 结束;一个新的任期(包含一次新的选举)会很快重新开始。
- Raft 保证了在任意一个任期内,最多只有一个 leader
任期的作用
- 不同的服务器节点观察到的任期转换的次数可能不同,在某些情况下,一个服务器节点可能没有看到 leader 选举过程或者甚至整个任期全程。
- 任期在 Raft 算法中充当逻辑时钟的作用,这使得服务器节点可以发现一些过期的信息比如过时的 leader 。
- 每一个服务器节点存储一个当前任期号,该编号随着时间单调递增。
- 服务器之间通信的时候会交换当前任期号;
- 如果一个服务器的当前任期号比其他的小,该服务器会将自己的任期号更新为较大的那个值。
- 如果一个 candidate 或者 leader 发现自己的任期号过期了,它会立即回到 follower 状态。(所以说老 leader 如果发生了网络分区,后来接收到新 leader 的心跳的时候,比拼完任期之后,会自动变成 follower。)
- 如果一个节点接收到一个包含过期的任期号的请求,它会直接拒绝这个请求。
选举的发起流程
-
每个 Node 启动的时候,初始化 Role 都是 Follower,并且启动计时器,超时还没接收到消息(可以是 Leader 的 AppendEntries RPC,也可以是 Candidate 的 Vote RPC)
-
Raft 集群的启动选举
Raft 使用一种心跳机制来触发 leader 选举。当服务器程序启动时,他们都是 follower 。一个服务器节点只要能从 leader 或 candidate 处接收到有效的 RPC 就一直保持 follower 状态。Leader 周期性地向所有 follower 发送心跳(不包含日志条目的 AppendEntries RPC)来维持自己的地位。
如果一个 follower 在一段选举超时时间内没有接收到任何消息,它就假设系统中没有可用的 leader ,然后开始进行选举以选出新的 leader。 -
选举过程
要开始一次选举过程,follower 先增加自己的当前任期号并且转换到 candidate 状态。然后投票给自己并且并行地向集群中的其他服务器节点发送 RequestVote RPC(让其他服务器节点投票给它)。
Candidate 会一直保持当前状态直到以下三件事情之一发生:
(a) 它自己赢得了这次的选举(收到过半的投票)
(b) 其他的服务器节点成为 leader
(c) 一段时间之后没有任何获胜者。这些结果会在下面的章节里分别讨论。
当一个 candidate 获得集群中过半服务器节点针对同一个任期的投票,它就赢得了这次选举并成为 leader 。
对于同一个任期,每个服务器节点只会投给一个 candidate ,按照先来先服务(first-come-first-served)的原则。
要求获得过半投票的规则确保了最多只有一个 candidate 赢得此次选举。
一旦 candidate 赢得选举,就立即成为 leader 。然后它会向其他的服务器节点发送心跳消息来确定自己的地位并阻止新的选举。图示说明
如图,当 leader 宕机,TiKV node 3 长时间收不到心跳并率先达到阈值,自身转化为 candidate,进入下一个 term 并发起选举
向其他 region 发起请求,进行投票;其他 follower 接收请求并同意比自己 term 大的请求(term=2<term=3)
如图,若碰巧所有 Tikv node 都转为 candidate 企图发起选取,election timeout 参数将会初始为随机值,减小每个 TiKV node 达到阈值的记录;若还是有多个 candidate 重复该过程,整个过程发起了多次选取
election timeout
- raft-election-timeout-ticks:设置 election timeout 有多少个相对时间单位(ticks,默认 1s)
- raft-base-tick-interval:设置每个相对时间单位(ticks)有多长时间
heartbeat time interval
-
raft-heartbeat-ticks:设置 heartbeat time interval 有多少个相对时间单位(ticks,默认 1s)
-
raft-base-tick-interval:设置每个相对时间单位(ticks)有多长时间
假设 raft-election-timeout-ticks=5,raft-base-tick-interval=1s,则 election timeout=5*1=5s
ps:election timeout ticks 不能小于 heartbeat ticks ↩
读写与 Coprocessor
数据写入
- 用户提交写请求
- TiDB Server 接收请求
- TiDB Server 向 PD 申请 TSO,获得 Region 元数据信息(哪一个 leader,在哪个 TiKV 上)
- TiDB Server 的写请求会由 TiKV 中的 raftstore pool 线程池接收进行处理
- 执行 Raft 日志复制过程(Propose、Append、Replicate、Commited、Apply)
完成数据写入操作。
数据读取
Index Read
本质上属于一种判断操作是否 apply 至 rocksdb 的机制。
用户在读取数据的时候,读取请求会提交到 TiDB Server,TiDB Server 从 PD 获取数据的元数据信息,包括所在的 TiKV、Region、Leader 等等信息。
问题一
TiDB Server 拿到元数据后去 TiKV 节点找 leader Region 读取数据,此时不能完全保证在读取数据的时候原来的 leader 还是 leader。因为这个时间间隔内,原 leader 可能失效,重新选举了 leader,也就是 TiDB Server 从 PD 处拿到的 leader 信息在真正要读数据的这个时间间隔内失效了,变成 follower 了,就不能读数据了。
TiDB 采用了读取时进行心跳检测机制,TiDB Server 拿到 leader 信息后,到 leader region 真正读数据的时候,由该 region 发起一次心跳检测,检测当前 region 是否还是 leader,如果是,就直接读取数据,如果不是,就不能读取数据,重新获取 leader region,从新的 leader 读取数据。
问题二
抛开 MVCC 机制,只考虑 Raft 协议,用户的读操作需要读取之前的请求已经提交的数据。在读取时,如果有写操作存在,则读取被阻塞。什么时候可读?
TiDB 引入了 ReadIndex 和 ApplyIndex。当读取数据时,先获取到要读取的数据所在的位置(index)。然后寻找 Raft 日志复制阶段中 commited 阶段的 index,确保 commited 的 index 大于要读取的数据的 index 后,记录 commited 的 index 作为 ReadIndex 值,寻找 Raft 日志复制阶段中的 apply 阶段的 index 作为 ApplyIndex 值。只有当 ApplyIndex 的值等于 ReadIndex 的值时,可以确定当前 index 的数据已经提交持久化,从而知道要读取的数据所在的 index 的值已确定持久化,此时读取的数据就是提交后的数据。
当某一会话提交操作(TSO=10:00),但其操作未 apply 至 rocksdb;此时 ReadIndex=1-95,ApplyIndex=1-92,rocksdb 中的数据为 1-92
当另一会话企图读取被修改的数据(TSO=10:05),此时 raftstore pool 将日志 commit 至 1-97,但 applypool 将操作应用至 1-93(该案例假设没有 MVCC 机制,若有则读取的是之前的数据即 1-93);即将 ReadIndex=1-97 并阻塞 commit,使其等待 apply 操作至 1-97
当 apply 至 1-95 时,事务的 commit 完成
当 apply 至 1-97 时,即 ApplyIndex 等于 ReadIndex,此时根据 raft log 的顺序性,事务 1-95 肯定已被持久化至 rocksdb
总结:利用日志的顺序性,通过判断后面的日志是否已 apply 推断出要读取的日志是否 apply
ReadIndex 一定大于要读的 raft log,当 ApplyIndex 等于 ReadIndex 时判断
Lease Read
Lease Read 也可以叫 Local Read。
TiDB Server 从 PD 获得 TSO 时的 leader 还是正常的,那么 leader 会发送心跳,时间间隔是 heartbeat time interval,而 follower 会等待 election timeout,如果达到 election timeout 还没收到心跳,才会进行重新选举 leader。这意味着,即便 TiDB Server 从 PD 获取到 TSO 后 leader 出现问题,至少需要等待 election timeout 这么长时间集群才会重新选举 leader,也就是在 election timeout 这个时间范围内,还是可以从原 leader 处读取数据的。这就是 Lease Read。
Follower Read
follower 的数据与 leader 的数据是一致的,所以读实际上可以从 follower 读的,也就是读写分离是可以的,**前提是:**保证 follower 的数据与 leader 的数据是线性一致的。
要保证数据的线性一致,Follower Read 是在 follower 节点上按照 Index Read 的方式读取数据的。先从 leader 节点获取到 leader 当前的 commit index,然后需要 follower 节点的 apply index 等于获取到的 commit index 后才能进行数据的读取。
Coprocessor
协同处理器,可以实现:执行物理算子,算子下推:聚合、全表扫描、索引扫描等。
比如,TiDB Server 接收到用户的请求。
如果不引入 Coprocessor 机制,那么 TiKV 的所有数据会发送到 TiDB Server 做运算,一方面增加网络带宽,一方面 TiDB Server 的负载会很高。
引入 Coprocessor 后,可以实现 count 算子下推。
每个 TiKV 节点计算自己节点上的 count,将值传到 TiDB Server,TiDB Server 只需要对各个节点的进行一次处理就好了。
↩
SQL 执行流程
DML 语句读流程
TiDB Server 中的 Protocol Layer 首先接收用户的 SQL 请求,TiDB Server 会到 PD 获取 TSO,同时经过解析模块 Parse 对 SQL 进行词法分析和语法分析,再由 Compile 模块进行编译,最终进行 Execute。
SQL 的 Parse 与 Compile
Protocol Layer 接收到 SQL 请求后,会由 PD Client 与 PD 进行交互,获取 TSO。
Parse 模块会对 SQL 进行词法分析、语法分析,生成抽象语法树 AST。
Compile 分成几个阶段:
-
预处理 preprocess:
检测 SQL 的合法性、绑定信息等,判断 SQL 是否是点查 SQL(仅查询一条数据,比如按 Key 查询),如果是点查语句则不需要执行 optimize 优化,直接执行即可。
-
优化 optimize:
- 逻辑优化,根据关系代数、等价交换等对 SQL 进行逻辑变换,比如外连接尝试转内连接等;
- 物理优化,基于逻辑优化的结果和统计信息,选择最优的算子。
SQL 的 Execute
Compile 的产物是执行计划,有了执行计划之后就由 Executor 进行执行。
Executor 先获取 information schema,information schema 可以预先从 TiKV 加载到 TiDB Server。然后 Executor 需要从 PD 获取 Region 的元数据信息,为减少 TiDB Server 与 PD 交互带来的网络开销、延迟,TiKV Client 的 region Cache 可以缓存 Region 的元数据信息,后续就可以直接使用。
Executor 执行数据读取有两种类型,点查 SQL,直接读取 KV;复杂 SQL,需要通过 DistSQL 将复杂 SQL 转换成对单表的简单查询 SQL。
TiKV 接收到读取请求后,会创建一个数据快照 snapshot,所有查询 SQL 都会进入 UnifyRead Pool 线程池,然后从 RocksDB KV 读取数据。
当数据读取完成后,数据通过 TiKV Client 返回给 TiDB Server。
由于 TiDB 实现了算子下推,对于聚合操作,TiKV 完成 cop task,TiDB Server 完成 root task,也就是在 TiDB Server 中还需要对下推算子的聚合结果进行汇总。
DML 语句写流程
写流程,会先经过一次读流程,将数据读取到缓存 MemBuffer 中,然后再进行数据修改,最后进行两阶段提交进行数据写入。
执行
Transaction 执行两阶段提交。
Transaction 按行读取 memBuffer 的数据进行数据写入,事务包含两个 TSO,一个是事务开始 TSO,一个是事务提交 TSO。
写请求发送给 TiKV 的 Scheduler,Scheduler 是接收并发处理的,需要负责协调冲突写入(两个会话并行写相同的 Key),冲突写入采用分配 latch,谁获得 latch 谁执行写操作的方式解决冲突。无冲突的写入,交 Raftstore,完成 Raft 日志写入过程,涉及本地 append、replicate、commited、apply 等过程。
DDL 语句流程
TiDB Server 接收到用户的 DDL 请求,start job 模块将操作写入队列,由 TiDB Server 的 Owner 角色的 workers 模块负责执行队列中的 DDL 语句。
workers 从 job queue 队列中获取待执行的 DDL 语句,执行,执行完成后将其放到 history queue 队列中。
执行
TiDB 支持 Online DDL,也就是 DDL 不锁表,不阻塞读、写。
同一时刻,只有一个 TiDB Server 的角色是 Owner 角色的,只有 Owner 角色的 TiDB Server 的 workers 才可以执行 DDL。schema load 负责将最新的表结构信息加载到 TiDB Server。
Compile 生成的执行计划,交给 start job,start job 先检查本机节点的角色是否是 Owner,如果是,则可直接由本机 workers 直接执行,如果不是,则 start job 将 DDL 操作封装成 job 加入到 job queue 队列中。如果 DDL 操作是对索引的操作,则 job 会加入到 add index queue。
Owner 的 workers 会定时扫描 job queue,发现 queue 中有 job 就获取执行,执行完成后将 job 加入到 history queue 中。
Owner 角色由 PD 协调,在 TiDB Server 之间是轮询切换,所以总体来说,每个 TiDB Server 都有机会成为 Owner。 ↩
-
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于