分布式数据库在抖音春晚活动中的应用
作者|马浩翔,火山引擎系统开发工程师
大家好,我是字节跳动火山引擎的系统开发工程师马浩翔,目前主要聚焦在分布式存储和分布式数据库的开发,计算和存储都略懂皮毛。今天我将为大家介绍我们的分布式数据库在抖音春晚活动中的实践应用。
分布式数据库架构简介
相信对数据库感兴趣的同学对上面这张图也不会陌生。这张图是 DB Engines 的数据库排名,准确来说是一个关系型数据库的排名。在 2021 年 4 月份的榜单上,MySQL 和 PG 都是关系型数据库的 Top5。这就意味着,如果我们想做一款数据库产品,大概率永远都绕不过 MySQL 和 PG 的生态。所以我们如果要做个数据库产品,不要想着完全自成一套,还是要把兼容 MySQL 和 PG 生态放在高优先级上。
这时候可能有同学会问,既然开源的 MySQL 和开源的 PG 发展得这么好,它们的生态非常完善,用户也非常多,排名也很靠前,我们为什么还需要去开发分布式数据库?这个问题的答案其实也比较显而易见,就是原有的架构不能很好地满足我们内部应用的需求,所以我们才会去寻找第二条路。
上图是现有的或者主流的大型数据库系统的架构,它分为三层:
最上一层是应用,今日头条,抖音,西瓜视频等都是应用。
中间层是数据库中间件层。
底层是数据库层以及数据库下面的单机存储。
这个架构应该是比较主流的大型后端的数据库架构,但这个架构有什么问题?
首先是这个架构里使用了数据库中间件。中间件本身存在一定的使用限制,对用户不是很友好。举个例子,它可能在使用的过程中需要用户感知一些 sharding key,如果用户不指定 sharding key,读写可能会被放大,使用起来性能较差,不是那么友好。
第二点是会遇到本地磁盘的容量限制。在传统的架构里,单机数据库是跑在单节点上的,单节点自然会受到本地磁盘容量的限制,大不了在一个节点上挂十几块盘,总容量也就会受这十几块盘的总容量限制。有同学可能会说,我们可以去做一个集群架构,通过主从做复制,又或者可以分库分表等等。那样的话,我们又绕回到第一个问题 ,要使用中间件支持,又会遇到一些限制。
第三点是传统单机数据库在部署和使用上可能会存在跨机房的问题,我们可能要在 RPO 和性能之间取得 tradeoff。
既然传统的大型数据库系统架构有这样一些问题,自然而然我们就会想着寻找另一条出路。那分布式数据库是不是我们要寻找的答案?目前看来,我们确实是在这条路上走得越来越远了。
分布式数据库架构简介
主流的分布式数据库的架构主要有以下两个类型:
Shared-Nothing 架构:最早使用 Shared-Nothing 架构的一些产品我们称之为 MPP 数据库。如果用户选择使用 MPP 架构的数据库,那他们可能更关心的是整个系统的吞吐量,对查询时延并不会特别敏感。MPP 数据库主要对接的是报表或者分析类的应用,可能经常会使用列式存储。但是,列存还是行存并不是绝对的,这只是对现有产品特点的总结。
Shared-Storage 架构:目前一些主流的基于 Shared-Storage 架构的产品都是用来处理实时的在线事务。使用这种架构的数据库产品,用户可能会更关心在线事务的处理时延,可能是毫秒级甚至是微秒级别的需求。这种产品主要对接在线的事务应用。在这种场景下,可能常常会使用行式存储而非列式存储,因为它没有分析和报表类的需求。
这里要再重申一下,这两种架构本身很难比较优劣,用户需要根据业务架构去选择数据库的架构。
我们顺着 Shared-Storage 这个方向继续往下深入看,下图是一个简要的 Shared-Storage 架构的分布式数据库架构图。
可以看到,我们的系统分为三个层次:
最上面是代理层;
中间是计算层;
最底层是分布式存储层。
可以看到三层之间各个节点是通过高速的网络互联,各层计算节点之间是没有直接的网络交互的。最底层的分布式存储层是一个共享存储池,可以使用多种不同的介质来进行最终的数据落地存储。
这样的一个数据库系统有以下一些特点:
灵活性强:因为是基于 Shared-Storage 架构实现的计算存储分离的数据库产品,当需要扩缩容的时候,计算层和存储层互相耦合度非常低,可以独立进行扩缩容,非常灵活。
兼容性好:DB Instance 可以 100% 兼容 MySQL 和 PostgreSQL 内核。
高可用:在存储层的分布式存储池里实现了数据多副本,并且可以跨多个机房部署,以提高系统的可用性。
高性能:可以部署为集群模式,在集群模式下集群性能肯定是远超单机的。
成本低:由于计算节点和存储节点可以独立扩缩容,互相不影响,不需要扩计算的时候同时扩存储的盘,然后扩盘的时候也不需要同时扩 CPU,因此成本比较低。同时我们在存储层做了很多高压缩比的技术方案,后面会详细介绍。
超大容量:支持 TB 甚至 PB 级别的超大容量数据表。
数据计算引擎解析
看了整体架构概览,接下来剖析一下计算引擎。数据库的计算引擎是用来处理计算逻辑和事务逻辑的,一些核心的模块包括:
接入层
Query Engine
Buffer Pool
日志子系统
事务子系统
锁子系统
可以这么说,缺了上述任意一个模块都很难构建出一个具有完备 ACID 特性的关系型数据库。
了解关键子模块后,我们来看看计算层的数据模型。对于用户或者后端应用开发者来说,数据库可能是用户、数据库和数据表的一个集合;但是对于数据库开发者来说,数据库本质是内存数据模型和磁盘数据模型的复杂组合。我们来看看有哪些数据模型。
内存(In-Memory)数据模型:首先肯定会有一个基于 page/block 组织的 LRU cache;还会有基于 page 组织的一个树状结构,用来组织数据、索引等;还有一个 global log buffer,或者可能也会实现成一个 thread local 的 log buffer 用于下刷日志。
磁盘结构方面,肯定会有 redo log、table space 以及临时表等文件。正是这些内存结构和磁盘结构共同组成了计算引擎的数据模型。
一条 SQL 的生命周期
知道了数据怎么组织后,我想大家比较好奇的一个问题是,作为用户,当写一条 SQL 到数据库系统后,数据库系统是如何处理这个 SQL 语句,把表里的结果查询出来并返回给用户的。这里简单介绍一下一条 SQL 的完整生命周期。
假设用户发了一条 SQL,要从两张表中 select 一些数据,然后加一些约束条件,例如在 where 里面加一些 filter 等等。那么当这条 SQL 进入了数据库系统,我们会:
先把 SQL 裸的字符串分切割成多个有效的 token。在这个例子里,可能是 SELECT 、T1 、 WHERE 等,这些都是有效的 token。
根据一定的语法规则把这些 token 组织成一棵抽象语法树,也就是 AST。组织成抽象语法树之后,要遍历这个树状结构。
根据这个树状结构和一些语法规则,可以把它组织成一个查询计划(目前我们还称之为逻辑计划)。然后我们会对逻辑计划进行优化,提高它的查询性能。最后我们会基于逻辑计划生成物理计划,物理计划描述的是我们怎么实际跟存储打交道,拉取哪些数据,需要做哪些具体的运算。
接下来执行引擎就出场了(目前比较主流的是 volcano 模型),执行引擎把已经生成好的物理计划执行一遍。执行过程中会与存储层交互获取数据,然后执行每个算子里面的计算逻辑,最终把计算后的结果批量返回给用户,用户就能得到查询的结果。
这就是一条 SQL 的完整生命周期。
计算引擎内核优化
通过前面的介绍相信大家对数据怎么组织,SQL 怎么执行都有了比较清晰的理解,下面会给大家介绍我们在内核级别做了哪些优化。
首先是对日志子系统做了非常深入的优化,甚至说是改造。我们废除了一些原生的刷盘机制,再结合新硬件做了高效的 append only 模型,并且丰富了 redo log 的类型和语义来支撑整个系统的运转。
实现了 Extent Data Cache。它是基于共享内存实现的。当数据库进程意外宕机时,重启后内存中肯定没有热数据,这个时候 Extent Data Cache 可以保存宕机前的热数据,避免冷启动的问题。
DDL 优化。结合分布式存储层提供的能力,我们做了比较深入的 DDL 优化,提高了 DDL 的效率和性能。
算子下推。结合分布式存储层的能力,我们把一些简单的算子下推到存储层,利用存储节点空余的 CPU 去做计算,借此提高复杂查询的性能。
实现了一个 lock-free 的 ReadView。可化解多事务并发时候的 global lock 瓶颈,提高了多事务并发能力。
融合了 redo log 和 Binlog。目的是消除原生的 binlog 和 redolog XA 机制带来的一些问题,同时融合成单流之后也能提高写入性能。
分布式存储系统解析
看完计算引擎,来看看第二个核心子系统——分布式存储层。经过对计算层的讲解,大家对数据类型肯定不陌生了。这里再提一下数据库最关键的两种数据,无非就是 redo log 和 Page。只要有这两者,数据库就能运转起来。所以我们整个存储层实际上就是围绕 redo log 和 page 的存储来构建的。在存储层我们有两个问题要解决:
第一个问题是关于 Page 的,在分布式存储系统里,如何把表存到存储层?
单机的传统数据库使用单机存储,可以看见单机的文件系统。单机数据库要存储数据很简单,通过单机文件系统上提供的接口写到本地磁盘上即可。在分布式架构下,存储是远端的一个分布式存储池。这时怎么把原来写到单机文件系统上的表存到分布式系统中远端的存储池呢?
其实答案很简单,就是构造一套分布式的映射规则。
从上图可以看到,在计算层会有很多数据表,每个数据表实际是一个由 Page 组成的 table space,我们要做的是把计算层的基本单元(Page),映射到存储层的基本单元(Segment)。这个映射规则可以基于哈希或者如图所示的 round-robin,或者任意自定义规则,只要能正确寻址和保证地址唯一性即可。
当把 Page 映射到 Segment 之后,就可以把 Segment 做成多个副本,复制到多个实际的存储池物理节点上。
这个模型有什么优势?
首先高可用高可靠,多副本可以跨机房存储。
其次是能提供比较好的计算性能,因为我们可以在多个副本上进行并行计算。举个简单的例子,假设我们要从头到尾把所有 Page 都 scan 一遍,最简单的做法可能是从零开始串行线性地 scan,但这样效率会很低。基于我们这个数据模型可以把这个 scan 同时下发到多个 Segment 上,因为本来 Page 就基于 Segment 打散了,所以我们可以同时在多个 Segment 上进行 scan。多个 Segment 又有可能被复制到不同的物理节点上,所以能充分地利用多个物理节点上的计算资源来进行计算操作,scan 就会很快。
第二个问题是围绕着 log 来发散的。我们现在知道了 Page 是怎么存储的,那么 Page 是怎么来的?其实很简单,我们构建这个分布式数据库,由始至终贯彻的一个非常重要的理念是:log is the database。我们最终落地的数据是 Page,这个 Page 就要从 log 中来。
计算引擎在处理事务的过程中,必然会产生很多 redo log,这些 redo log 会被提交到计算引擎的 redo log buffer,经过解析得到一个 Page-based 的 log buffer。前面介绍了映射规则,实际上在这里也要用同样的规则把 Page-based log buffer 打散成 Segment-based log buffer。然后把 Segment-based log buffer 写到远端存储池里,远端存储池自然会根据 Segment 组织起来一个多 Page 多版本的 log 链表,每个 Page 只要向前消费这些针对自身修改的 redo log,就能不断产生新的版本,然后去服务不同版本需求的 Page Read。这就是从 log 到 Page 的全流程。
说到这里,我相信大家会有另一个更加好奇的问题,就是要存下来 log、Page 等这么多数据,而且是多副本存储,我们怎么去控制成本?这也是很实在的一个问题。我们采用了以下几种关键技术:
数据在存储层是 tiering 存储,可以利用多种不同的存储介质,例如用 persistent memory 存最热的数据,用高性能的 SSD 存温的数据,HDD 存归档的冷数据。利用不同价格的不同介质来进行存储,可以从物理硬件的角度解决或缓和成本问题。
单机存储引擎跑在存储节点上,在单机存储引擎内部,我们实现了一个高效的压缩算法进行数据压缩,同时可保证不会损耗太多性能。这是从软件层面来缓和成本问题。
智能副本策略:我们的存储系统是多副本存储的,但在一些场景下可以不使用真正意义上完整的多副本,例如可以使用 EC、lazy replica 等策略来帮助缓和成本。
综合以上这些降低成本的技术,即使我们做了一定的数据冗余,我们还是能比较好的控制存储层的成本。
分布式数据库在抖音春晚活动中的实际应用
现在大家应该已经对我们的分布式数据库系统有了一定的了解,接下来就揭秘一下抖音春晚活动背后支撑大规模流量洪峰的黑科技。
分布式数据库在抖音春晚活动中支撑的业务
整个抖音春晚红包活动的流量非常大,分布式数据库在其中支撑了大量的在线和离线业务,包括:设备推送、小说、钱包等,其中最有代表性的是设备推送的业务。
该业务的量级非常大,用户量达到十亿级,峰值的读 QPS 可达 600W+,峰值写 QPS 可达 360W+,数据存量是 20+ TB。在如此高流量和大存量的情况下,我们和业务做了以下联调的工作:
Query Pattern 分析:一般情况下,一个成熟业务不会出现没建索引或者索引质量不好等低级错误,所以我们首先做的就是直接去分析 Query Pattern ,看看从哪里可以改善事务的性能。在分析过程中发现业务特点主要以 update 为主,事务的大小可能不太合适。所以我们做了一些事务拆分,降低 IO size,去除 global lock 以及 update 性能的优化,帮助事务跑的更快。
计算/存储性能摸底:先对虚拟机和物理机设置一个最小的规格,基于这个最小规格得到一个 benchmark 结果,在此基础上慢慢增加规格,看计算和存储性能是否能够相对线性的扩展。经过五六轮的摸底测试之后,最终能在满足业务 SLA 的情况下达到 QPS 要求。
性能压测:业务方要求 600W+ 的读 QPS 和 360W+ 的写 QPS,我们的性能压测要做到比这个值更高一点,才能保证活动当天不出事故。所以我们在摸底测试的基础上,又进行了多轮性能压测。在性能压测中发现了一些缓存策略不对或者效果不好的问题,就针对性地做了存储层的 Page IO 的缓存优化来提高读写性能。
故障演练:性能压测完之后,最关键的就是故障演练和容灾方案。在春晚当天,相比业务不可用,宁愿性能差。所以我们在故障演练方面做了非常多的预案和 test case,包括:针对网络故障的 test case:例如用 iptables 模拟网络全丢包的场景,甚至还会直接随机 kill 一些存储节点来看它们宕机之后会不会引发连锁反应。磁盘故障演练:注入一些单机磁盘故障,去 kill 一些存储节点、元数据节点和单机磁盘,通过这样的演练来模拟存储磁盘故障。
网络故障和存储故障演练之后,我们根据结果做了一些应急的容灾方案和工具,很幸运在春晚当天一个预案也没有用上,很顺利地支撑了整个业务在春晚当晚的活动。
总结展望
以上就是火山引擎分布式数据库及其在抖音春晚活动中应用的介绍。最后想和大家探讨一下我们团队对分布式数据库未来演进方向的一些思考。
利用新硬件加速。新硬件包括不仅限于 RDMA 网络,Persistant Memory,还有可计算存储硬件等等。
多模计算引擎:除了关系型数据库之外,我们可能还有其他比较流行的一些数据模型或者数据库,例如文档数据库、图数据库等等。可以畅想一下在未来实现一个支持 all in one 的计算引擎,通过一条 SQL 或者其他查询语句直接执行跨多个计算引擎的事务。
持续对关系型数据库内核做深入的优化,甚至是改造或者推倒重来:首先是现在的 B+ 树,都是单点的在一个单机存储引擎内部的 B+ 树,未来可能会尝试做分布式的 B+ 树以及相对应的一些分布式算法。除了悲观事务,还可以探索一下乐观事务,验证能否将乐观事务和悲观事务进行混合使用。现在在锁上面做的文章虽然比较少,但是它可做的东西其实很多,例如可以尝试更多样的锁调度算法,还可以引入谓词锁来丰富锁系统。最后一点,AI 技术已经成为热门方向,但 AI 技术和数据库的结合还是相对冷门和小众的。我们可以畅想一下,在未来是否做到 AI for DB 或者 DB for AI,用 AI 进行自动调参和自动索引质量诊断,甚至可以把 AI 带到存储层,在存储层实现在线格式诊断,进行行列存格式转换。
Q&A
Q:在哪个环节判断数据存储为热数据还是冷数据?
A:前面介绍过我们的架构是计算和存储分离的,计算层和存储层互相不耦合,互相也不感知,所以必然是在存储层去判断数据的热、温和冷的程度。那么这个判断的规则是怎样的?首先我们可以非常简单的根据访问频率进行判断;其次也可以基于 IO 压力做判断,有时候有可能它经常被访问,但是它每次被访问的 IO 压力并不是那么大,也可能被归为温/冷数据;
Q:既要支持 MySQL 又要支持 PG,是怎么做到的?
A:现在我们只兼容 MySQL 和 PG,但只要是基于 log 和 Page 这种机制的数据库,我们理论上都是可以支持的。背后的原理其实是我们在存储层做了一层统一泛化的抽象,基于 log is the database 的思想,做了很多从 log 到 Page 转化的通用接口。基于这些接口,只要有 redo log、Page data 的一些计算引擎的数据库,都可以对接到这一套统一泛化的接口上,接进来存储层自然就能对接上整个系统,进而兼容更多的数据库产品和计算引擎,成为一个 database family。
评论