TiDB架构原理

MySQL的一些集群方案和数据库中间件都在视图解决当表条目变大时,查询性能下降的问题。TiDB的设计原生适用于大规模数据量的存储和查询,重点学习了关于其计算、存储和调度的三篇文档,整理如下。

架构

上图展示了TiDB的整体架构设计,重点来看TiDB、PD和TiKV的功能:

  • TiDB

    1. 负责接收SQL请求、语法检查
    2. 做SQL到逻辑查询逻辑的转换
    3. 基于PD信息将逻辑查询转化为物理查询计划
    4. 从TiKV获取数据后整合并返回;
  • PD(Placement Driver)

    1. 保存SQL逻辑表数据在物理TiKV中分布的映射关系
    2. 对TiKV集群做调度和负载均衡
    3. 分配全局事务ID
  • TiKV

    1. KV存储引擎

计算(TiDB)

整层架构如下图所示:

转换规则

TiDB最终是将数据以KV的形势存储到分布式的TIKV上,因此TiDB的重要作用就是做SQL表到KV存储的转换。
对于一个表来讲,需要存储的数据包括:

  1. 表的元信息
  2. 表中的条目
  3. 索引数据

元信息

每个 Database/Table 都被分配了一个唯一的 ID,这个 ID 作为唯一标识,并且在编码为 Key-Value 时,这个 ID 都会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 Key,Value 中存储的是序列化后的元信息。

表条目

TiDB 对每个表分配一个 TableID,每一个索引都会分配一个 IndexID,每一行分配一个 RowID(如果表有整数型的 Primary Key,那么会用 Primary Key 的值当做 RowID),行记录转换方式为:

1
2
Key: tablePrefix{tableID}_recordPrefixSep{rowID}
Value: [col1, col2, col3, col4]

实例: SQL表格式

1
2
3
1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30

Row转换后

1
2
3
t10_r1 --> ["TiDB", "SQL Layer", 10]
t10_r2 --> ["TiKV", "KV Engine", 20]
t10_r3 --> ["PD", "Manager", 30]

索引数据

Unique Index

1
2
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: rowID

非 Unique Index

1
2
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
Value: null

实例

1
2
3
t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null

操作与查询过程

对数据的操作包括:

  1. 对于 Insert 语句,需要将 Row 写入 KV,并且建立好索引数据。
  2. 对于 Update 语句,需要将 Row 更新的同时,更新索引数据(如果有必要)。
  3. 对于 Delete 语句,需要在删除 Row 的同时,将索引也删除。

执行查询语句Select count(*) from user where name="TiDB”;流程:

  1. 构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,那么我们用 0 和 MaxInt64 根据 Row 的 Key 编码规则,就能构造出一个 [StartKey, EndKey) 的左闭右开区间
  2. 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据
  3. 过滤数据:对于读到的每一行数据,计算 name=”TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据
  4. 计算 Count:对符合要求的每一行,累计到 Count 值上面 这个方案肯定是可以 Work 的,但是并不能 Work 的很好,原因是显而易见的:

为了减少网络开销,会尽量将一些计算下沉到TiKV, 比如将上面的扫描KeyRange、过滤数据和计算Count都放到TiKV, 然后基于各个分布式TiKV计算之后,将结果再上报到TIDB做汇总。

存储(TiKV)

TiKV的存储需要解决一下问题:

  1. 如何高效存储和访问;
  2. 数据冗余与一致性问题;
  3. 当数据量太大的时候,如何解决前面两个问题;

如何高效存储和访问

  1. 这是一个巨大的 Map,也就是存储的是 Key-Value pair(底层是采用RocksDB一个单机的 Key-Value Map存储)
  2. 这个 Map 中的 Key-Value pair 按照 Key 的二进制顺序有序,也就是我们可以 Seek 到某一个 Key 的位置,然后不断的调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value

MVCC

