Code前端首页关于Code前端联系我们

Figma 扩展 Postgres 数据库架构的解决方案

terry 2年前 (2023-09-26) 阅读数 73 #后端开发

2020 年,由于新功能的结合、准备推出第二个产品以及更多用户(数据库流量每年增长约 3 倍),Figma 的基础设施面临一些增长挑战问题。我们知道早年支持 Figma 的基础设施无法扩展以满足我们的需求。我们仍然使用单个大型 Amazon RDS 数据库来保存大部分元数据(例如权限、文件信息和评论),虽然它可以无缝处理我们的许多核心协作功能,但单台机器有其局限性。

最值得注意的是,由于一个数据库提供的查询量很大,我们观察到在高流量期间 CPU 利用率高达 65%。

随着使用量接近极限,数据库延迟变得越来越难以预测,从而影响核心用户体验。

如果我们的数据库完全饱和,Figma 将停止工作。

远非 因此,作为基础设施团队,我们的目标是在可扩展性问题成为迫在眉睫的威胁之前主动识别并解决它们。

我们需要设计一种解决方案,减少潜在的不稳定因素,并为未来的规模化铺平道路。此外,随着我​​们实施该解决方案,性能和可靠性将继续保持一流;我们团队的目标是建立一个可持续的平台,让工程师能够在不影响用户体验的情况下快速迭代 Figma 的产品。如果 Figma 的基础设施是一系列道路,我们就无法在施工期间暂时关闭高速公路。

我们首先进行了一些战术修复,以确保多一年的运行时间,同时为更全面的方法奠定基础:

  1. 将我们的数据库升级到最大的可用实例(从 r5.12xlarge 到 r5.24xlarge)以最大限度地利用 CPU使用 Runway
  2. 创建多个只读副本以扩展读取流量
  3. 为新用例构建新数据库,以限制原始数据库的增长
  4. 添加 PgBouncer 作为连接池,以限制连接数量增长的影响(数千)
Figma实现Postgres数据库架构扩展的方案

我们添加了 PgBouncer 作为连接管理器

虽然这些修复是一种提升,但它们也有局限性。通过分析数据库流量,我们了解到写入(例如收集、更新或删除数据)占数据库使用量的很大一部分。此外,由于应用程序对复制延迟很敏感,因此并非所有读取或数据提取都可以移动到副本。所以从读写的角度来看,我们仍然需要从原始数据库加载更多的工作。是时候放弃渐进式变革并寻找长期解决方案了。

选择垂直扩展

我们首先研究了水平扩展数据库的可能性。许多流行的托管解决方案与我们在 Figma 使用的数据库管理系统 Postgres 不兼容。如果我们决定使用水平可扩展的数据库,我们要么必须找到与 Postgres 兼容的托管解决方案,要么自己托管。

迁移到NoSQL数据库或Vitess(MySQL)将需要复杂的双读写迁移,尤其是NoSQL,还需要应用程序端进行重大更改。对于与 Postgres 兼容的 NewSQL,我们将拥有云管理的分布式 Postgres 最大的单集群足迹之一。我们不想成为第一个遇到任何扩展问题的客户。我们对我们的托管解决方案几乎没有控制权,因此在没有进行我们规模的压力测试的情况下依赖它们会让我们面临更多风险。

如果无法选择托管解决方案,我们的另一个选择是自托管。然而,由于迄今为止我们一直依赖托管解决方案,因此我们的团队需要进行大量的前期工作才能获得支持自托管所需的培训、知识和技能。这将意味着巨大的运营成本,这将使我们的注意力从可扩展性上转移——这是一个更关乎生存的问题。

决定水平切割后,我们必须转向。

我们决定按表垂直分区数据库,而不是水平拆分。

我们没有将每个表拆分为多个数据库,而是将 表组 移动到它们自己的数据库中。

这被证明具有短期和长期的好处:垂直分区现在减轻了原始数据库的负载,同时为将来的表子集的水平切片提供了一种前进的方向。

垂直分区方法

但是,在开始此过程之前,我们必须首先确定要分区到其自己的数据库中的表。有两个重要因素:

  1. 影响:移动表应该移动很大一部分工作负载
  2. 隔离:表不应与其他表紧密连接

