今天分享的题目是 MatrixOne(以下简称 MO) 如何基于 K8S + S3 构建 HTAP 数据库,实际上也可以说是如何面向云来构建一个全新的 HTAP 系统。之所以把 K8S 和 S3 放到标题里,是因为我们认为这是云原生时代对于数据库架构影响最大的两项技术。
接下来的分享分成三部分,首先聊聊我们对云的理解,尤其是云给数据库设计带哪些机遇和挑战;其次描述我们如何面向云的特性,从零开始设计一款全新的 HTAP 数据库;最后,将分享我们的云服务 MatrixOne Cloud 的设计。
云的机遇与挑战
关于云,我主要讲一讲我在开发过程中自己的体感。云的计算资源有三个特点:
- 按需使用,按量计费;
- (在一定规模内)近似无限资源;
- (几乎)覆盖所有规格;
这几点组合在一起,无限资源和按量付费意味着算力的无限弹性伸缩,反正想扩容给钱就行,缩容后开销归零。而按量计费和规格齐全意味着我们不仅可以在广阔的搜索空间内找到最适合业务负载的硬件配置,还能根据负载变化不断去调整硬件配置。这两点结合起来,我们在一定规模内就能安全地假设云基础设施的扩展性没有上限。那么回过来看,应用的扩展性又怎么样呢?目前我们看到的是无状态应用已经能够很好地进行扩展,而有状态应用,尤其是数据库应用的扩展性还有很大的提升空间。举个例子,发轫于虚拟机和块存储时代的 shared nothing 架构虽然在理论上具有无限水平扩展的能力,但往往要提前规划好数据分片(partition),而无论是范围分片(range partition)还是哈希分片(hash partition)动态调整副本数时会涉及到昂贵的分片迁移操作。这样的设计在上下线机器频次很低的数据中心不是问题,但在云上就白白浪费了基础设施层面天然的动态伸缩能力。
其次是存储,重点当然在 S3,也就是对象存储,因为 S3 已经几乎成了云上对象存储的代名词,下面我们就以 S3 来指代对象存储。关于 S3 的讨论已经非常多了,我在这里再说一遍未免有点老调重弹,但为了完整性我们还是看一下对象存储对上层应用的关键影响:
- 👍11 个 9 的可靠性,无限的共享存储空间;
- 🤔指数级的存储费用降低和 IO 费用提升(相比块存储);
- 😣长尾延迟高,性能抖动严重。
S3 的可靠性和无限的共享存储空间以一个独特的角度解决了设计数据库时的存储扩展性问题:消灭问题,直接把存储层扩展性交给另一个系统。而与块存储截然不同的成本模型(存储便宜,IO 极贵)以及性能模型(高吞吐、高延迟)则决定了 S3 并不是“低垂的果实”,直接将面向块存储设计的存储系统搬到 S3 上得到的往往是性能下降和账单爆表的双输结果。
最后是 Kubernetes(K8S),我认为 K8S 和云并没有必然的联系,但公有云、私有云、混合云齐头并举的繁荣生态天然需要一个能够封装所有云基础设施的事实标准,而 K8S 恰好是这段历史的选择。MO 从第一天开始就将 K8S 作为首选的运行环境,这里当然有很多技术上的原因,但最重要的是所有的基础设施都在对接 K8S,而应用去适配 K8S 也就通过 K8S 这层抽象适配了所有基础设施。当然,尽管 K8S 几乎可以说是一个”被动“的选择,我们也确实得到了很多 K8S 的利好。比如说,K8S 天然支持以 CRD 和自定义控制器扩展集群的自动化运维能力,这我们得以将 MO 的应用层资源伸缩和配置调整与 K8S 资源层的调整紧密整合起来,打造出第三部分要说的 serverless 能力。
回到 MO,我们自称 MO 是一个云原生的数据库。对我,作为一个开发者来说,这里”云原生“的含义就是在设计 MO 时,我们把上面说的计算资源、S3、K8S 作为了基础假设。那接下来一起看看 MO 的架构是怎样扬长避短,充分利用好云的这些特性的。
MatrixOne 架构
这是一张简化的 MatrixOne 架构图,分成三个模块,我们逐一分析。
对象存储(Object Storage)是 MO 的设计基石之一。MO 选择将全量数据存储在 S3,让所有数据可以全局共享,无限扩展。但刚刚我们也看到,S3 的性能和成本问题都需要应用层进行处理,这就引入了 MO 的事务层。
事务层(Transaction Layer)由日志服务(LogService)和事务节点(TN, Transaction Node)组成。从一个外部视角来看,事务日志实际上就是数据库,因为只要有全量的事务日志,就可以随时还原出状态并保证数据完整性。因此,一个事务只要追加到了事务日志内,我们就认为事务提交成功,修改被安全地持久化了。为了确保事务日志的持久性(Durability),我们基于块存储,采用成熟的 Raft 3 副本方案存储设计了日志服务来存储 MO 的事务日志。那熟悉 Raft 的朋友可能就会问两个问题,一是三副本高性能块存储成本怎么压得住,二是单 Raft 组吞吐够吗。这就构成了我们的下一个设计决策,即事务层必须足够轻量。事务层的 TN 节点仅决定事务是否可以提交,将提交的事务写入事务日志,并异步刷入 S3,而在某一个事务时间戳之前的数据都持久化到 S3 之后,TN 就会往 S3 写入一个检查点(checkpoint),并删除检查点之前的所有事务日志。通过这样的设计,LogService 只需要存储最近一小段时间的最新事务日志(MO 称之为 LogTail),实际的生产实践中,只需要购买数十 GB 的高性能块存储就能满足 LogService 的存储需求。
另一个听起来可能反直觉的设计是 MO 的 TN 目前是单副本运行,这也和事务层轻量化的设计相关。因为 TN 是无状态的,每次故障重启后都只需要拉取一部分 S3 上的原数据并在内存中回放 LogTail 就能开始工作,耗时在亚秒级,这个耗时和成熟的主备协议中的探活超时在一个量级上。换言之,TN 宕机重启是数秒不可用,基于 Raft 或其他协议也需要同样量级的时间探测到 Leader 宕机进行换主。当然,目前的设计对于滚动升级这样的有计划重启并不友好,因为有计划升级时我们可以主动迁移 TN Leader,消除停机时间。因此 MO 会支持 TN 以主备模式运行,让 MO 在不同部署环境下对于成本和可用性的权衡有更多选择空间。但这不影响我们这里要说明的重点,那就是整个事务层,包括事务节点和日志服务,都是非常轻量的。
正是依靠事务层,MO 将事务提交直接写入事务日志并异步刷 S3,规避了 S3 的长尾延迟和 IO 昂贵两个问题,而轻量化的设计也决定了事务层并不会成为性能瓶颈和成本黑洞。
最后是计算层,MO 的计算节点(CN, Compute Node)承担所有的计算工作,包括约束检测。CN 只需要读取必要的 S3 数据文件再叠加事务层的 LogTail 就可以在内存中还原出相关库表的最新状态开始处理计算任务,因此 CN 是完全无状态的。不同 CN 可以使用不同的硬件资源,执行不同的计算任务并且无限水平扩展。因此 MO 可以同时为 TP、AP 和 Streaming 提供合适且隔离的硬件资源,并随着负载变化自动调整这些资源,成为一套真正意义上的 HSTAP 系统。
在 CN 上,我们同样面临 S3 的性能和成本问题。因此每个 CN 都有一部分内存(一级缓存)和一块本地存储(二级缓存)作为 S3 的 cache,并且 CN 之间会通过 gossip 交换彼此的缓存信息。通过这些信息,CN 一方面能在分发计算任务时获得更好的缓存本地性(cache locality),另一方面对于数据访问,CN 也会优先到其他持有对应数据缓存的 CN 上拉取这份数据,假如没有任何 CN 持有对应数据,才访问 S3。每个 CN 的本地缓存构成了一个共享缓存(shared cache),这是计算层减少 S3 的 IO 次数,从而提升性能、降低成本的关键。
这里还有个坑没填,分析事务层的时候我们提到日志服务一个 Raft 组不做 sharding 吞吐量够不够。而 MO 既然是 HSTAP 系统,就不止要服务好高频但较小的 TP 写,也要能应对海量数据导入。这一方面依赖于日志服务内大量的工程优化,我们实测下来在 AWS 的 c7g 上用 io2 EBS 可以达到 800+MB/s 的事务日志吞吐。另一方面则依赖 CN 内写流程的自适应优化,CN 会按照事务的写入行数区分事务写和批量写,对于批量写,CN 将数据文件直接写入 S3,在提交事务时不再提交数据,只提交数据文件的 S3 句柄,这极大降低了数据导入对事务日志的吞吐需求。
整个架构串联起来,可以看到 MO 的计算层充分利用了云提供的基础设施弹性能力,事务层解决了 S3 在处理 TP 业务时的短板,这就是为什么我们在第一节说 MO 是面向云来设计的一个数据库系统。MO 是生于云,同时也志在成为一个可靠、经济的云原生基础设施,或许十年后,我再来分享,聊的就是怎么面向 MO 去设计一个云原生或者“MO 原生”的应用了。那下一节就来看看 MO 的云服务—— MO Cloud。
MatrixOne Cloud
MO Cloud 来自应用对云服务的需求,当然不是说现在云上没有好用的云数据库,但我们看到了两个机遇,一是真正能够在生产实践中做到一个系统解决绝大部分问题的 HSTAP 系统屈指可数,二是很多优秀的云数据库服务都有供应商绑定的问题。因此我认为 MO Cloud 的竞争力就在于上一节的 HSTAP 架构和这一节的多云支持,无供应商绑定上。我们看看 MO Cloud 是怎么做的。
MO Cloud 和典型的多 K8S 联邦架构比较相似,分为一个中央控制面和可以任意添加的数据面。控制面和每个数据面都是一个 K8S 集群。
中央控制面负责全局管控和多云协同以及全局可观测性,MO Cloud 的微服务和编排系统内的中央控制器都运行在中央控制面内。
每个数据面实例在 MO Cloud 内被称为一个单元(Unit),每一个单元对应一套物理上的 K8S 集群。单元负责运行 MO 集群,一个 MO 集群可以运行在一个或多个单元上。每个单元都会运行一个称为 unit-agent
的组件来和中央控制面通信,不断拉取控制面下发的期望状态并在本地进行调谐实现。大家应该发现了,整套架构就像一个 K8S 架构的放大版,而 unit-agent
就类似 kubelet。因此,这套架构也像 K8S 能够管理异构的节点一样能够轻松扩展异构的单元:不同云上的单元都可以以统一的方式接入 MO Cloud 的编排系统,对接特定基础设施的复杂度都留在单元内解决。这意味着 MO Cloud 天然就有多云和跨云支持,也能支持纳管用户自己的私有云(Bring Your Own Cloud)。
在具体实现上,unit-agent
会在每个单元内拉起一组通用的控制器和一组面向特定基础设施的控制器,这些控制器共同完成基础设施和 MO 集群在单元内的生命周期管理和自动化运维。
单元内的每套 MO 集群都多租户的,因此我们在图上可以看到给一些颜色不同 CN,这些就是不同租户的独享 CN。不同租户的计算资源通过这种方式进行物理隔离。
其次是前两节我们说到 MO 的 CN 节点支持异构和弹性伸缩,这当然是优势,但也带来一个问题就是容量规划的复杂度很高。定一个带有余量的硬件配置是可行的,但除非业务是非常均匀单一的,否则固定硬件将带来很多资源浪费。因此 MO Cloud 的典型服务模式就是无服务器(Serverless),用户只为 SQL 付费。实际上我们都知道,无服务器并不是真的没有服务器,而是服务器不需要用户来管。MO Cloud 需要做无服务器的逻辑不仅仅在于帮助用户做了服务器管理的事情,更是因为数据库系统声明式查询的特性天然决定了 MO 内部拥有比用户更多的信息,具体来说就是数据库的统计信息和查询计划的信息,来决定当前和接下来一段时间内最优的服务器配置应该是怎么样,并不断去调整配置。
其中最典型的一个场景是缩容到零,也就是一个租户已经没有业务了,这时候就直接关闭这个租户的所有 CN。反过来,当租户再次发起一个数据库连接,MO 需要立刻再为租户分配一个独享的 CN。
为了应对这样的场景,MO Cloud 总是会打开 MO 的 Proxy 组件,Proxy 负责处理用户会话(SQL Session)。在从零弹出的场景下,Proxy 会从一组池化的 CN 内拿一个闲置的 CN 节点,进行一些标签信息的调整,将它标记为这个租户的独享 CN,再使用这个 CN 开始服务用户的会话。这套逻辑不仅适应从零弹出,也适应要为特定的负载临时弹出一个或一组 CN 的场景。
同时,Proxy 也负责弹性伸缩和配置变更时 MO 对外服务的质量。由于 MySQL 会话是有状态的, MySQL 协议也没有会话迁移的设计,因此 MO 要保证 CN 的变配不影响用户的长连接就必须使用一个类似 Proxy 这样的轻量、稳定的组件维持与用户的会话,再由 Proxy 实现 Proxy 到 CN 之间的会话迁移。举个例子,在缩容时,Proxy 会逐步将待缩容 CN 上的会话迁移到剩余的 CN 上,迁移完成后再下线这个 CN。这样的设计保证了 MO 内部为了适应变化的负载而做出的频繁变配行为不会影响用户的连接和查询。最终让 MO Cloud serverless 所蕴含的“替用户管服务器”这件事能够做得平滑而高效。
总结
回顾一下,今天我从云开始,以典型 S3 和 K8S 这两项技术作为切入点,跟大家分享了我们眼中云带来的范式变化。目前上云已经不是一件新鲜的事了,而要充分发挥云的潜力(尤其是对数据库系统而言)需要面向云去重新做设计。接下来,我们看到 MO 怎样用轻量级的事务层和无限扩展的计算层实现对云能力的扬长避短,重走了一遍造出一个云原生的 HSTAP 数据库内核的历程。最后,我们一起了解了 MO Cloud 设计原则以及多云支持、Serverless 的技术细节。在今天,无状态应用的云原生化已经非常成熟,而我们认为数据系统也将不再例外。生于云,成为云,这就是 MatrixOne 和 MatrixOne Cloud。