TDSQL 是腾讯云旗下金融级分布式数据库产品,具备强一致高可用、全球部署架构、分布式水平扩展、高性能、 HTAP 双引擎、Oracle 兼容、企业级安全、便捷易运维等特性,目前金融核心系统客户已超过 20 家,尤其 在银行传统核心系统领域,位居国内第一阵营。客户涵盖多家国有大行,及平安银行、张家港银行、昆山农 商行等头部银行及广泛金融行业机构。
TDSQL 的全称为 Tencent Database SQL。下图是 TDSQL 的最新技术架构,它描绘了一个计算/存储分离、 数据面 / 管控面分离的高可用的原生分布式架构的关系型数据库。 TDSQL 分为三层:绿色部分是计算层,称之为 SQLEngine ;紫色部分是管控层,简称为 MC ,负责整个 集群的管控调度工作;蓝色部分是存储层,称为 TDStore 。 TDSQL 架构三个重要的特性:全分布式,无论在计算层还是管控层,每一层都是分布式的架构;计算与存 储分离;可实现动态可扩展功能特性。
TDSQL 的四个主要功能特点分别是:
分布式架构主要分为计算层、存储层和管控层。
首先是计算层。TDSQL 计算层最大的特点是多主架构,每个节点都支持读写,完全相互独立;每个计算节 点为无状态,具备扩容优势,可实现 MySQL 的高度兼容,具备无状态化的弹性扩容的架构特性。
其次是存储层。它是我们自研的 KV 存储引擎。在存储层和计算层的交互中,存储层承担事务协调者的角色。 我们以一定方式将数据打散到每个存储节点上,每个数据分片称为 Region 。存储层对所有数据的状态自身 无感知,只负责数据的读写,所有数据的调度都由它与 MC 的交互来进行。
最后是管控层。它是一个分布式的集群,以 N 倍的方式去部署。在整个集群中,它要同时承担管控层面和数 据层面的工作。比如在数据层面,MC 负责一个全局、一个中心的严格递增,是唯一的分配者角色,且同时 负责管理所有的计算节点、存储节点的元数据、MDL 锁。
以下是数据库中常见的主功能流程:
分布式事务。数据库的访问是天然的分布式事务。整体流程为:从 MySQL 客户端发送请求,通过计算层对 存储层进行读写,读写过程中,存储层和计算层都会去和 MC 交互,获取时间戳,最终确定每个事务之间的 偏序关系,完成整个流程。
无感知扩缩容。当存储空间不够时就需要扩容。在扩容过程中,必然会涉及到数据的搬迁。如下图例子所示, 整个集群中只有一个存储节点,当需要扩容时,可以在界面上点击多购买一个存储节点。此时存储节点上数 据的分裂搬迁、计算层对最终数据路由的感知、计算层感知路由的变化后完成的重试等过程可以完全自洽地 包含在整个数据库体系中,实现业务层无感知。
Online DDL。TDSQL 的分布式体系架构采用多写架构。为达到更好的并发性能,需要在多写的架构下,实 现 Online DDL。相较同类产品,当业务尝试在运行过程中加一列、加一个索引时,需要借助外部的工具, 及堵塞业务来完成 DDL 的操作。为了更好的用户体验,降低对业务的影响,通过 TDSQL 可以把多写架构 下的 DDL 以原生方式去完成。
TDSQL 架构的三大功能特性:
这些特性直接、简单, 却在工程落地时遇到 诸多挑战,主要是高 性能、高可用、复杂 调度三个方面。下面 将着重分享我们在高 性能、复杂调度方面 遇到的挑战。
首先是高性能方面 的挑战。在架构上, 要做到分布式的完 全存算分离的架构, MC 作 为 集 群 内 唯 一一个中心的管控 模块,必须承担全 局授时源角色。
在分布式事务的整体架构图中,可以了解到 MC 在事务过程中需要和存储层做网络交互,提供时间戳,这是 关键路径。同时 TDSQL 的计算层和存储层都可以灵活扩缩容。存算分离、高扩展的两个特性也意味着 MC 必须要具有非常高的性能。
在复杂调度层面。我们设计成数据和管控完全分离的架构,数据完全存储在TDStore上,只负责数据流的读写。 其他工作完全交由管控层去进行,MC 只需要监控整个集群的状态做出关于存储资源的决策。
高性能方面的探索与实践
在高性能方面,我们采用 非常经典的三段式协程架 构,即一个协程收、处理、 最后再发。这种架构在我们突破 60 万时就达到性能瓶颈。
通过性能分析,我们意识 到性能瓶颈集中在第二个 协程里。于是我们将出现 瓶颈的地方并行化。但第 二个协程增加到一定时, 下个瓶颈又出现,因为协 程 1 是单管道模式,新的 瓶颈点集中在协程 1。
我们在下个版本里做了一个略微复杂的 N 对 N 架构,也是多协程架构。基于此我们发现性能可以提升但 CPU 的消耗非常大。我们的设计目标是 MC 在性能方面有较强的表现,其性能数据能到达 500 万。但当时 尽管达到 75 核,数据还是停留在 320 万。我们对此进行 perf 分析,发现问题主要来自 RPC 解析,因为 MC 主要网络框架的实现是基于 GRPC 的网络通讯,会有比较大的头部序列化和反序列化的性能开销。
已知性能阻碍存在于网络框架,我们优化的目标就成为摆脱网络框架带来的性能限制。对此我们给时间戳的 获取开发了 TCP ROW Socket 通道。因为时间戳数据结构有一个天然优势,即请求无状态、无依赖,只包 含两三个整型字段。这时在网络上发来的任何请求,MC 只需要收到一个,回答一个,可以去定制化完成请求。
在弃用该框架后,性能提升飞快,在比较低的 CPU 开端的情况下,可以将性能提升到 450 万。但在后续过 程中,我们发现瓶颈出现在请求进队列还有请求出队列的过程中。我们使用 Go Channel 作为消息的进出队 列载体,Channel 虽然好用且轻量,但底层依旧带锁实现,push/pull 操作存在着百纳秒级别的开销。
对此我们计划实现无锁队列,需要实现单生产者、单消费者模式的场景。基于这种场景,我们实现一个简单 的信号量,作为两者之间的唤醒机制。使用该优化方案后,资源的消耗明显降低且达到更高的性能,峰值吞 吐突破 750 万。
最初 500 万的目标虽已完成,但我们团队仍认为性能数据还可以更好。以下为经典的 CPU 缓存的 MESI 状 态转换图。一行 CPU 的 Cache Line 可以容纳 64 个字节,在我们的数据结构中,将其中多个 8 字节的变 量放在同一个缓存,如果一个更新非常多,另一个更新的少,就会影响另一个原子变量的读写。从图片右边 可知,在这里把变量的 8 字节对齐后,就解决 CPU 缓存行的问题,性能数据也从 750 万上升至 920 万。 此时我们的目标是实现单中心的千万级别的性能数据。
为了进一步实现单中心的千万级别的性能数据,我们从业务场景进一步深挖。在整个集群中,MC 在时间戳 方面是单一的提供者,而集群中众多的计算节点和存储节点会产生源源不断的请求,因此在分析生产者和消 费者速度时,我们发现生产者速度远远跟不上消费者速度。为此我们进行了简单的改造,即来一批请求再消 费一批时间戳,以批量请求方式去唤醒消费者。为了适应业务场景,我们还对该优化做了开关,运维人员可 以根据业务场景的需求进行调节。执行批量化操作后,整体峰值已经提升至两千万,许多数据库实例的业务 场景都无法到达这种高压力。
下图为 TDSQL 原生数据库下,我们通过一系列优化手段达到 2000 万性能数据的过程。
由于实现数据面与管控面的分离,MC 要负责整个集群所有跟资源相关的管控。如图所示,图中画有 MC 的 主要功能。
MC 需要负责时间戳的提供,管理全局的唯一 ID 的分配、DDL 的协调、计算层管控层资源的元数据以及数 据分片的管理。在管控层的不同层面,所有跟管理调度相关的工作都集中在 MC 。
为了实现复杂调度,我们首先划分资源层级,制定可用的具有可扩展性的基础框架,将 MC 模块从管理任务 中释放。将每个资源层级必须划分清楚,使得数据路径上的所有模块只需要被动执行,不需要更新状态。我 们从集群层面、复制组层面和副本层面进行划分,划分出许多子状态及子步骤。
比如在扩容过程中,有一个数据分片,副本分布在 123 三个存储节点中,如果要进行数据迁移使得一主两备 变为 1234 分布,任意时刻这四个节点都知道自己要做的原子子步骤,整个迁移过程实现零感知。
迁移过程包含五个原子子步骤:先在节点 4 上创建新部分,再将新部分加入到原本的数据复制同步组中,去 掉的副本的状态设置为 off line,最后再把该副本删除。在分布式数据库中随时可能有节点挂掉,但通过步 骤划分,无论是 MC 挂掉还是 TDStore 挂掉,节点拉起来后都知道要如何操作。通过这样的机制,存储层 对每个任务的推进或回滚完全无感知,全部由 MC 来做协调。
该机制主要有以下三方面的效果:
首先是性能。该机制对性能提升的促进非常显著,在集群比较大时可以轻松支持 EB 级存储。比如在 500 万 数据分片的量级下,MC 用 20 个核就能完全支持。通过数据状态与调度状态的分离,大大降低了 MC 负载。 性能上的收益还体现在存储层上。在任意时刻它只需要接收到一个原子步骤即可。因此在代码层面,存储层 不需要任何关注数据资源状态的代码,更有利于进行性能优化。
其次是健壮性。每个任务都是有限的状态机,任意一个参与者,如管控或存储,出现交互中断,都能够以确 定方式进行任务的回滚或恢复。
最后是可扩展性。每个管控任务分为多个原子步骤进行,有利于以插件式方式去定义其他更多更复杂的任务。 整个过程中只需要将原子步骤拼装组合,MC 就可以实现复杂调度。
原始版本中,MC 对数据分布管理只有物理位置概念,基于扩展引擎和分布式协议打造的 KV 存储引擎,数 据分片在整个分布式存储集群中按照主键从空到正无穷的字符序来进行分布。比如创建表或二级索引时,如 果要表达成 KV 形式,主键和二级索引都有对应的 ID 。存储层中以 Key 区间代表一个数据分片,如 01-02 数据分片,落在存储节点 1 上,02-03 数据分片,落到存储节点 2 上。这种情况下,同一张表的数据的主 键和二级索引会落在不同的 TDStore 上,这就会造成很大的负面影响。
举个例子,有一张表,每天有大量不同的流水写入,有三亿行数据,业务为方便查询,做了 20 个索引。在 最坏的情况下,20 个索引分别落在不同的 Region ,又落在了不同的 TDStore 。数据库使用者从操作上更 新了一行,但可能会发展成 20 个涉及到 60 个参与者的分布式事务,带来 60 次同步的性能损耗。在这种情 况下,我们针对经常出现的业务场景对两阶段提交进行优化,让更多的提交变成一阶段提交。
我们设立表内数据的概念,每个数据在物理层面都可以知道每个 Key 落在哪个 TDStore,但无法感知到它 们属于哪个二级索引。对此我们需要建立关系去创建表,使得在创建表和索引时,MC 可以感知到每个 Key 在物理意义上属于哪个 TDStore ,逻辑意义上属于哪些表的分区、属于哪些表的二级索引。
我们还引入了复制组的概念。在原始版本中,每个数据分片是一个复制组,现在则是将多个 Region 归属于 一个复制组,通过管控体系架构的改变,将表数据和二级索引放在同一复制组里。这种做法的好处有两方面: 一方面,业务中常常按照分区键来划分事务,一次性修改的数据非常大,可能只落在同一复制组里,这时需 要进行多次网络交互才能完成一个分布式事务,优化后只需要一次落盘即可完成;另一方面的好处是计算下推,由于计算层可以感知到要写的主键、二级索引都在同一 TDStore 的同一复制组内,就可以提前将逻辑 下推到存储层中完成。
接下来解决的问题是表与表之间的亲和性。在部分系统中,以一定规则如哈希去分区的表结构中,在更新表 1 分区时,也会去访问表 2 的 1 分区。这就要求管控层必须理解表与表之间的概念。如果能感知到它们是在 同一组事务里被操作的单位,就可以更好地改善事务的两阶段提交。
对此我们提供了一个扩展语法。假如用户有需求,可以去指定他所倾向的数据分布策略,为该策略命名,允 许在该分布策略里再指定分区策略。如下图所示,当下面第三行创建表时,如果有两个表在业务场景中经常 被访问,就可以将它们关联到同一 DP 组里,MC 会在后台创建表。所有的分布策略都会通过 MC 进行, MC 可以在后台将这些关联的表做背后的调度优化。这就为更多跨表之间的操作提供较多的可能性。
未来我们仍有许多挑战需要克服。首先是全局事务时间戳,目前 MC 承担许多的管控操作,后续我们计划 将其设计成多进程模式,全局事务时间戳独占一个进程。其次是 Lieutenant 分流,我们计划增加副队长角色, 分流部分网络。最后是数据亲和调度的利用也是我们未来会去重点攻坚的领域。
出处:InfoQ 腾讯云技术实践精选集2021