基于 TiDB 的应用多活架构
基本架构模型
分布式数据库天然具备多活的特性。通过写入时的分布式一致性算法,确保集群中大多数节点数据保持一致,在涉及少数节点的故障场景,能够实现无数据丢失的容灾接管(RPO=0)。
基于分布式数据库的业务应用,从节点存活的角度来看具有相同的可用性等级。不同于数据库一般从内部运行看待可用性的方式,业务应用往往从外部看待自身的可用性,即不单是实例进程的存活,流量也要同时适配实例角色的转移。在物理距离不可忽视的场景,由网络带来的性能问题,也是流量适配必然要考虑的因素。
在业务流量视角,自顶向下看是应用访问数据库的接入点问题,自底向上看是数据库的数据分布与操作暴露问题。两者不是单方面的依赖和约束,不同数据库自身的架构决定了框架上的制约,应用对数据的需求决定数据分布的规则。同时,通过引入上层管理中间件,可以调和应用与数据库的冲突,即实现对应用影响更平滑的数据库变更。

如上图所示,一般情况下,从业务应用到数据节点是一个四层架构。其中,数据库集群不直接对外暴露数据节点的情况,通过数据代理进行统一接入与数据寻址,数据代理应是无状态的,否则难以扩容与容灾;业务应用不直接连接数据代理,通过负载均衡组件平衡数据访问流量,同时解耦应用与数据库。
在流量路径上,业务应用访问本地负载均衡组件,负载均衡组件访问本地数据代理集群,数据代理可能会访问异地数据节点。可以看到,应用访问数据库的接入点是稳定的,核心是数据节点的分布和数据在节点上的分布。
TiDB 整体架构
TiDB 是 PingCAP 公司自主设计和研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理(Hybrid Transactional and Analytical Processing, HTAP)的融合型分布式数据库产品,具备水平扩缩容、金融级高可用、实时 HTAP、兼容 MySQL 协议和 MySQL 生态等重要特性,适合高可用、强一致要求较高、数据规模较大等各种应用场景。
核心模块
在内核设计上,TiDB 将整体架构拆分成了多个模块,各模块之间互相通信,组成完整的分布式数据库系统。对应架构如图所示:

主要有三个成员:负责元信息管理的 PD Server、负责请求接入的 TiDB Server、负责数据存储的 TiKV Server。
PD (Placement Driver) Server:整个 TiDB 集群的元信息管理模块,负责存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构,提供 TiDB Dashboard 管控界面,并为分布式事务分配事务 ID。PD 不仅存储元信息,同时还会根据 TiKV 节点实时上报的数据分布状态,下发数据调度命令给具体的 TiKV 节点。
TiDB Server:SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,不存储数据,可以启动多个 TiDB 实例,通过负载均衡组件(如 TiProxy、LVS、HAProxy、ProxySQL、F5 等)对外提供统一的接入地址。
TiKV Server:负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range[StartKey, EndKey)的数据,每个 TiKV 节点会负责多个 Region。TiKV 中的数据会自动维护多副本(默认为三副本),天然支持高可用和自动故障转移。
TiFlash:TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,TiFlash 中的数据以列式进行存储,主要功能是为分析型的场景进行加速。
部署架构
在 TiDB 集群中,PD 和 TiKV 都通过 Raft 来实现数据容灾。根据 Raft 的原理,集群副本数量应是奇数,集群会选举出唯一的 Leader,数据读写有单点特性。
应用和数据库部署的物理区域相同,以距离和隔离程度,按地域和可用区划分,也就有跨地域容灾和跨可用区容灾。按照分布式集群特性,常见的架构有单地域多可用区架构和双地域多可用区架构,多地域多可用区架构管理模式与双地域多可用区架构雷同。考虑建设成本,TiDB 还扩展了同步模式,可以支持单地域双可用区架构。
以上架构要做到高可用、一致性和对用户透明,需要特定的数据分布和调度策略共同配合实现。
单地域多可用区
单地域多可用区架构,一般是同城 3AZ 方案,满足任一中心故障不影响数据一致性和系统可用性。

