原文链接:100X Scaling: How Figma Scaled its Databases

Figma 是一个协作设计平台,在过去几年中经历了疯狂的增长。自 2018 年以来,其用户基数增长了近 200%,每月吸引约 300 万用户。

随着越来越多的用户加入,基础架构团队发现自己处境艰难。他们需要一种快速扩展数据库的方法,以跟上日益增长的需求。

数据库栈就像 Figma 的骨干。它存储和管理所有重要的元数据,如权限、文件信息和评论。自 2020 年以来,它最终增长了惊人的 100 倍!

这是一个好问题,但也意味着团队必须发挥创造力。

在本文中,我们将深入探讨 Figma 的数据库扩展之旅。我们将探索他们面临的挑战、做出的决策以及想出的创新解决方案。最后,你将更好地了解为像 Figma 这样快速增长的公司扩展数据库需要什么。

Figma 数据库栈的初始状态

2020 年,Figma 仍然使用单个大型 Amazon RDS 数据库来持久化大多数元数据。虽然它很好地处理了所有事情,但一台机器的能力是有限的。

在流量高峰期,CPU 利用率超过 65%,导致数据库延迟不可预测。

Figma 数据库架构演变

虽然完全饱和还遥不可及,但 Figma 的基础架构团队希望主动识别和解决任何可扩展性问题。他们从一些战术修复开始,例如:

  • 将数据库升级到可用的最大实例(从 r5.12xlarge 升级到 r5.24xlarge)。
  • 创建多个只读副本以扩展读取流量。
  • 为新的用例建立新的数据库,以限制原始数据库的增长。
  • 添加 PgBouncer 作为连接池,以减少越来越多连接的影响。

这些修复为他们额外争取了一年的运行时间,但仍存在一些限制:

  • 根据数据库流量,他们了解到写入操作占整体利用率的主要部分。
  • 并非所有读取操作都可以移动到副本,因为某些用例对复制延迟的影响很敏感。

显然,他们需要一个更长期的解决方案。

第一步:垂直分区

当 Figma 的基础架构团队意识到他们需要扩展数据库时,他们不能直接关闭所有东西从头开始。他们需要一个解决方案,在解决问题的同时保持 Figma 平稳运行。

这就是垂直分区发挥作用的地方。

可以把垂直分区想象成整理衣柜。不是有一大堆乱七八糟的东西,而是把它们分成单独的部分。在数据库术语中,这意味着将某些表移动到单独的数据库中。

对于 Figma 来说,垂直分区是救星。它允许他们将高流量的相关表(如”Figma 文件”和”组织”的表)移动到各自独立的数据库中。这提供了一些急需的喘息空间。

为了识别要分区的表,Figma 考虑了两个因素:

  • 影响:移动表应该转移大量的工作负载。
  • 隔离:表不应与其他表强连接。

为了衡量影响,他们查看了查询的平均活动会话数(AAS)。这个统计数字描述了在特定时间点专用于给定查询的平均活动线程数。

衡量隔离性稍微棘手一些。他们使用运行时验证器钩入 ActiveRecord(他们的 Ruby ORM)。验证器将生产和事务信息发送到 Snowflake 进行分析,帮助他们根据查询模式和表关系识别适合分区的表。

一旦确定了表,Figma 就需要在不中断服务的情况下将它们迁移到数据库之间。他们为迁移解决方案设定了以下目标:

  • 将潜在的可用性影响限制在 1 分钟以内。
  • 自动化过程,使其易于重复。
  • 能够撤销最近的分区。

由于他们找不到能够满足这些需求的现成解决方案,Figma 构建了一个内部解决方案。总体而言,它的工作方式如下:

  • 准备客户端应用程序从多个数据库分区查询。
  • 将表从原始数据库复制到新数据库,直到复制延迟接近 0。
  • 暂停原始数据库上的活动。
  • 等待数据库同步。
  • 重新路由查询流量到新数据库。

垂直分区迁移过程

为了使迁移到分区数据库更顺利,他们创建了单独的 PgBouncer 服务来虚拟地分割流量。实施了安全组,确保只有 PgBouncer 可以直接访问数据库。

