logo头像

Always believe youself.

TiDB入门-2

本文于632天之前发表,文中内容可能已经过时。

TiDB 数据库的计算

TiDB 在 TiKV 提供的分布式存储能力基础上,构建了兼具优异的交易处理能力与良好的数据分析能力的计算引擎。

表数据与 Key-Value 的映射关系

TiDB 中数据到 (Key, Value) 键值对的映射方案。这里的数据主要包括以下两个方面:

  • 表中每一行的数据,以下简称表数据
  • 表中所有索引的数据,以下简称索引数据

在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个 (Key, Value) 键值对,需要考虑如何构造 Key。首先,OLTP 场景下有大量针对单行或者多行的增、删、改、查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一 ID(显示或隐式的 ID),以方便快速定位。其次,很多 OLAP 型查询需要进行全表扫描。如果能够将一个表中所有行的 Key 编码到一个区间内,就可以通过范围查询高效完成全表扫描的任务。

基于上述考虑,TiDB 中的表数据与 Key-Value 的映射关系作了如下设计:

为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用 TableID 表示。表 ID 是一个整数,在整个集群内唯一。
TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID。

SQL 层简介

TiDB 的 SQL 层,即 TiDB Server,负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层 TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。

这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。

SQL 运算

最简单的方案就是通过上一节所述的表数据与 Key-Value 的映射关系方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。

比如 select count(*) from user where name = "TiDB" 这样一个 SQL 语句,它需要读取表中所有的数据,然后检查 name 字段是否是 TiDB,如果是的话,则返回这一行。

具体流程如下:

  • 构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,使用 0 和 MaxInt64 根据行数据的 Key 编码规则,就能构造出一个 [StartKey, EndKey)的左闭右开区间。
  • 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据。
  • 过滤数据:对于读到的每一行数据,计算 name = “TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据。
  • 计算 Count():对符合要求的每一行,累计到 Count() 的结果上面。

流程示意图:

image

分布式 SQL 运算

尽量靠近存储节点,以避免大量的 RPC 调用。首先,SQL 中的谓词条件 name = “TiDB” 应被下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。然后,聚合函数 Count(*)也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个 Count(*) 的结果即可,再由 SQL 层将各个节点返回的 Count(*) 的结果累加求和。

数据逐层返回的示意图:
image

SQL 层架构

TiDB 的 SQL 层要复杂得多,模块以及层次非常多,下图列出了重要的模块以及调用关系:

image

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

TiDB 数据库的调度

PD (Placement Driver) 是 TiDB 集群的管理模块,同时也负责集群数据的实时调度。

TiKV 集群是 TiDB 数据库的分布式 KV 存储引擎,数据以 Region 为单位进行复制和管理,每个 Region 会有多个副本 (Replica),这些副本会分布在不同的 TiKV 节点上,其中 Leader 负责读/写,Follower 负责同步 Leader 发来的 Raft log。

需要考虑以下场景:

  • 为了提高集群的空间利用率,需要根据 Region 的空间占用对副本进行合理的分布。
  • 集群进行跨机房部署的时候,要保证一个机房掉线,不会丢失 Raft Group 的多个副本。
  • 添加一个节点进入 TiKV 集群之后,需要合理地将集群中其他节点上的数据搬到新增节点。
  • 当一个节点掉线时,需要考虑快速稳定地进行容灾。
    • 从节点的恢复时间来看
      • 如果节点只是短暂掉线(重启服务),是否需要进行调度。
      • 如果节点是长时间掉线(磁盘故障,数据全部丢失),如何进行调度。
    • 假设集群需要每个 Raft Group 有 N 个副本,从单个 Raft Group 的副本个数来看
      • 副本数量不够(例如节点掉线,失去副本),需要选择适当的机器的进行补充。
      • 副本数量过多(例如掉线的节点又恢复正常,自动加入集群),需要合理的删除多余的副本。
  • 读/写通过 Leader 进行,Leader 的分布只集中在少量几个节点会对集群造成影响。
  • 并不是所有的 Region 都被频繁的访问,可能访问热点只在少数几个 Region,需要通过调度进行负载均衡。
  • 集群在做负载均衡的时候,往往需要搬迁数据,这种数据的迁移可能会占用大量的网络带宽、磁盘 IO 以及 CPU,进而影响在线服务。

以上多个同时出现,就不太容易解决,因为需要考虑全局信息。同时整个系统也是在动态变化的,因此需要一个中心节点,来对系统的整体状况进行把控和调整,所以有了 PD 这个模块。

调度的实现

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

注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 根据当前自身状态来定。