同城 3AZ 是 TiDB 集群最常规的部署架构,也是运维成本最低的方式。
对 PD Server:
每个可用区部署 1 个 PD 实例,3 个 PD 实例组成 Raft 集群;
选举其中 1 个实例作为 Leader,PD 实际在其中 1 个 AZ 进行读写;
任意 1 个 PD 实例故障不影响 PD 整体的可用性,但主 PD 故障会触发选举,有恢复周期(可能 10 秒级别)。
对 TiDB Server:
每个可用区部署任意数量(不为 0)的 TiDB 实例,可以根据流量负载决定;
TiDB 连接主 PD 获取元数据和 TSO(Timestamp Oracle,用于全局事务时序);
TiDB 根据数据分布连接各个 TiKV 实例。
对 TiKV Server:
每个可用区部署任意数量(不为 0)的 TiKV 实例,数据 Region 的副本数和 TiKV 的实例数不需要完全一致,但要满足两个条件(副本数小于等于实例数,副本数为奇数);
任意 1 个 TiKV 实例故障不影响 TiKV 整体的可用性,但数据 Region 有主副本,固定在其中 1 个 AZ 进行读写,主副本所在 TiKV 实例故障会触发选举,同样有恢复周期;
副本数确定的情况下,增加 TiKV 实例数不一定能提升 TiKV 整体的可用性(例如 3 副本 5 实例,2 个实例故障可能导致某些数据 Region 不可用),需要结合节点负载进行权衡。
同城 3AZ 架构下,节点实例故障是自愈的,一般不用进行干涉。主要副作用是跨 AZ 网络延迟:
所有写入操作需要同步复制到至少 2 个 AZ 当中,TiDB 采用二阶段提交方式(Percolator 事务模型),写入耗时至少为 2 倍 AZ 间网络延迟;
TiDB 中的每个事务都需要向主 PD 获取 TSO(一般写入事件 2 次,只读事件 1 次),如果 TiDB 和主 PD 不在同一个 AZ,会受 AZ 间网络延迟影响;
TiDB 连接数据主副本读取数据,如果 TiDB 和数据主副本不在同一个 AZ,也有跨 AZ 网络延迟。
实际上不能完全避免跨 AZ 流量,数据写入必须要进行同步复制,这是数据集群构建的基础。可以通过调度策略将 PD 和 Region 的主节点迁移到同一个可用区,业务流量也全部路由到这个 AZ,这样除复制外的流量都能在 AZ 内闭环,但这种方式资源闲置太多,现实意义不大。
双地域多可用区
双地域多可用区架构,一般是两地三中心方案,以一地为主,有生产数据中心、同城容灾中心、异地容灾中心。
一般认为地域间的网络稳定性是不可控的,跨地域的网络延迟也不为业务所接受。根据 Raft 单点读写的特性,完全没有跨地域的请求是不可能的,尽可能避免跨地域延迟要满足以下条件:(1)主节点和多数集在同一地域;(2)流量尽可能分发给主节点所在地域。

所以,两地三中心架构的设计目标以一个地域为主提供服务,能够容忍任一中心发生故障,容灾地域故障不影响生产地域业务,生产地域故障能在容灾地域恢复。为实现上述目标,TiDB 官方推荐采用 2-2-1 的 5 副本数据分布模式,如上图 PD 和 TiKV,同时通过调度策略将 PD 和 Region 的主节点限制在生产地域。
从可用区级别故障容忍度来说,3 副本也是可行的,每个 AZ 持有 1 个副本,生产地域 2AZ 也能组成多数集。副本数的选择还是要综合考虑可靠性、性能、成本等方面,例如:是否有更细粒度容灾需求(如机架、主机等)、写多副本的开销、资源投入等等。
限制主节点仅在生产地域之后,容灾地域的业务流量必然会跨地域读写(PD、TiKV),需要业务从上层流量入口进行规划。TiDB 提供了只读节点的实践,算是额外扩展,不属于集群组建的部分。
容灾地域故障不影响集群运转,生产地域任一 AZ 故障将导致集群需要跨地域达到共识,会对性能产生影响,但保障了可用性。
生产地域故障将导致集群暂时不可用,需要从容灾地域单副本中恢复集群,此时需要对集群进行重配置操作,也会丢失部分未同步的数据。
简单地形容两地三中心架构,可以说是跨中心的多活,跨地域的主备,中心级别故障可以自愈,主要地域故障需要外部干涉,干涉的及时性和有效性将显著影响地域级别的容灾效果。
单地域双可用区
单地域双可用区架构,指同城双活方案,一般是应用进行高可用扩展最先考虑到的方式,边际收益最高。
分布式一致性的大数原则天然不适应偶数分区,尤其是两个分区,任一分区故障可能会导致整体不可用,相较单个分区的可用性没有稳定提升。TiDB 提供自适应同步(Data Replication Auto Synchronous, DR Auto-Sync)模式,通过 Raft 集群角色分配和状态控制来达到双活的效果。