为了衡量影响,我们查看了平均值查询的活动会话 (AAS),它描述了给定时间专用于给定查询的活动线程的平均数量。我们通过以 pg_stat_activity10 毫秒间隔进行查询来计算此信息,以确定与查询相关的 CPU 等待,然后按表名称聚合信息。

每个表的“隔离”程度证明了其简单分区的核心。当我们将表移动到不同的数据库时,我们会丢失重要的功能,例如表之间的原子事务、外键验证和联接。因此,相对于开发人员需要重写 Figma 应用程序的量而言,移动表的成本可能会很高。我们需要通过专注于识别易于分区的查询模式和表来制定策略。

这对于我们的后端技术堆栈来说是很困难的。

我们使用 Ruby 作为我们的应用程序后端,它服务于我们的大部分 Web 请求。这些反过来又生成了我们的大部分数据库查询。我们的开发人员使用 ActiveRecord 来编写这些查询。由于Ruby和ActiveRecord的动态特性,仅通过静态代码分析很难确定哪些物理表受到ActiveRecord查询的影响。

作为第一步,我们创建了一个连接到 ActiveRecord 的运行时验证器。这些验证器将生产查询和交易信息(例如调用者位置和涉及的表)发送到我们的云数据仓库 Snowflake。我们使用此信息来查找始终引用同一组表的查询和事务。如果这些工作负载的成本很高,这些表将被确定为垂直分区的主要候选者。

管理迁移

一旦确定了要分区的表,我们就需要制定一个在数据库之间迁移它们的计划。虽然这很容易离线完成,但离线对于 Figma 来说不是一个选择 - Figma 必须始终保持支持实时用户协作的能力。我们需要协调数千个应用程序后端实例之间的数据移动,以便它们可以在正确的时刻将查询路由到新数据库。这将使我们能够对数据库进行分区,而无需为每个操作使用维护窗口或停机时间,这会对我们的用户造成干扰(并且还需要工程师在办公时间工作!)。我们需要一种能够满足以下目标的解决方案:

  1. 将潜在的可用性影响限制在 1 分钟以内
  2. 自动化该过程,以便轻松重复
  3. 能够撤消最近的分区

我们无法找到一个解决方案一种预构建的解决方案可以满足我们的要求,但我们还希望能够灵活地调整该解决方案以供将来使用。只有一种选择:建立我们自己的。

我们的定制解决方案