TiKV 的 MVCC 实现是通过在 Key 后面添加 Version 来实现的。 key的排列变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
Key1-Version3 -> Value
Key1-Version2 -> Value
Key1-Version1 -> Value
....
Key2-Version4 -> Value
Key2-Version3 -> Value
Key2-Version2 -> Value
Key2-Version1 -> Value
....
KeyN-Version2 -> Value
KeyN-Version1 -> Value
....

TiKV 的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方会尝试重新执行整个事务。

数据冗余与一致性问题

  1. 通过在多台物理节点上保存多个副本来做冗余;
  2. 使用raft协议来保障数据的一致性;

总结起来就是下图的架构设计,通过单机的 RocksDB,我们可以将数据快速地存储在磁盘上;通过 Raft,我们可以将数据复制到多台机器上,以防单机失效。数据的写入是通过 Raft 这一层的接口写入,而不是直接写 RocksDB。

Region

  1. 将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,每一段叫做一个 Region;
  2. 尽量保持每个 Region 中保存的数据不超过一定的大小(默认是 64mb);
  3. 每一个 Region 都可以用 StartKey 到 EndKey 这样一个左闭右开区间来描述。

当数据量大时,如何解决前两个问题

  1. 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的 Region 数量差不多;
  2. 以 Region 为单位做 Raft 的复制和成员管理;

调度(PD)

Pd之间本身通过etcd来实现分布式高可用容错和数据的一致性;

信息收集

  1. 每个 TiKV 节点会定期向 PD 汇报节点的整体信息
  2. 每个 Raft Group 的 Leader 会定期向 PD 汇报信息

调度策略

  1. 一个 Region 的 Replica 数量正确
  2. 一个 Raft Group 中的多个 Replica 不在同一个位置
  3. 副本在 Store 之间的分布均匀分配
  4. Leader 数量在 Store 之间均匀分配
  5. 访问热点数量在 Store 之间均匀分配
  6. 各个 Store 的存储空间占用大致相等
  7. 控制调度速度,避免影响在线服务
  8. 支持手动下线节点

调度实现

  1. PD 不断的通过 Store 或者 Leader 的心跳包收集信息,获得整个集群的详细数据
  2. 根据这些信息以及调度策略生成调度操作序列
  3. 收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果

binlog

  1. TiDB 实例连接到各个 Pump 节点并发送 binlog 数据到 Pump 节点。
  2. Pump 集群连接到 Drainer 节点,Drainer 将接收到的更新数据转换到某个特定下游(例如 Kafka、另一个 TiDB 集群或 MySQL 或 MariaDB Server)指定的正确格式。

数据热点问题

为解决热点问题,TiDB 引入了预切分 Region 的功能,即可以根据指定的参数,预先为某个表切分出多个 Region,并打散到各个 TiKV 上去。

1
SPLIT TABLE table_name [INDEX index_name] BETWEEN (lower_value) AND (upper_value) REGIONS region_num

BETWEEN lower_value AND upper_value REGIONS region_num 语法是通过指定上、下边界和 Region数量,然后在上、下边界之间均匀切分出 region_num 个 Region。

1
SPLIT TABLE table_name [INDEX index_name] BY (value_list) [, (value_list)] ...

BY value_list… 语法将手动指定一系列的点,然后根据这些指定的点切分 Region,适用于数据不均匀分布的场景

均匀切分

由于 row_id 是整数,所以根据指定的 lower_value、upper_value 以及 region_num,可以推算出需要切分的 key。TiDB 先计算 step(step = (upper_value - lower_value)/num),然后在 lower_value 和 upper_value 之间每隔 step 区间切一次,最终切出 num 个 Region。

1
SPLIT TABLE t BETWEEN (-9223372036854775808) AND (9223372036854775807) REGIONS 16;

不均匀切分

如果已知数据不是均匀分布的,比如想要 -inf ~ 10000 切一个 Region,10000 ~ 90000 切一个 Region,90000 ~ +inf 切一个 Region,可以通过手动指定点来切分 Region,示例如下:

1
SPLIT TABLE t BY (10000), (90000);
0%