在这个解决方案中,TiDB 将 Raft 集群的角色划分为 Voter/Follower/Learner。实际上 Voter 和 Follower 都是组成数据集群的节点,是相同节点在不同场景不可角色的体现:
在 Leader 正常工作时,响应 Leader 请求进行数据同步的叫 Follower;
当 Follower 判断与 Leader 断连,会触发选举,将自己变成 Candidate;
选举发生后,Follower 会变成 Voter,为选举进行投票;
Learner 只同步数据,不参与选举,不参与投票。
上图所示架构描述了一个 6 副本集群,实际参与共识的只有 5 位成员,即 Voter+Follower。同城双活需要指定主中心,主中心分配 3 个 Voter,副中心分配 2 个 Follower 和 1 个 Learner,通过调度策略限制 Leader 只在主中心产生。最少数量来说,4 个副本(2 个 Voter+1 个 Follower+1 个 Learner)也能达到相同效果,容灾粒度不同。
正常情况下,主中心 3 副本构成多数集可以快速提交写操作。但这样的话,副中心的数据节点就是异步写入,主中心故障时可能会丢失数据。TiDB 增加了 Group 的概念,可以要求每个分组至少有一个节点提交,将 Voter 和 Follower 分别编组,就能实现数据同步写到副中心的至少一个节点。
这种架构可以容忍任意两个数据节点(TiKV)发生故障,副中心的 3 节点同时故障也不影响系统正常运行,主中心的 3 节点同时故障则需要对集群进行重配置操作,将 Learner 加入到集群中构成多数集(3 节点)继续提供服务,这也是 Learner 存在的意义。
相对应的,TiDB 的自适应同步定义了三种状态来管理数据同步:
sync:同步复制。主副中心网络正常,副中心至少有一个节点保持数据同步,保障灾备数据完整性。
async:异步复制。副中心故障、网络隔离或超时,不保证副中心数据保持同步,主中心多数集正常运行,优先保障可用性。
sync-recover:恢复同步。节点故障恢复,数量满足同步复制的条件,集群进入恢复的中间状态,数据恢复后切换回同步复制模式。
虽然同城双活看起来是性价比最高的方式,但违背了分布式集群固有的设计,有削足适履的嫌疑,实际隐性成本很高,比如:最少 4 副本,资源冗余多;状态管理复杂,容灾预期不可控;需要较重的人工干预等等。
逻辑数据模型
TiKV 是一个 Key-Value 的存储引擎,底层依赖的是 RocksDB(Facebook 基于 LevelDB 开发的)。
存储单位
TiDB 按 Key 的连续顺序切分数据,每个分段作为数据存储的基本单位,术语叫做 Region。
每个 Region 负责存储一个 Key Range[StartKey, EndKey)的数据,每个 TiKV 节点会负责多个 Region。Region 也是 Raft 进行同步的基本单位,一组通过 Raft 算法分布到多个 TiKV 实例的多副本 Region 叫做一个 Raft Group。