在较高层面上,我们实施了以下步骤(步骤 3-6 在几秒钟内完成,以尽量减少停机时间):

  1. 准备客户端应用程序 从多个数据库分区进行查询
  2. 从以下位置复制表原始数据库到新数据库,直到复制延迟接近 0
  3. 暂停原始数据库上的活动
  4. 等待数据库同步
  5. 将查询流量重定向到新数据库
  6. 继续活动为问题正确准备重要的客户端应用程序应用程序后端的复杂性让我们感到焦虑。如果分区后我们错过了崩溃的边缘怎么办?为了降低操作风险,我们使用 PgBouncer 层来获得运行时可见性并确保我们的应用程序配置正确。

    与产品团队合作使应用程序与分区数据库兼容后,我们创建了一个单独的 PgBouncer 服务来实际共享流量。安全组确保只有 PgBouncer 可以直接访问数据库,这意味着客户端应用程序始终通过 PgBouncer 进行连接。首先对 PgBouncer 层进行分区,为客户端提供错误路由查询的空间。我们能够检测到路由不匹配,但由于两个 PgBouncer 具有相同的目标数据库,因此客户端仍然可以查询数据。 Figma实现Postgres数据库架构扩展的方案

    我们的初始状态 Figma实现Postgres数据库架构扩展的方案

    PgBouncer 分区后的数据库状态

    一旦我们验证应用程序正在为每个 PgBouncer 准备单独的连接(并正确发送流量),我们就继续。 Figma实现Postgres数据库架构扩展的方案

    数据分区后数据库的状态

    “逻辑”选择

    在Postgres中有两种复制数据的方式:流式复制或逻辑复制。我们选择逻辑复制是因为它允许我们:

    1. 迁移表的子集,这样我们就可以从目标数据库中更少的存储开始(减少硬件存储可以提高可靠性)。
    2. 复制到运行不同主要版本的Postgres的数据库,这意味着我们可以使用此工具以最短的停机时间执行主要版本升级。 AWS 具有用于主要版本升级的蓝/绿部署,但此功能尚不适用于 RDS Postgres。
    3. 设置反向复制,这允许我们回滚操作。

    使用逻辑复制的主要问题是我们要处理数TB的生产数据,因此初始数据复制可能需要几天甚至几周才能完成。我们希望避免这种情况,不仅要最小化复制失败窗口,还要减少重新启动的成本。我们仔细考虑了协调快照恢复并在正确的时间开始复制,但恢复消除了存储空间不足的可能性。相反,我们决定调查为什么逻辑复制的性能如此缓慢。我们发现副本速度慢是由 Postgres 在目标数据库上维护索引的方式造成的。尽管逻辑复制会批量复制行,但一次索引一行的更新效率很低。通过删除目标数据库中的索引并在第一次数据复制后重建索引,我们将复制时间减少到几个小时。

    通过逻辑复制,我们能够构建从新分区的数据库回到原始数据库的反向复制流程。 在原始数据库停止接收流量后,此复制流会立即激活(更多信息见下文)。新数据库中的更改将被复制回旧数据库,如果我们回滚旧数据库将包含这些更新。

    关键步骤

    一旦解决了复制问题,我们就处于协调查询重定向的关键步骤。每天,数以千计的客户服务都会在任何给定时间查询数据库。如此多的客户端节点的协调很容易失败。通过分两步执行切片操作(首先对 PgBouncer 进行分区,然后对数据进行分区),数据分区的关键操作只需要在为分区表提供服务的几个 PgBouncer 节点之间进行协调。

    以下是所发生情况的概述:我们跨节点进行协调,仅短暂停止所有相关的数据库流量,以允许逻辑复制同步新数据库。 (PgBouncer 可以轻松支持暂停和重定向新连接。)当 PgBouncer 暂停新连接时,我们撤销客户端对原始数据库中分区表的查询权限。经过短暂的宽限期后,我们将取消任何剩余的航班请求。由于我们的应用程序主要发送持续时间较短的请求,因此我们通常会取消10个以下的请求。此时,在流量暂时停止的情况下,我们需要验证我们的数据库是否相同。

    在重新路由客户端之前确保两个数据库相同是防止数据丢失的基本要求。我们使用LSN来确定两个数据库是否同步。如果我们在确定没有新写入后尝试来自原始数据库的 LSN,则可以等待副本通过该 LSN 进行操作。此时,原件和副本中的数据是完全相同的。 Figma实现Postgres数据库架构扩展的方案

    同步机制的可视化

    检查副本同步后,我们停止复制并将副本提升到新数据库。反向复制的设置如前所述。然后我们恢复 PgBouncer 中的流量,但现在请求将路由到新数据库。Figma实现Postgres数据库架构扩展的方案

    计划概述

    规划我们的横向未来

    从那时起,我们在生产中执行了多次分区操作,每次都实现了我们最初的目标:在不影响可靠性的情况下完成工作。解决可扩展性问题。我们的第一次操作涉及移动两个高流量电表,而 2022 年 10 月的最后一次操作涉及 50 个电表。在每次操作期间,我们观察到约 30 秒的部分可用性影响(约 2% 的请求被拒绝)。如今,每个数据库分区的驱动器空间已大大增加。我们最大分区的 CPU 利用率约为 10%,并且我们减少了分配给一些流量较低分区的资源。

    但我们的工作还没有完成。现在有很多数据库,客户端应用程序必须维护每个数据库的知识,并且随着我们添加更多数据库和客户端,路由复杂性呈指数级增加。我们引入了一种新的查询路由服务,当我们扩展到更多分区时,该服务将集中并简化路由逻辑。我们的一些表的写入流量或磁盘占用量高达数十亿行和 TB,这些表将分别遇到磁盘利用率、CPU 和 I/O 瓶颈。我们一直都知道,只要 我们 依赖垂直分区,我们最终就会达到扩展极限。回到我们最大化杠杆率的目标,我们为垂直分区创建的工具将更好地为我们提供高写入流量的水平分区表。它为我们提供了足够的跑道来完成当前的项目,并保持 Figma 的“高速公路”畅通,同时仍然能够看到拐角处。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门