首先分区 PgBouncer 层为客户提供了缓冲,可以在最初所有 PgBouncer 实例具有相同目标数据库的情况下路由查询。在此期间,团队还可以检测路由错误并进行必要的纠正。

下图显示了此迁移过程。

实现复制

通过垂直分区进行扩展

数据复制是扩展数据库读取操作的好方法。当涉及到垂直分区的数据复制时,Figma 在 Postgres 中有两个选项:流复制或逻辑复制。

他们选择逻辑复制主要有 3 个原因:

  • 逻辑复制允许他们复制表的子集,这样他们可以在目标数据库中以小得多的存储 footprint 开始。
  • 它使他们能够复制数据到运行不同 Postgres 主要版本的数据库中。
  • 最后,它允许他们设置反向复制以在需要时回滚操作。

然而,逻辑复制很慢。初始数据复制可能需要数天甚至数周才能完成。

Figma 非常希望避免这个漫长的过程,不仅是为了最大限度地减少复制失败的窗口,也是为了降低出现问题时重新启动的成本。

但是是什么让这个过程如此缓慢呢?

罪魁祸首是 Postgres 在目标数据库中维护索引的方式。虽然复制过程批量复制行,但它也一次更新一行索引。通过删除目标数据库中的索引并在数据复制后重建它们,Figma 将复制时间减少到几小时。

水平扩展的需求

随着 Figma 的用户基数和功能集的增长,对数据库的需求也在增长。

尽管他们尽了最大努力,垂直分区仍有局限性,特别是对于 Figma 最大的表。有些表包含数 TB 的数据和数十亿行,对于单个数据库来说太大了。

有几个问题尤其突出:

  • Postgres Vacuum 问题:Vacuuming 是 Postgres 中一个重要的后台进程,它回收被删除或过时行占用的存储。如果不定期 vacuuming,数据库最终会用完事务 ID 并停止运行。然而,vacuuming 大表可能会消耗大量资源并导致性能问题和停机。

  • 最大每秒 IO 操作数:Figma 的最高写入表增长如此之快,很快就会超过 Amazon RDS 的最大 IOPS 限制。

为了更好地理解,想象一个图书馆藏书快速增长。最初,图书馆可以通过添加更多架子(垂直分区)来应对。但最终,建筑物本身将耗尽空间。无论你如何高效地安排架子,你都无法在一个建筑物中容纳无限数量的书籍。这就是当你需要开始考虑开设分馆的时候。

这就是水平分片的方法。

对于 Figma 来说,水平分片是一种将大表拆分到多个物理数据库中的方法,使他们能够扩展到单个机器的限制之外。

下图显示了这种方法:

水平分片方法

然而,水平分片是一个复杂的过程,有其自身的一系列挑战:

  • 某些 SQL 查询变得效率低下。
  • 必须更新应用程序代码以有效地将查询路由到正确的分片。
  • 必须跨所有分片协调模式更改。
  • Postgres 不再能强制实施外键和全局唯一索引。
  • 事务跨越多个分片,这意味着 Postgres 不能用于强制实施事务性。

探索替代方案

Figma 的工程团队评估了其他 SQL 选项,如 CockroachDB、TiDB、Spanner 和 Vitess,以及 NoSQL 数据库。

然而,最终他们决定在现有的垂直分区 RDS Postgres 基础设施之上构建水平分片解决方案。

做出这个决定有多个原因:

  • 他们可以利用现有的 RDS Postgres 专业知识,他们已经可靠地运行了多年。
  • 他们可以使解决方案适应 Figma 的特定需求,而不是让他们的应用程序适应通用的分片解决方案。
  • 如果出现任何问题,他们可以轻松地回滚到未分片的 Postgres 数据库。
  • 他们不需要将基于 Postgres 架构构建的复杂关系数据模型更改为 NoSQL 等新方法。这允许团队继续构建新功能。

Figma 独特的分片实现

Figma 的水平分片方法是针对其特定需求以及现有架构量身定制的。他们做出了一些不寻常的设计选择,使其实现与其他常见解决方案区分开来。

让我们看看 Figma 分片方法的关键组件:

Colos(协作定位)用于分组相关表

Figma 引入了”colo”或协作定位的概念,这是一组相关的表,它们共享相同的分片键和物理分片布局。

