作为云原生技术先驱,腾讯云数据库内核团队致力于不断提升产品的可用性、可靠性、性能和可扩展性,为用户提供更加极致的体验。为帮助用户了解极致体验背后的关键技术点,本期带来腾讯云数据库专家工程师王鲁俊给大家分享的腾讯云原生数据库TDSQL-C的架构探索和实践,内容主要分为四个部分:
本次分享主要分为四个部分:
第一部分,介绍腾讯云原生数据库 TDSQL-C 产品架构,包括产品的研发背景和架构主要特性;
第二部分,分享用户场景实践,针对线上真实的用户场景做一些分析和针对性实践;
第三部分,分享系统关键优化;
第四部分,分享产品未来演进。
TDSQL-C 产品架构
背景
腾讯云原生数据库最初采用的是传统架构,也就是 MySQL 实例,或者说是采用 Binlog 复制的主备方式的架构。但这种架构在现有的一些用户需求来看是有很多问题的。
比如存储容量。传统架构实例的存储上限受限于本地磁盘上限,一般是几百 G,或者几个 T 的量级,做扩展的成本非常高而且麻烦。当用户数据非常多时,会做分库分表,使用现有的分库分表中间件或解决方案会带来一些分布式事务的问题。
做业务的同学知道,分布式事务处理起来会比较麻烦,涉及如何应对故障,如何应对分布式事务产生的数据不一致等问题。用户在存储容量方面的需求是实例容量大于 100T,并且存储容量能够快速透明的扩展。
其次是可靠性。传统架构基于 BinLog 复制,普通的异步或者半同步的复制方式可能会丢数据,同步的复制方式性能损失会比较大。用户在可靠性方面的需求是第一不能丢数据,即 RPO 等于 0;第二数据是有多副本容灾的,也就是要达到一定程度的数据可靠性。
此外还有可用性,可用性对于用户来讲就是服务有多长时间不可用,比如在传统架构发生一次 HA,或者宕机重启,这段时间服务都是不可用的。HA、恢复时间慢对用户来讲很难接受,传统架构 HA 或者副本的恢复速度可能达到了分钟级。第二个问题是基于 BinLog 复制的时候,主备副本的延迟比较高,有些可能达到分钟级,甚至达到小时级。用户希望能够快速切换 HA,实现秒级恢复,还包括回档功能,如果有副本,希望副本的延迟能够比较小,最好是秒级以下,甚至是毫秒级。
最后是可扩展性。传统架构的扩展性是非常复杂的,基于 Binlog 创建只读副本也会非常复杂,要先把原始的数据给复制过来,然后搭建主备同步,只读副本才能开始工作,这个过程至少是分钟级甚至是小时级别的。对用户来讲,他们希望当读需求有扩展性需求的时候,可以实现秒级的读副本扩展。
我们针对这四个方面的用户需求,采用了存储计算分离架构,这也是 TDSQL-C 所采纳的核心的架构想法。
简单来说,要解决存储容量和可靠性方面的问题,第一我们会用云存储,云存储之间是可以水平扩展的,理论上它的容量是无限的,而且对于每一份数据都有多副本来保证可靠性。数据分散在云存储的各个节点上,在这个基础上可以做持续备份,并行回档等功能。
在可用性方面,数据放在云存储上之后,数据的分片是可以做并行恢复的,回档也可以做并行回档。物理复制的时延一般会比基于 Binlog 的逻辑复制更低一点。
最后在可扩展性方面,共享存储的优势更加明显,当新建一个只读副本的时候,数据不需要复制一份出来,因为数据是在云存储上作为共享数据存在的,只需要把数据共享,另外再构建增量的数据复制就可以了。
架构特性
上面这张图是 TDSQL-C 的整体架构,从这个架构中我们可以看到,它整体上分为上一层的计算层和下一层的存储层。
计算层有一个读写节点,可以提供读写请求,还有多个只读节点,可以提供读请求,也就是图里边的 Master 节点和 Slave 节点。当读写请求,尤其是写请求进来以后,Master 节点也就是读写节点产生数据的修改,然后它会把修改产生的 InnoDB 的 Redo Log 下传到整个存储层,同时把 Redo Log 分发到自己的 RO 节点。
存储层负责管理数据,当产生的 Redo 日志发送到存储层之后,它可以负责 Redo 日志的回放,Segment 把它存储的页面对应的 redo 日志 apply 到自己的页面上来。整个存储层是架设在 COS 存储服务上。
TDSQL-C 的存储可以自动扩容,最大支持超过 1PB 的容量,目前我们的产品最大支持到 96CPU 和 768GiB 的规格。性能方面,只读大概能跑到一百万以上 QPS,写性能也能超过 40 万 QPS。
基于这种共享存储的架构,我们可以做到秒级故障切换,包括秒级的快照备份和回档,并且因为存储层本身可以做弹性,计算层也可以做弹性,所以可以实现一定程度的 Serverless。
此外,当需要扩展只读的时候,可以很容易的增加只读节点,TDSQL-C 现在最多可以挂 15 个只读节点,并且在读写节点和只读节点之间只有毫秒级的延迟。因为整个工程是基于 MySQL 代码库演化过来的,所以是百分之百兼容 MySQL 的。
场景实践
接下来,我会介绍并分析几个比较典型的场景实践。
Serverless
上图描述的是一些业务预测未来一段时间的数据存储或者数据计算的需求是持续上涨的,但实际上可能真实的用户需求是图中灰色的曲线。为了做好服务,用户要提前买好服务库实例,比如图中红色的折线,一开始就要准备好这种规格的数据库实例。
这种情况有一个坏处,实际买的规格都是比真实需求偏大的,这就会造成存储资源或计算资源的浪费。如果某些时刻有突发的流量进来,突然对存储或者计算的资源要求非常高,就会出现机器实例资源跟不上、规格太小等情况,这会对业务造成很大的影响。
我们认为理想的情况应该是图中蓝色的曲线,这个曲线的整个资源容量跟真实业务需求的变化规律是一样的,并且总是比真实的需求稍微多一点。这样就能真正把资源充分利用起来,而且最大程度上降低成本开销。
在一些真实的例子里,我们发现有些业务是开发测试场景,业务真正上线之前,会做一些系统的测试开发,这个过程对数据库的需求频率是非常低的。还有一些像 IoT、边缘计算、SaaS 平台,他们的负载变化有非常大的规律性,白天压力比较大,但是晚上压力比较小。
TDSQL-C 做到了智能极致的弹性,能够根据负载来快速启停实例。第二是按需计费,用了多少花多少,不用不付费,可以做到按秒的计量,按小时的结算。
弹性扩容
另一个我们在线上业务中发现的实践需求是弹性扩容。有些业务每天都能产生大量的数据,比如有的业务一天产生几百 G 的数据,并且这种业务对单库的容量要求很高,通常都是几十 T,甚至上百 T 的数据。
有些场景,像开发测试场景,开发完或者某一次测试完,数据库直接就删掉了,生命周期非常短。另外有些历史库场景,历史数据存储只是存储最近一段时间的,特别老的数据直接就删除了,删除了这些数据希望空间立刻回收,不要再产生存储成本了。
TDSQL-C 可以做到按需扩容,存储根据操作页面按需扩容,如果产生的数据比较多就扩展出来,不需要预先规划好要做多少存储。第二点是自动回收,有一些空闲空间,比如数据已经删除了,这些数据实际不需要了,但是传统的 RDS 是逻辑删除的,这块可能还会继续产生费用,TDSQL-C 可以做到按实际的容量来计费。
备份回档
很多场景对备份回档要求比较高,比如金融行业,因为金融行业对数据安全关注度非常高,他们对备份的速度和备份的时效性都有很高的要求。还有像游戏业务,可能会涉及到频繁的回档,所以对备份回档的速度要求也比较高。
回档作为“后悔药”,对很多业务来讲都是很重要的一个功能,用户可能会产生一些误操作。
TDSQL-C 可以做到持续备份,存储分片可以根据备份点进行并发的独立备份,同时可以做到设定全局的一致性备份点来进行备份。此外,TDSQL-C 也可以做到并行回档,每一个分片并行回档各自的数据的全量和增量的备份,并行回放自己的日志。还有 PITR,也就是可以快速的恢复到数据库的任意时间点的数据的状态。
系统关键优化
极速启停
第一个优化就是前面提到的,如何做到极速启停,也就是支持更好的 Serverless。
这边有个测试数据,第一个测试数据叫停机时间,指的是计划内的停机时间,即主动停机。TDSQL-C 跟传统的 RDS 数据库对比,传统 RDS 数据库停机需要 26 秒,TDSQL-C 可以做到 3 秒内停机。
第二个是启动时间,就是停机之后重新把数据库实例拉起来,大概需要多少时间能够恢复起来,RDS 需要 48 秒,TDSQL-C 可以做到 4 秒就启动起来。
我们分析了一下,这 48 秒有 21 秒是在做事务恢复,也就是第三个柱状图,我们对事务系统的并行初始化、表锁恢复做了一些优化,可以把恢复时间降到一秒。
第四个指标叫性能恢复时间,这个指的是比如重启之前数据库的 QPS 大概跑到了 20 万 QPS,重启之后大概需要多长时间才能重新恢复到 20 万 QPS。这对很多业务来说都是很重要的,是恢复质量的问题。传统 RDS 在有些场景下需要 260 秒才能恢复,但是用 TDSQL-C 3 秒就能恢复了。
这里我们做了一些优化,用的**独立 BP **的优化方式。Buffer Pool 跟数据库实例进程是解耦的,由这台机器上的另外一个进程来负责管理这块内存,当数据库实例重启之后,Buffer Pool 是可以继续用的,这种方式就避免了重启之后,整个 Buffer Pool 都是冷的,需要很长时间慢慢预热,省了这个过程,所以恢复时间会非常快。
二级缓存
另一个系统关键优化是二级缓存。二级缓存是 TDSQL-C 在存储计算分离架构下做的比较创新的优化,也是对架构的一个重要补充。
如此前所说,存储层是可以水平扩展的,这意味着数据量膨胀了很多倍,可能几十倍、上百倍。数据量多了,但是计算层计算节点的 Buffer cache,也就是 InnoDB 的 Buffer Pool 的容量并没有太大的变化,这就意味着需要用以前相同大小的 Buffer Pool 来服务更多的数据。
大家知道 InnoDB 的 Buffer Pool 在一定程度上承担了读缓存的作用,服务更多的数据,意味着读缓存的效率可能会下降。以前一些并不是 IO Bound 的场景,在这种数据量大了的场景下就变成 IO Bound 了,或者以前本来就是 IO Bound 的场景,IO Bound 更严重了,这样对性能影响还是比较大的。
其次,传统 RDS 的 BufferPool 和本地磁盘空间的存储中间,是没有其他硬件存储设备的。但是在存储计算分离架构下,Buffer Pool 可能跑得非常快,它的 IO 延迟很低,但数据是存放在远端的机器上的,我们这边叫 Remote IO。需要通过网络访问其他机器的 SSD,在这之间有至少两个层次的硬件存储设备,一个是 SSD,就是本机硬盘,另外一块是 Persistent Memory,就是持久化内存,这些是我们可以利用起来的。我们把这一类存储用作 secondary cache,通过这种方式能够有效的减缓 IO Bound 场景下 Buffer Pool 命中率低的问题,因为我们可以缓存很多的热数据,能够加速数据的访问。
通过测试可以看到,随着数据量的增大,整个性能提升还是比较明显的,在很多场景下性能可以提升到百分之一百以上,达到一倍多。
当然这个问题也有其他的解决方案,有些产品用的是水平扩展 DRAM,类似于我们的 Buffer Pool,就是把 Buffer Pool 放在远端的机器上,通过更好的 RDMA 来访问这部分内存,而且这个内存可能分散在多台机器上,这种方式也能减缓 IO Bound 场景的一些开销。
但相对来讲,我个人认为使用 Secondary cache 这种方式系统的整体应用成本更低一点,因为毕竟内存的成本比 SSD 的成本高的多,尤其是非易失性内存,像 3D Xpoint,慢慢流行起来之后,价格是慢慢降低的。用二级缓存的方式,整体的实例成本能够下降非常多。
极致伸缩
还有一个优化是极致伸缩,我们把存储功能下放到存储层之后,存储层会有存储池这样一个概念。
有一些逻辑跟传统 RDS 方式是类似的,比如段管理还是以 1M 的 extent 为粒度来管理。也有些逻辑有很大的差异,比如我们把存储空间的扩展,整个 offload 到存储层,整个空间都池化。当我们发现某个 extent 里面的所有页面都回收了之后,变成了一个 Free Extent,就可以物理上真正的把它删除,而不只是标记为删除。通过这种方式真正删除之后,客户的存储成本就降低下来了,也就是能够实现按需计费的能力。
极速备份回档
极速备份回档的实现包含两部分:备份、回档。
此前提到,我们的数据是分散到分布式存储组件上的,分布式存储包含很多的存储节点,而且它本身还有一定的计算能力。当实例需要做备份的时候,每个存储节点都可以独自的去做备份,我们叫自治备份,它可以持续的做备份。当我们需要全局一致的备份位点时,可以由计算节点来负责协调,通过一些特殊的命令,或者日志来通知所有存储层的节点基于快照做一个统一的备份。
跟备份相反的一个操作是回档,基于备份再把实例的数据恢复到某个时间点,回档也是并行回档的,每个计算节点都可以独立的做自己的回放。
针对极速备份回档的测试数据看,1TB 的备份时间,RDS 实例需要 61 分钟,TDSQL-C 只需要 21 分钟就够了。1TB 的回档恢复时间,RDS 需要 168 分钟之多,但 TDSQL-C 22 分钟就可以恢复出来。
Instant DDL
还有一些优化是功能性优化,包含 Instant DDL 和并行构建索引。
Instant DDL 是 MySQL 8.0 新增的一个功能,它指的是在处理新增列,或修改列类型,或删除列 DDL 的时候,可以仅仅修改原数据就直接返回。
大概的原理是,比如这张表原来有三列,现在需要新增一列,变成四列,只需要在系统表里面标记一下,这张表就从原来的三列变成了四列。之后再新写入的数据都是按四列写入的,原来的数据在磁盘上存的是三列的,新插入的数据会打上新格式数据的标记,原来的数据是没有标记的,当用户读取的时候,返回客户之前根据标记来决定。如果是旧数据,我们就给它补一个新的列,一般补默认值 default value;如果是新列就直接返回,通过这种方式就做到了 O(1) 的 DDL,时间非常短。
并行构建索引
接下来介绍下并行构建索引,比如 create index,或者 optimize table 的时候,都会涉及到一些表的重建。
RDS 构建索引的时候,尤其是 8.0 的相对早一点的版本,都是单线程构建的。构建过程是先扫描所有的主表数据,扫描之后,根据扫描到的每一行主表数据,再根据索引信息,生成对应的索引行,这些索引行生成后存储到临时文件里面。
第二步是对这个临时文件按照索引行的索引键进行排序,一般是 mergesort,完成之后把它导入到一个空的 Btree 里,这样就完成了整个索引的构建。我们针对这块做了并行化优化,扫描主表、外排,还有把数据导入到 Btree,这三个过程都是可以并行化的。
在第一个阶段,我们基于 InnoDB 8.0 的 parallel DDL 做了并行扫描。第二步的 mergesort 我们也做了基于采样的并行化,通过这种方式来提升并行度。第三步构建 Btree 的时候,也是可以并行化的,比如产生了八万行的索引行,如果八并发,每一个并发线程负责一万行数据的构建。最后再把形成的八个子 Btree 合并成一个大的 Btree,再去压缩层高等等。我们测下来很多场景下能提升到两倍以上,还有很多场景可以提高的更多。
未来演进
第一个我们在探索的演进叫 Global Database。
如上图所示,左边有一个 Primary 实例,这个实例读写节点产生了 Redo log,Redo log 需要分发到存储层,我们现在新增了 Log Store 模块,它负责接收和分发日志,通过这种方式,Log Store 一定程度上可以提升日志的响应速度和整体 Redo log 的 IO 吞吐,进一步提升写性能。
另外一个很重要的点是 Primary 实例跟右侧 Standby 实例可以通过各自的 Log Store 来建立数据复制的链路。通过这种方式,相当于扩展出了一个只读节点,实现读扩展,而且是跨 Region 的读,因为 Standby 可以部署在另外一个 Region 上。另外我们通过这种方式实现了跨 Region 容灾,这对于很多金融业务来讲都是刚需。
另一个我们在探索的演进是计算下推。
根据我们的架构,存储和计算是分开的,计算在上面,存储在下面,存储不只有存储能力,还拥有一定的计算能力,像刚才提到的备份恢复,每个节点可以独立持续的做备份,就是利用存储层计算能力的一个例子。
除此之外还有很多业务逻辑也可以通过这种方式把存储层的计算资源利用起来,第一个是页面内计算下推。比如现在要做一个有条件的扫描,扫描到了某个页面,这个页面可能有一百行数据,满足条件的有五条,可以把条件下推到存储层,直接放在存储层做过滤,只把这五条数据返回给计算层就可以了,这就避免了把这一百行全部读到计算层,在计算层再做计算,减少了中间网络带宽的消耗。
第二个是 Undo 页面间的计算下推,我们 InnoDB 是支持 MVCC 即多版本的,举个例子,我们现在启动一个只读,这个读事务快照相对比较老,比如读一小时之前的,当我现在去读的时候,发现某一行数据太新了,不是我想要的那个数据,需要找到以前的版本。
这个过程在 InnoDB 里面,需要找到这一行对应的 Undo 页面,把它的前镜像找出来,要读的是 Undo 页面记录的前镜像,这个过程如果放在计算节点做,需要把原始的页面数据加载到计算节点,然后根据读快照把 Undo 页面找出来,再把 Undo 页面应用到数据页面上,产生一个旧版本的数据。这整个过程都可以在存储层来做,我们现在也是把这个下沉下来的。
还有一个下推叫写下推。比如我就改某个页面 Header 部分的前几个字节,或者 Page Header 的某个字段,这种情况很多是盲写的,不需要读出来,直接可以更新,这种情况也是可以下推的。
计算下推在存储计算分离的架构下是很自然的一个事情,刚才讲到了存储计算分离,它的存储层带有一定的计算能力,大量的计算实际上都是可以下沉到存储层的,哪些计算可以下推并没有很明显的边界。我个人觉得除了事务之外,大部分计算型的都可以下推到存储层,甚至可以把存储层的一些计算资源当成纯粹的计算资源,不关心它是不是存数据。