TiDB集群体系结构 作者:周万春 微信:lovemysql3306 1、CAP分布式 CAP 理论是分布式系统的一个基础理论,它描述了任何一个分布式系统最多只能满足以下三个特性中的两个: 一致性(Consistency) 可用性(Availability) 分区容错性(Partition tolerance 2、TiDB整体架构 TiDB 有以下的一些优势: 纯分布式架构,拥有良好的扩展性,支持弹性的扩缩容 支持 SQL,对外暴露 MySQL 的网络协议,并兼容大多数 MySQL 的语法,在大多数场景下可以直接替换 MySQL 默认支持高可用,在少数副本失效的情况下,数据库本身能够自动进行数据修复和故障转移,对业务透明 支持 ACID 事务,对于一些有强一致需求的场景友好,例如:银行转账 具有丰富的工具链生态,覆盖数据迁移、同步、备份等多种场景 三大模块:每个模块都是分布式的架构。 1、计算层(SQL)------------》TiDB 2、分布式存储(K-V键值对)----》PD 3、元信息系统、分布式调度-----》TiKV + TiFlash TiDB 1、无状态、不存储数据 2、接受客户端连接 3、执行 SQL 解析和优化 4、生成分布式执行计划 5、数据读取请求转发给底层的存储层 TiKV TiKV + TiFlash TiKV 分布式 KV 存储(默认分布式存储引擎)。 支持弹性的扩容和缩容。 默认3个多副本。 支持高可用和自动故障转移。 TiFlash 把数据以列式的形式进行存储(是为了分析型的场景加速)。 PD 整个 TiDB 集群的元信息存储。 存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构。 为分布式事务分配事务 ID。 支持高可用。 TiKV 节点实时上报的数据分布状态。 PD 下发数据调度命令给具体的 TiKV 节点。 3、TiKV说存储 本地存储(RocksDB) TiKV 的 KV 存储模型和 SQL 中的 Table 无关! 数据保存在 RocksDB 中,再由 RocksDB 将数据落盘。 一个TiKV里面有两个 RocksDB,一个用于存储数据,一个用于存储 Raft。 TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft 的日志复制功能,将数据安全可靠地同步到复制组的每一个节点中。 Raft 是一个一致性协议。 Raft 负责: Leader(主副本)选举 成员变更(如添加副本、删除副本、转移 Leader 等操作) 日志复制(通过 Raft,将数据复制到其他 TiKV 节点) 数据写入: 数据的写入是通过 Raft 这一层的接口写入,而不是直接写 RocksDB。 数据写入 ---》Raft ---》RocksDB ---》磁盘 Region 对于一个 KV 系统,将数据分散在多台机器上有两种比较典型的方案: Hash:按照 Key 做 Hash,根据 Hash 值选择对应的存储节点。 Range:按照 Key 分 Range,某一段连续的 Key 都保存在一个存储节点上。(TiKV选择此方案) 连续的 key-value 划分为一个 Region,默认大小 96 M。 TiKV: Region1: key1-value1 key2-value2 key3-value3 Region2: key4-value4 key5-value5 key6-value6 以 Region 为单位做数据的分散和复制: 以 Region 为单位,将数据分散在集群中所有节点上,并且保证每个节点上 Region 数量尽可能相同。(此均匀分布是 PD 干的活) 以 Region 为单位做 Raft 的数据复制和成员管理。 TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本,TiKV 将每一个副本叫做一个 Replica。 Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft Group。 其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。 所有的读和写都是通过 Leader 进行,读操作在 Leader 上即可完成,而写操作再由 Leader 复制给 Follower。 TiKV 的 MVCC 实现是通过在 Key 后面添加版本号来实现。 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 ...... 通过 RocksDB 的 SeekPrefix(Key_Version) API,定位到第一个大于等于这个 Key_Version 的位置。 分布式事务ACID TiKV 的事务采用的是 Google 在 BigTable 中使用的事务模型:Percolator。 在 TiKV 层的事务 API 的语义类似下面的伪代码: tx = tikv.Begin() tx.Set(Key1, Value1) tx.Set(Key2, Value2) tx.Set(Key3, Value3) tx.Commit() 4、TiDB谈计算 表数据与 Key-Value 的映射关系 TiDB 会为每个表分配一个表 ID,用 TableID 表示。表 ID 是一个整数,在整个集群内唯一。 TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。 如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID。 每行数据构成的 (Key, Value) 键值对: Key: tablePrefix{TableID}_recordPrefixSep{RowID} Value: [col1, col2, col3, col4] 索引数据和 Key-Value 的映射关系 TiDB 为表中每个索引分配了一个索引 ID,用 IndexID 表示。 对于主键和唯一索引,我们需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对: Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue Value: RowID 对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,我们需要根据键值范围查询对应的 RowID。 因此,按照如下规则编码成 (Key, Value) 键值对: Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID} Value: null 一个表内所有的行都有相同的 Key 前缀,一个索引的所有数据也都有相同的前缀。 元信息管理 元信息也是以 Key-value 存储在了 TiKV 中。 每个 Database/Table 都被分配了一个唯一的 ID。这个 ID 会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 Key,Value 中存储的是序列化后的元信息。 当前所有表结构信息的最新版本号,也会构成 Key-value 键值对,存储在 pd-server 内置的 etcd 中。 其Key 为"/tidb/ddl/global_schema_version",Value 是类型为 int64 的版本号值。 有一个后台线程在不断的检查 etcd 中存储的表结构信息的版本号是否发生变化,并且保证在一定时间内一定能够获取版本的变化。 TiDB 的 SQL层,即 tidb-server 负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层 TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。 将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。 这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。 5、PD讲调度 作为一个分布式高可用存储系统,必须满足的需求,包括四种: 副本数量不能多也不能少 副本需要分布在不同的机器上 新加节点后,可以将其他节点上的副本迁移过来 节点下线后,需要将该节点的数据迁移走 作为一个良好的分布式系统,需要优化的地方,包括: 维持整个集群的 Leader 分布均匀 维持每个节点的储存容量均匀 维持访问热点分布均匀 控制 Balance 的速度,避免影响在线服务 管理节点状态,包括手动上线/下线节点,以及自动下线失效节点 信息收集 调度依赖于整个集群信息的收集,简单来说,我们需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息: 每个 TiKV 节点会定期向 PD 汇报节点的整体信息。 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 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。 默认是 30 分钟,如果一直没有心跳包,就认为是 Store 已经下线。 再决定需要将这个 Store 上面的 Region 都调度走。但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知 PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。 调度的策略 一个 Region 的 Replica 数量正确 当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的 Replica 数量不满足要求时,需要通过 Add/Remove Replica 操作调整 Replica 数量。出现这种情况的可能原因是: 某个节点掉线,上面的数据全部丢失,导致一些 Region 的 Replica 数量不足 某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了 Replica 的 Region 的 Replica 数量多过,需要删除某个 Replica 管理员调整了副本策略,修改了 max-replicas 的配置 一个 Raft Group 中的多个 Replica 不在同一个位置 副本在 Store 之间的分布均匀分配 访问热点数量在 Store 之间均匀分配 各个 Store 的存储空间占用大致相等 控制调度速度,避免影响在线服务 如果希望加快调度(比如已经停服务升级,增加新节点,希望尽快调度),那么可以通过 pd-ctl 手动加快调度速度。 支持手动下线节点 当通过 pd-ctl 手动下线节点后,PD 会在一定的速率控制下,将节点上的数据调度走。当调度完成后,就会将这个节点置为下线状态。 调度的实现 PD 不断的通过 Store 或者 Leader 的心跳包收集信息,获得整个集群的详细数据,并且根据这些信息以及调度策略生成调度操作序列, 每次收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息, 将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。 注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行, 具体是否会执行以及什么时候执行,由 Region Leader 自己根据当前自身状态来定。