为了创建 colo,他们选择了少数分片键,如 UserId、FileId 或 OrgID。Figma 的几乎每个表都可以使用这些键之一进行分片。

这为开发人员提供了一个友好的抽象来与水平分片表交互。

colo 内的表支持跨表连接和完整事务,当限制为单个分片键时。大多数应用程序代码已经以类似的方式与数据库交互,这最小化了应用程序使表准备好进行水平分片所需的工作。

下图显示了 colo 的概念:

Colos 概念

逻辑分片与物理分片

Figma 将应用层的”逻辑分片”概念与 Postgres 层的”物理分片”概念分开。

逻辑分片涉及为每个表创建多个视图,每个视图对应于给定分片中数据的子集。对表的所有读取和写入都通过这些视图发送,使表看起来是水平分片的,即使数据物理上位于单个数据库主机上。

这种分离使 Figma 能够解耦迁移的两个部分,并独立实施它们。他们可以在执行风险更大的分布式物理分片之前,进行更安全、风险更低的逻辑分片 rollout。

回滚逻辑分片只是一个简单的配置更改,而回滚物理分片操作将需要更复杂的协调以确保数据一致性。

DBProxy 查询引擎用于路由和查询执行

为了支持水平分片,Figma 工程团队构建了一个名为 DBProxy 的新服务,它位于应用程序和连接池层(如 PGBouncer)之间。

DBProxy 包括一个轻量级查询引擎,能够解析和执行水平分片查询。它由三个主要组件组成:

  • 查询解析器:读取应用程序发送的 SQL 并将其转换为抽象语法树(AST)。
  • 逻辑计划器:解析 AST,提取查询类型(插入、更新等)和查询计划中的逻辑分片 ID。
  • 物理计划器:将查询从逻辑分片 ID 映射到物理数据库,并重写查询以在适当的物理分片上执行。

下图显示了这三个组件在查询处理工作流中的实际用途。

DBProxy 架构

在水平分片世界中,查询总是有权衡的。单个分片键的查询相对容易实现。查询引擎只需要提取分片键并将查询路由到适当的物理数据库。

但是,如果查询不包含分片键,查询引擎必须执行更复杂的”分散 - 收集”操作。这个操作类似于捉迷藏游戏,你将查询发送到每个分片(分散),然后从每个分片拼凑答案(收集)。

下图显示了单分片查询与分散 - 收集查询的工作原理。

单分片与分散 - 收集查询

正如你所见,这增加了数据库的负载,拥有太多的分散 - 收集查询会损害水平可扩展性。

为了更好地管理事情,DBProxy 处理负载调节、事务支持、数据库拓扑管理和改进的可观察性。

影子应用程序准备框架

Figma 添加了一个”影子应用程序准备”框架,能够预测实时生产流量在不同潜在分片键下的行为。

这个框架帮助他们保持 DBProxy 简单,同时减少了应用程序开发人员重写不支持的查询所需的工作。

所有查询和相关计划都记录到 Snowflake 数据库中,他们可以在那里运行离线分析。根据收集的数据,他们能够选择支持最常见 90% 查询的查询语言,同时避免查询引擎中最坏情况的复杂性。

结论

Figma 的基础架构团队在 2023 年 9 月发布了他们的第一个水平分片表,标志着他们数据库扩展之旅中的一个重要里程碑。

这是一个成功的实施,对可用性的影响最小。此外,团队在分片操作后没有观察到延迟或可用性的回归。

Figma 的最终目标是水平分片他们数据库中的每个表,实现近乎无限的扩展。他们已经确定了几个需要解决的挑战,例如:

  • 支持水平分片模式更新
  • 为水平分片主键生成全局唯一 ID
  • 为业务关键用例实施原子跨分片事务
  • 实施分布式全局唯一索引
  • 开发 ORM 模型以提高开发人员速度
  • 自动重新分片操作,以点击按钮实现分片拆分

最后,在获得足够的运行时间后,他们还计划重新评估他们当前使用内部 RDS 水平分片的方法, versus 将来切换到开源或托管替代方案。


本文为学习目的的个人翻译,译文仅供参考。

原文链接:100X Scaling: How Figma Scaled its Databases

版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。