轻松驾驭 EB 级千万 QPS 集群,TDSQL 新敏态引擎元数据管控与集群调度的演进之路
日前,TDSQL 新敏态引擎正式发布,支持无限扩展、在线变更,可以完美解决对于敏态业务发展过程中业务形态、业务量的不可预知性,高度适配金融敏态业务。
该引擎 100%兼容 MySQL,计算/存储资源均可独立全透明弹性扩缩容,实现了 PB 级存储的 Online DDL;计算层每个节点均可读写,轻松支撑千万级 QPS 流量,可以有效应对业务的变化。针对海量数据存储的场景,实现最高 20 倍压缩率的超高压缩比存储能力,大幅节省资源成本。其独有的数据形态自动感知特性,使数据能根据业务负载情况实现自动迁移,打散热点,降低分布式事务比例,获得极致的扩展性和性能。
计算存储分离和管控数据分离的体系架构,为 TDSQL 新敏态引擎带来了动态扩展上的灵活性,但同时也带来了在集群海量元数据管控和多维资源调度上的挑战。
本期将由腾讯云数据库专家工程师唐彦,为大家深度解读 TDSQL 新敏态引擎元数据管控与集群调度的探索实践,主要讲述 TDSQL 在海量元数据管控、复杂资源调度方面的架构原理,以及线上应用场景中在高性能和高可用方面的实践。以下是分享实录:
# TDSQL 架构介绍
TDSQL 最新发布的新敏态引擎技术架构,它描绘了一个计算/存储分离、数据面/管控面分离的高可用的原生分布式架构的关系型数据库。
TDSQL 分为三层:绿色部分是计算层,称之为 SQLEngine;紫色部分是管控层,简称为 MC,负责整个集群的管控调度工作;蓝色部分是存储层,称为 TDStore。
TDSQL 有三个重要的架构特性:全分布式,无论在计算层还是管控层,每一层都是分布式的架构;计算与存储完全分离,每一层均可独立动态扩缩容;数据流与控制面完全分离,关键读写路径与资源调度逻辑互不影响。
**TDSQL 的四个主要功能特点:**
● 高度兼容 MySQL。对于从单机 MYSQL 迁移过来的业务,TDSQL 实现高达 100% 的兼容,对业务层无入侵,能够实现无感知迁移。
● 高可扩展性。在存储层和计算层,用户只需手动在管理界面上添加一个存储节点或计算节点,后续内部的管控机制会自洽地完成整个扩缩容流程。
● 支持原生 Online DDL,可多写架构下以原生方式实现 Online DDL。
● 全局读一致性。TDMetaCluster 统一分配全局唯一递增事务时间戳,实现金融级场景下的数据强一致。
分布式架构主要分为计算层、存储层和管控层。
首先是计算层。TDSQL 计算层最大的特点是多主架构,每个节点都支持读写,完全相互独立;每个计算节点为无状态,具备扩容优势,可高度兼容 MySQL 8.0,具备无状态化的弹性扩容的架构特性。
其次是存储层 TDStore,它是我们自研的分布式 KV 存储引擎。在存储层和计算层的交互中,存储层承担事务协调者的角色。我们以一定方式将数据打散到每个存储节点上,每个数据分片称为 Region。存储层对所有数据的状态自身无感知,只负责数据的读写,所有数据的调度都由它与 MC 的交互来进行。
最后是管控层。它是一个分布式的集群,以一主 N 备的方式去部署。在整个集群中,它要同时承担管控层面和数据层面的工作。比如在数据层面,MC 负责全局唯一且严格递增的事务时间戳的分配,同时负责管理所有的计算节点、存储节点的元数据、MDL 锁等等。
**以下是数据库中常见的主功能流程:**
● 分布式事务。TDSQL 天然支持分布式事务的特性。整体流程为:从 MySQL 客户端发送请求,通过计算层对存储层进行读写,读写过程中,存储层和计算层都会去和 MC 交互,获取时间戳,最终确定每个事务之间的偏序关系,完成整个流程。
● 无感知扩缩容。当存储空间不够时就需要扩容。在扩容过程中,必然会涉及到数据的搬迁。如下图例子所示,整个集群中只有一个存储节点,当需要扩容时,可以在界面上点击多购买一个存储节点。此时存储节点上数据的分裂搬迁、计算层对最终数据路由的感知、计算层感知路由的变化后完成的重试等过程可以完全自洽地包含在整个数据库体系中,实现业务层无感知。
● Online DDL。TDSQL 的分布式体系架构采用多写架构。为达到更好的并发性能,需要在多写的架构下,实现 Online DDL。相较同类产品,当业务尝试在运行过程中加一列、加一个索引时,需要借助外部的工具,及堵塞业务来完成 DDL 的操作。为了更好的用户体验,降低 DDL 操作对正常业务读写的影响,TDSQL 实现了在多写架构下以原生方式支持的方式去实现 Online DDL 操作的完成。
# TDSQL 管控层在分布式场景下的挑战
**TDSQL 架构的三大功能特性:**
● 原生分布式,全部都是分布式,没有中心节点管控。
● 存算分离,计算跟存储完全分离,不在一个服务器上。
● 数据跟管控完全分离,数据层面完全不参与数据管理。
这些特性从字面上看起来都比较直接、简单,但我们却在工程落地时遇到诸多的技术挑战,总结起来主要是高性能、高可用、复杂调度三个方面。下面将着重分享我们在高性能、复杂调度方面遇到的挑战。
首先是高性能方面的挑战。在架构上,要做到分布式的完全存算分离的架构,MC 作为集群内唯一一个中心的管控模块,必须承担全局授时源角色。
在分布式事务的整体架构图中,可以了解到 MC 在事务过程中需要和存储层做网络交互,提供时间戳,这是关键路径。同时 TDSQL 的计算层和存储层都可以灵活扩缩容。存算分离、高扩展的两个特性也意味着 MC 必须要具有非常高的性能。
在复杂调度层面。我们设计成数据和管控完全分离的架构,数据完全存储在 TDStore 上,只负责数据流的读写。其他工作完全交由管控层去进行,MC 只需要监控整个集群的状态做出关于存储资源的决策。
# TDSQL 元数据管控挑战与实践
**3.1 高性能方面的探索与实践**
在高性能方面,我们采用非常经典的三段式协程架构,即一个协程收、处理、最后再发。这种架构在我们突破 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 在时间戳方面是单一的提供者,而集群中众多的计算节点和存储节点会产生源源不断的请求,因此在分析生产者和消费者速度时,我们发现生产者速度远远跟不上消费者速度。为此我们进行了简单的改造,即来一批请求再消费一批时间戳,以批量请求方式去唤醒消费者。为了适应业务场景,我们还对该优化做了开关,运维人员可以根据业务场景的需求进行调节。执行批量化操作后,整体峰值已经提升至两千万,许多数据库实例的业务场景都无法到达这种高压力。
**3.2 复杂调度方面的探索与实践**
由于实现数据面与管控面的分离,MC 要负责整个集群所有跟资源相关的管控。如图所示,图中画的就是 MC 的主要功能。
MC 需要负责时间戳的提供,管理全局的唯一 ID 的分配、DDL 的协调、计算层管控层资源的元数据以及数据分片的管理。在管控层的不同层面,所有跟管理调度相关的工作都集中在 MC。
为了实现复杂调度,我们首先划分资源层级,制定可用的具有可扩展性的基础框架,将存储模块从管理任务中释放。MC 必须将每个资源层级划分清楚,使得数据路径上的所有模块只需要被动执行,不需要关心数据的状态。我们从集群层面、复制组层面和副本层面进行划分,划分出许多子状态及子步骤。
比如在扩容过程中,有一个数据分片,副本分布在 123 三个存储节点中,如果要进行数据迁移使得一主两备变为 124 的分布模式,在整个过程中,任意时刻这四个节点都知道自己要做的原子子步骤,而无须感知到整个迁移过程。
迁移过程包含五个原子子步骤:先在节点 4 上创建新部分,再将新部分加入到原本的数据复制同步组中,去掉的副本的状态设置为 offline,最后再把该副本删除。在分布式数据库中随时可能有节点挂掉,但通过步骤划分,无论是 MC 挂掉还是 TDStore 挂掉,节点拉起来后都知道要如何操作。通过这样的机制,存储层对每个任务的推进或回滚完全无感知,全部由 MC 来做协调。
**该机制主要有以下三方面的效果:**
首先是性能。该机制对性能提升的促进非常显著,在集群比较大时可以轻松支持 EB 级存储。比如在 500 万数据分片的量级下,MC 用 20 个核就能完全支持。通过数据状态与调度状态的分离,大大降低了 MC 负载。性能上的收益还体现在存储层上。在任意时刻它只需要接收到一个原子步骤即可。因此在代码层面,存储层不需要任何关注数据资源状态的代码,更有利于进行性能优化。
其次是健壮性。每个任务都是有限的状态机,任意一个参与者,如管控或存储,出现交互中断,都能够以确定方式进行任务的回滚或恢复。
最后是可扩展性。每个管控任务分为多个原子步骤进行,有利于以插件式方式去定义其他更多更复杂的任务。整个过程中只需要将原子步骤拼装组合,MC 就可以实现复杂调度。
**3.3 数据分布方面的探索与实践**
原始版本中,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 的 1 分区时,也会去访问表 2 的 1 分区。这就要求管控层必须理解表与表之间的概念。如果能感知到它们是在同一组事务里被操作的单位,就可以更好地改善事务的两阶段提交。
对此我们提供了一个扩展语法。假如用户有需求,可以去指定他所倾向的数据分布策略,为该策略命名,允许在该分布策略里再指定分区策略。如下图所示,当下面第三行创建表时,如果有两个表在业务场景中经常被访问,就可以将它们关联到同一 DP 组里,MC 会在后台创建表。所有的分布策略都会通过 MC 进行,MC 可以在后台将这些关联的表做背后的调度优化。这就为更多跨表之间的操作提供较多的可能性。
# 未来机遇与挑战展望
未来我们仍有许多挑战需要克服。首先是全局事务时间戳,目前 MC 承担许多的管控操作,后续我们计划将其设计成多进程模式,全局事务时间戳独占一个进程。其次是 Lieutenant 分流,我们计划增加副队长角色,分流部分网络。最后是数据亲和调度的利用也是我们未来会去重点攻坚的领域。
评论