TiDB 对 Region 有自适应调度策略,主要围绕几个方面进行:一是资源利用率,二是 Raft 分组容灾,三是节点伸缩。所以,Region 的 Range 不是固定的,可能会根据容量拆分合并;Region 副本的分布也不是固定的,可能会根据资源情况腾挪迁移。
可以通过调度配置指定某些数据分区的 Leader 优先落在哪个 TiKV 实例,这在期望流量尽可能在某个 AZ 闭环的场景往往很有用,但 Region 自适应调度隐含了与之相悖的冲突——Region 是可分裂、可迁移的,流量路由并不稳定。硬要控制 Region 不可分裂和不可迁移往往适得其反,即使不考虑资源利用率均衡问题,可用性和性能的优先级也高于预定规则。
最好不要过度干涉 TiDB 的自适应调度,选择合理的物理区域,尽可能保障网络条件,结合容灾架构,以容灾区域为粒度进行调度配置是更合适的选择。
表映射
TiDB 兼容 MySQL 协议,逻辑上要实现关系模型到 K-V 模型的映射,主要有表数据映射、表索引映射和 MVCC 数据映射,锁数据等不展开。
表数据映射:
每个表分配一个整数 ID,在整个集群内唯一,用 TableID 表示。
每行数据分配一个整数 ID,在表内唯一,用 RowID 表示。
Key: t{TableID}_r{RowID}
Value: [col1, col2, col3, col4]
表索引映射:
每个索引分配一个整数 ID,用 IndexID 表示。
主键索引:
Key: t{TableID}_i{IndexID}_{索引值}
Value: RowID
二级索引:
Key: t{TableID}_i{IndexID}_{索引值}_{RowID}
Value: null
MVCC 数据映射:
在 Key 后面添加版本号实现
Key1_Version3 -> Value
Key1_Version2 -> Value
Key1_Version1 -> Value
从 TiDB 表映射的设计可以判断:
表数据是聚合的,Region 按 RowID 顺序拆分;
表数据和表索引的 Key 前缀不同,同一行的数据和索引可能分布在不同 Region;
同一行多版本数据是 Key 后缀拼接,按行拆分 Region,同一行的数据在相同 Region。
分区表
业务视角的应用数据是关系表,除了基础的单表外,TiDB 还支持多种分区表。分区表就是大表的横向拆分,主要有 Range 分区、Range COLUMNS 分区、Range INTERVAL 分区、List 分区、List COLUMNS 分区、Hash 分区和 Key 分区。
Range 分区:
范围切割,Range 必须是连续的,并且不能有重叠
用“value less than”进行定义,可以用表达式转成数字比较
Range COLUMNS 分区:
Range 分区的变体,支持一列或多列,不支持表达式
分区列的数据类型可以是整数 (integer)、字符串(CHAR/VARCHAR)、DATE 和 DATETIME
Range INTERVAL 分区:
Range 分区的语法糖,按步长切割
List 分区:
精确分区,为每个分区指定包含集
用“values in”进行定义
List COLUMNS 分区:
List 分区的变体,可以将多个列用作分区键
除数字外,还可以使用字符串、DATE 和 DATETIME 类型
Hash 分区:
哈希均匀分散,只需要指定分区数量
Key 分区:
与 Hash 分区一样,可以保证数据均匀分散到一定数量的分区
Hash 分区只能根据一个指定的整数表达式或字段进行分区,而 Key 分区可以根据字段列表进行分区,且 Key 分区的分区字段不局限于整数类型
在多活容灾场景,可以为分区表指定放置策略配合流量调度:例如按 UserID 哈希分区,p0 分区的主节点放在 az0、p1 分区的主节点放在 az1,将 p0、p1 分区的用户流量分别导向 az0、az1。
从前面的讨论可以知道,TiDB 对这样的放置策略只是最大努力(Best-Effort),不是严格保证,放置策略的生效速度也跟集群所处的环境和资源使用情况有关,不是即时的过程,作为部署架构锦上添花的优化可能合适,严格进行数据流量拆分明显不足。
版权声明: 本文为 InfoQ 作者【陈一之】的原创文章。
原文链接:【http://xie.infoq.cn/article/8cbecec2041dd44541b082e77】。文章转载请联系作者。
评论