原文|The growing pains of database architecture
作者|Tim Liang, Software Engineer at Figma
2020 年,因为 Figma 不断加入新功能,筹备第二条产品线和用户不断增长导致数据库流量每年以 3x 速度增长,我们的基础设施遇到了增长瓶颈。很清楚的是,原本的基础设施无法扩展以满足新需求,我们用了单个大型 Amazon RDS 数据库来存储元数据,比如权限、文件信息和评论等,虽然可以丝滑处理大多核心协作功能,但只有一个数据库的话限制很大。尤其在高峰期,流量达到 65% 以上时,单个数据库查询量过大导致 CPU 利用率上升。随着使用接近极限,数据库延迟变得越来越不可预测,严重影响用户体验。
如果数据库完全饱和,Figma 就会停止工作。
我们离宕机还挺遥远,但作为基础设施团队,我们得主动识别并解决可扩展性问题。需要一种解决方案以减少潜在的不稳定因素,并为未来的规模铺平道路。在实施该解决方案时,性能和可靠性是首要考虑因素;我们的团队旨在构建一个可持续发展的平台,使工程师能够快速迭代 Figma 而不影响用户体验。如果说 Figma 的基础设施是道路,那我们不能关闭高速公路来进行工作。
我们从一些修复开始,先延长一下道路的生命,并为一个更完整解决方案打下基础:
- 将数据库升级到最大实例(从 r5.12xlarge 到 r5.24xlarge),以最大化 CPU 利用率。
- 创建多个读取副本以扩展读取流量。
- 为新用例建立新数据库以限制原始数据库增长。
- 添加 PgBouncer 作为连接池器来限制新增连接(已经达到上千)所产生影响。
虽然以上措施改善了些许,但也有局限性。分析了数据库流量后,我们发现写入操作,比如收集、更新或删除数据对消耗了大量数据库利用率。此外,并非所有数据读取都可以移动到副本中,因为应用程序对复制延迟滞后的敏感度不同。因此,从读和写两个方面来看,我们仍需要给原始数据库减压。是时候摆脱渐进式变化并寻找长期解决方案了。
探索之路
首先,我们探索了水平扩展数据库的可能。Figma 使用的数据库管理系统是 Postgres,很多流行的托管解决方案并不兼容。如果我们决定使用可水平扩展的数据库,那要么找到一个兼容 Postgres 的托管解决方案,要么自托管。
迁移到 NoSQL 数据库或 Vitess (MySQL) 要复杂的双重读写迁移,特别是对于 NoSQL 来说还要进行工程浩大的应用程序端更改。如果用支持 Postgres 的 NewSQL 数据库,我们将会是云上分布式 Postgres 中的最大单集群,我们不想冒险成为第一个遇到缩放问题的客户。对于托管方案,我们能控制的比较少,因此在没有经过针对我们规模级别的压力测试就依赖它们会带来更多风险。如果不用托管方案,那就得自托管。但由于迄今为止我们一直依赖托管方案,在团队能够支持自托管所需大量培训和投入,这意味着成本,也会分散我们主要关注的可扩展性 - 这才是个生死攸关的问题。
在决定不采取水平分区的两种前进路径之后,我们决定垂直分区。这同时具有短期和长期效益:垂直分区现在可以缓解原始数据库的压力,并为以后水平划分子集提供了一条路。
我们的分区方法
在开始前,我们首先需要确定要将哪些表分区到自己的数据库中。有两个重要因素:
- 影响:移动表应该能够解决大部分工作负载
- 隔离性:这些表不应与其他表紧密相连
为了衡量影响,我们参考了查询的平均活跃会话(AAS),它描述了在某一时刻给定查询的活跃线程数量的平均值。我们通过以 10 毫秒间隔查询 pg_stat_activity 来计算 AAS,以识别与查询相关联的 CPU 等待,并按表名聚合信息。
每个表「隔离」的程度对于是否容易进行分区至关重要。当我们将表移到另一个数据库同时,我们也失去了重要功能,例如原子事务、FK 验证和连接表。因此,移动表可能会需要开发人员重新编写 Figma 中很多代码,成本较高。我们最好通过识别易于分区的查询模式和表来制定策略。
但是,从后端角度看这很困难。Ruby 作为我们应用程序后端,服务了大部分 Web 请求,它们生成了大部分数据库查询语句,开发人员使用 Active Record 编写这些查询语句。由于 Ruby 和 Active Record 的动态性,仅通过静态代码分析很难确定哪些物理表受到 Active Record 查询的影响。首先,我们创建了运行时验证器,这些验证器连接到 Active Record。这些验证器将生产查询和事务信息(例如调用者位置和涉及的表)发送到 Snowflake(我们的云上数仓)中进行处理。我们使用此信息查找经常引用相同组表格的查询和事务。哪儿工作负载成本高,那这些表就作为垂直分区的主要候选项。
管理迁移
一旦确定了要分区的表,就要制定一个计划来在迁移。虽然离线操作很简单,但对于我们来说不是一个选项 - Figma 需要始终保持在线和高效以支持用户的实时协作。我们要协调跨数千个应用程序后端实例的数据移动,以便它们可以在正确的时刻将查询路由到新数据库。这样就可以在没有维护窗口或停机时间(这会对用户造成干扰,并且还需要工程师进行非工作时间内的工作)情况下分区数据库。我们的解决方案要满足以下目标:
- 将潜在可用性影响限制在 1 分钟内
- 自动化该过程,使其易于重复执行
- 近期的分区操作能撤回
没能找到符合我们要求的预构建解决方案,而且我们也希望灵活地适应未来情况。只有一个选择:自己构建。
定制的解决方案
高层次上,我们进行了以下操作(第 3-6 步在几秒钟内完成,以最小化停机时间):
- 准备客户端应用程序以从多个数据库分区查询
- 将表从原始数据库复制到新数据库,直到复制延迟接近 0
- 暂停原始数据库的活动
- 等待数据库同步
- 将查询流量重新定向到新的数据库
- 恢复活动
正确准备客户端应用程序是一个重要问题,Figma 应用程序后端的复杂性让我们很焦虑。如果在分区之后错过了某些边缘情况会怎么样呢?为降低风险,我们靠 PgBouncer 层来获取运行时可见性,并确保应用程序正确配置。与产品团队合作,以确保应用程序兼容分区后的数据库后,我们创建了单独的 PgBouncer 服务虚拟分流流量。安全组确保只有 PgBouncer 可以直接访问数据库,这意味着客户端应用程序始终通过 PgBouncer 连接。首先分区 PgBouncer 层将给客户留出余地。尽管我们能检测到路由不匹配,但由于两个 PgBouncer 具有相同的目标数据库,客户端仍将成功查询数据。
一旦验证了应用程序已准备好为每个 PgBouncer 建立单独的连接(并正确发送流量),我们继续进行下一步。
合乎逻辑的选择
在 Postgres 中,有两种复制数据的方式:流复制或逻辑复制。我们选择了逻辑复制,因为它允许我们:
- 传输一部分表格,这样开始时,我们可以在目标数据库使用更小的存储占用(减少存储硬件占用可以增加可靠性)。
- 复制到运行着不同 Postgres 版本的数据库中,这意味着我们可以进行最小停机时间主版本升级。AWS 针对主版本升级支持蓝绿部署,但该功能尚未在 RDS Postgres 上提供。
- 设置反向复制,使我们能够回滚操作。
使用逻辑复制的主要问题是我们有几个 TB 的生产数据,因此初始数据副本可能要数天甚至数周才能完成。我们希望避免这种情况,不仅为了将窗口期限制在最小范围内,也为了减少重新启动的成本。我们考虑过在正确的时间进行快照恢复并开始复制,但一个快照恢复就淘汰了能有较小存储占用空间。于是,我们开始调查为什么逻辑副本性能如此低下。原来拷贝降速是由于 Postgres 在目标数据库中维护索引的方式导致的。逻辑复制批量复制行,但它更新索引的效率低下:一次更新一行。删除目标数据库中的索引并在初始数据副本之后重建索引后,制作副本时间缩短到了几小时。
通过逻辑复制,我们能够从新分区数据库构建反向复制流,并返回原始状态。在原始数据库停止接收流量后,立即启动此复制流。对新数据库进行修改将被回传到旧数据库,在回滚事件中旧数据库会有这些更新。
关键步骤
解决了复制问题后,我们到了协调查询重定向的关键步骤。每天每时都有数千个客户端服务查询数据库。跨越这么多客户端节点进行协调很容易出问题。通过分两个阶段(先对 PgBouncers 分区,然后再是数据)执行切片操作,关键的数据分区操作只需要在为分区表提供服务的少量 PgBouncer 节点之间进行协调。
以下是正在进行中的操作概述:我们短暂地跨节点暂停所有相关数据库流量,方便同步新数据库以实现逻辑复制(PgBouncer 可以方便地支持暂停新连接和重定向)。当 PgBouncer 暂停新连接时,在原始数据库上撤销客户端对已分区表的查询权限。经过短暂的宽限期后,我们会取消任何剩余未完成的查询。由于我们的应用程序大多只会发出短时查询,因此通常会取消不到 10 个查询请求。此时,在流量暂停状态下,我们需要验证两个数据库是否相同。
在重定向客户端之前确保两个数据库相同是防止数据丢失的基本要求。我们使用日志序列号(LSN)来确定两个数据库是否同步。一旦确认没有新写入,就从原始数据库中取样一个 LSN,然后等待副本回放到此 LSN。此时,原始数据库和副本中的数据是相同的。
在确认副本已经同步之后,我们停止复制并将副本提升为新数据库,并如前所述设置反向复制。然后恢复 PgBouncer 中的流量,不过现在查询被转到了新数据库。
为未来规划
我们已经在生产环境中成功执行了多次分区操作,每一次都达到了最初的目标:在不影响可靠性的情况下解决可扩展性问题。我们第一次操作涉及移动两个高流量表,而 2022 年 10 月的最后一波操作涉及 50 个表。在每个操作期间,我们观察到大约 30 秒的部分可用性影响(请求丢失率约为 2%)。现在,每个数据库分区都具还有大量空间。我们最大的分区 CPU 利用率约为 10%,并且我们已经减少了一些低流量区所分配的资源。
现在还有很多工作要做。数据库很多时,客户端应用程序必须维护对每一个数据库的了解,并且随着添加更多数据库和客户端路由复杂度呈乘法级别增长。因此,我们引入了新的查询路由服务来集中和简化路由逻辑以便于扩展到更多分区。一些表有高写入流量或数十亿行和数千兆字节磁盘占用空间,这些表将分别遇到磁盘利用率,CPU 和 I/O 瓶颈问题。我们清楚,如果仅依赖垂直分区,最终还是会碰到扩展限制。回到最大化杠杆的目标上来说,我们为垂直分区打造的工具将让我们更好地处理高写入流量表的水平切片。它为我们提供了足够的「跑道」来维护当前项目并保持 Figma「高速公路」的畅通,同时也可以看到未来发展方向。
💡 你可以访问官网,免费注册云账号,立即体验 Bytebase。