Figma 数据团队的横向分片之旅 | Figma博客
2024 年 4 月 4 日

我们九个月的旅程,将 Figma 的 Postgres 栈进行了水平分片,并揭开了(几乎)无限可扩展性的关键。

垂直分区是一个相对容易且非常有影响力的扩展杠杆,让我们很快获得了很大的操作空间。这也是通往水平分片之路上的一个垫脚石。

自2020年以来,Figma 的数据库栈已经增长了将近100倍。这是一个“好”问题,因为它意味着我们的业务正在扩张,但也带来了一些棘手的技术挑战。在过去的四年里,我们已经做出了重大努力,以保持领先并避免潜在的成长疼痛。2020年,我们在 AWS 的最大物理实例上运行了一个单独的Postgres数据库,到2022年底,我们已经建立起一个具有缓存、只读副本和十几个垂直分片数据库的分布式架构。我们将相关表(如“Figma 文件”或“组织”)分成了 他们拥有自己的垂直分区,这使我们能够获得递增的扩展收益,并保持足够的发展空间,以保持领先于我们的增长。 尽管我们在增量扩展方面取得了进展,但我们始终知道垂直分区只能带我们走这么远。我们最初的扩展工作集中在降低Postgres的CPU利用率上。随着我们的集群规模越来越大且更加异构化,我们开始监视一系列瓶颈。我们利用历史数据和负载测试结合起来,量化数据库在CPU和IO、表大小和写入行数方面的扩展限制。识别这些限制对于预测每个分片的可用空间至关重要。然后我们可以在问题膨胀之前优先处理扩展问题。 数据显示,我们的一些表格,包含数百万亿行数据,已经变得太大,无法在单个数据库中容纳。在这种规模下,我们开始在Postgres空闲时看到可靠性影响,这些是保持Postgres不会耗尽事务ID并崩溃的关键后台操作。我们的写入量最高的表格增长如此迅速,以至于我们很快就会超出亚马逊关系数据库服务(RDS)支持的最大IOPS(每秒IO操作)。在这里,垂直分区无法帮助我们,因为分区的最小单元是一个单独的表。为了使我们的数据库不会崩溃,我们需要一个更大的杠杆。

我们制定了一系列目标和必备条件,以解决短期挑战,同时为长期平稳增长做好准备。我们的目标是:

  • 减少开发人员的影响: 我们希望处理大部分复杂的关系数据模型。 在我们的应用程序支持下,应用程序开发人员可以专注于在 Figma 中构建令人兴奋的新功能,而不是重构我们代码库的大部分内容。

  • 透明地扩展规模: 在未来扩展规模时,我们不希望在应用程序层面进行额外的更改。这意味着在进行任何初始工作以使表格兼容之后,未来的规模扩展应该对我们的产品团队是透明的。

  • 跳过昂贵的回填操作: 我们避免了涉及在 Figma 的大型表格或每个表格进行回填的解决方案。考虑到我们表格的大小和 Postgres 的吞吐量限制,这些回填操作将需要数月的时间。

  • 实现渐进式进展: 我们确定了一些方法,可以逐步推出,以在我们降低主要生产变更的风险的同时减少风险。这降低了主要故障的风险,并使数据库团队能够在迁移过程中维持 Figma 的可靠性。

  • 避免单向迁移: 我们保持了可以回滚的能力。 回退操作,即使在完成物理分片操作之后仍可回退。这降低了在发生未知情况时陷入糟糕状态的风险。

  • 保持强大的数据一致性: 我们希望避免复杂的解决方案,如双写,这些解决方案很难在不停机或牺牲一致性的情况下实现。我们也希望找到一个解决方案,可以让我们在几乎零停机时间的情况下扩展。

  • 发挥我们的优势: 由于我们面临严格的截止日期压力,因此在可能的情况下,我们更倾向于采用可以逐步在我们增长最快的表上推出的方法。我们的目标是利用现有的专业知识和技术。

探索我们的选择 #

有许多流行的开源和托管解决方案可用于与Postgres或MySQL兼容的水平分片数据库。在我们的评估过程中,我们探索了CockroachDB、TiDB、Spanner和Vitess。然而,切换到这些替代数据库之一将带来重大的风险和复杂性。 我们需要进行复杂的数据迁移,以确保两个不同数据库存储之间的一致性和可靠性。在过去几年里,我们已经积累了大量关于如何在内部可靠高效地运行RDS Postgres的专业知识。在迁移过程中,我们将不得不从零开始重建我们的领域专业知识。考虑到我们非常激进的增长速度,我们只剩下几个月的时间。在必要的时间表上,降低完全新存储层的风险,并完成我们最关键的业务用例的端到端迁移将是极其危险的。我们更倾向于已知的低风险解决方案,而不是可能更容易但带有更高不确定性的选项,因为那样我们对结果的控制更少。

随着公司发展壮大,NoSQL数据库是另一种常见的默认可扩展解决方案。然而,我们目前的Postgres架构建立在非常复杂的关系数据模型之上,NoSQL API并不提供这种多样性。我们希望能够继续利用我们当前的基础架构,并且不愿意放弃这种灵活性。 为了让我们的工程师专注于发布出色的功能和构建新产品,而不是重写几乎整个后端应用程序;NoSQL 不是一个可行的解决方案。

考虑到这些权衡,我们开始探索在现有的垂直分区 RDS Postgres 基础设施上构建一个横向分片的解决方案。对于我们这样的小团队来说,在内部重新实现一个通用的横向分片关系数据库是没有意义的;这样做的话,我们将与大型开源社区或专门的数据库供应商建立的工具竞争。然而,由于我们将水平分片定制到 Figma 的特定架构中,我们可以提供一个更小的功能集。例如,我们选择不支持原子跨分片事务,因为我们可以解决跨分片事务失败的问题。我们选择了一种最大程度减少应用层所需更改的位置策略。这使我们能够支持 Postgres 的一个子集。 即使在这些较窄的要求下,我们知道水平分片将是迄今为止我们最大且最复杂的数据库项目。幸运的是,过去几年我们的增量扩展方法为我们提供了足够的时间来进行这项投资。在2022年底,我们开始解锁几乎无限的数据库可伸缩性,而水平分片——将单个表或一组表分割并将数据分布在多个物理数据库实例中的过程——是关键。一旦在应用层对表进行了水平分片,它就可以支持任意数量的物理分片。 我们可以通过简单地运行物理分片拆分来进一步扩展我们的数据库层。这些操作在后台透明进行,几乎没有停机时间,也不需要应用程序级别的更改。这种能力将使我们能够在剩余的数据库扩展瓶颈之前取得突破,消除了 Figma 的最后一个主要扩展挑战之一。如果垂直分区让我们加速到高速公路速度,水平分片可以消除我们的速度限制,让我们飞翔。 垂直分区

垂直分区是一种数据库设计方法,旨在将数据库表按照功能或属性进行分隔。这种方法有助于提高数据查询性能和降低系统负载。通过垂直分区,可以将频繁访问的数据与不经常使用的数据分开存储,从而实现数据的优化管理和访问控制。

在垂直分区中,通常会将常用的数据存储在一个表中,而将不经常用到的数据存储在另一个表中。这样可以减少在查询时涉及到的数据量,提高查询速度。此外,垂直分区还可以根据不同的用户角色或权限需求,将数据进行分割,实现更精细化的数据访问控制。

总的来说,垂直分区是一种优化数据库设计的方法,可以根据实际需求和业务场景,将数据进行有效的划分和管理,从而提升系统性能和数据访问效率。 这是一个包含Base64编码图像的Markdown内容。

iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACw0lEQVQ4jYWTa2/aZhiG+6fbKmmUlkAAG3yKgZK0lB3aqatWbUu3NSRBPXxIOBS/xgY3hNiAISVZVm3aH7gm280U7cs+XLL8Wr7f+34Ot4ZXD4kY/IfozL0o44gCo2/vEap3mCt3OK2tYh/l6AwKdF/msXM6dl5H/FLAXZS4Nfi9wuBTieFkC+/MwPMNBqGJuywj5lv0j2T8WpG5ViFUKvhVBdHM0+pLdF9J2KqG0FTEXhH3PBK8KDMUBcavUky/X2fywzofmxnEiUL7TMF6KzGtP2JR/olZ6UeCnceIhkzbkeg1Nunra9jGGnYzh/spEjwvMdrPcK7d5XP6NlfZ2wS1VUQnT8dXsY8lgrrMQjeYazr+joRo5mg5Mt1fs1hKGktNI/bzicPheYlxo8DS1PhD2eZKrTB5LCGOJXozHedE46SZJXiZihk1MthOgY5XpLMrYSkGlqpjRZEXXwRPmyaL+gsu629Y1hr4T3ewjmV6oR7HGAQGQ0+NGZwlZ/bUwGpksbX72MZ9xGHuhsN9hYtKhT/Np3w260yfKLFDKzRwAwOvLXFymGF0kME7yuOONYSv09vLIrQUtpFCHH6JHNVwvJfhUlnh7/Qqf22uMNu5h93K82Gi0e/KjL/JEBpZQj3LWS2N/T5HO6rhroxQDYRmJF2Ox2ZZxmvlCb5bY/FohfmTFUa7DxBuIenyO4mgViHces7UeI5frSL25XhsPvwWRU4h9BTi4NphNIehGdfHs4sMHYXBqY6zMOlNdcR7Cb+2xUz/iqn2NX7VjDvaciS6rzcR2jpCX09qGI3Nv1sSCV9WkmdEtCnLMq5dZPwizWx7g7C6wfjZBuIoEpRp/yzRK2j0FA3r9Y1NGcYk6xYTvV9fMjcZugqjthTj9Ys4EwMR6PQOJKztBPFGvu 水平分片

水平分片比我们以前的扩展工作复杂得多。当一个表分割到多个物理数据库时,我们失去了在ACID SQL数据库中视为理所当然的许多可靠性和一致性属性。例如:

  • 某些 SQL 查询变得低效或不可能支持。
  • 应用程序代码必须更新,以提供足够的信息,以便尽可能有效地将查询路由到正确的 Shard。
  • 必须协调所有 Shard 上的模式更改,以确保数据库保持同步。 外键和全局唯一索引不能再由 Postgres 强制执行。
  • 事务 现在可以跨多个分片,这意味着无法再使用Postgres来强制执行事务性。现在可能会出现这样的情况:对某些数据库的写入成功,而对其他数据库的写入失败。必须注意确保产品逻辑能够抵御这些“部分提交失败”(想象一下将团队从一个组织转移到另一个组织,却发现一半的数据丢失了!)。

我们知道实现完全水平分片将是一个多年的工作。我们需要尽可能降低项目风险,同时提供增量价值。我们的第一个目标是,尽快将生产中一个相对简单但流量很高的表进行分片。这将证明水平分片的可行性,同时延长我们最繁忙数据库的使用时间。然后,我们可以继续构建其他功能,同时努力对更复杂的表组进行分片。即使是最简单的功能集仍然是一个重大的工作。从头到尾,我们的团队大约花了九个月的时间来对我们的第一个表进行分片。 我们独特的方法

我们的水平分片工作基于许多其他人所做的,但有一些不寻常的设计选择。以下是一些亮点:

  • Colos:我们将相关表组的水平分片到共同位置(我们亲切地称之为“colos”),它们共享相同的分片键和物理分片布局。这为开发人员与水平分片表进行交互提供了友好的抽象。
  • 逻辑分片:我们在应用层将“逻辑分片”的概念与Postgres层的“物理分片”分开。我们利用视图来执行更安全和成本更低的逻辑分片推出,然后再执行更具风险的分布式物理故障转移。
  • DBProxy查询引擎:我们构建了一个DBProxy服务,拦截应用层生成的SQL查询,并动态路由查询到各种Postgres数据库。DBProxy包括一个能够解析和执行查询的查询引擎。 横向分片查询。DBProxy还使我们能够实现动态负载分担和请求敷衍。
  • **阴影应用准备度:**我们添加了一个“阴影应用准备度”框架,能够预测生产流量在不同潜在分片键下的行为。这使产品团队清楚地了解需要重构或删除哪些应用逻辑,以准备应用进行横向分片。
  • **完整逻辑复制:**我们避免了实现“过滤逻辑复制”(仅将数据子集复制到每个分片)的需要。相反,我们复制整个数据集,然后只允许读取/写入属于给定分片的数据子集。

我们的分片实现 #

横向分片中最重要的决定之一是使用哪个分片键。横向分片增加了许多围绕分片键的数据模型约束。 例如,大多数查询需要包含分片键,以便将请求路由到正确的分片。某些数据库约束,如外键,只有在外键是分片键时才有效。分片键还需要将数据均匀分布在所有分片上,以避免热点问题,导致可靠性问题或影响可伸缩性。

Figma存在于浏览器中,许多用户可以并行在同一个Figma文件上进行协作。这意味着我们的产品由一个相当复杂的关系数据模型驱动,捕获文件元数据、组织元数据、评论、文件版本等等。

我们曾考虑为每个表使用相同的分片键,但在我们现有的数据模型中没有一个好的候选者。要添加统一的分片键,我们必须创建一个复合键,将该列添加到每个表的模式中,运行昂贵的回填以填充它,然后大幅重构我们的产品逻辑。相反,我们根据Figma独特的数据模型来调整我们的方法。 我们为每个表选择了一些分片键,如UserID、FileID或OrgID。几乎Figma的每个表都可以使用这些键进行分片。

我们引入了colo的概念,为产品开发人员提供了友好的抽象:在colo内部的表支持跨表连接和完整事务,当限定为单个分片键时。大多数应用程序代码已经以这种方式与数据库交互,这最大程度地减少了应用程序开发人员为让表准备好进行水平分片所需的工作。 表按UserID和FileID分片,每个都放在一起

三个数据孤岛,每个都包含“文件表”和“文件评论表”的区块。 选定分片键后,我们需要确保数据在三个数据存储中均匀分布。 数据在所有后端数据库之间的分布是一个挑战。不幸的是,我们选择的许多分片键都使用自增或Snowflake时间戳前缀的ID。这将导致一个单个分片包含大部分数据的显著热点。我们探索了迁移到更随机化ID,但这需要昂贵且耗时的数据迁移。因此,我们决定使用分片键的哈希进行路由。只要选择了足够随机的哈希函数,我们就能确保数据的均匀分布。其中一个缺点是,对分片键的范围扫描效率较低,因为连续的键将被哈希到不同的数据库分片上。然而,在我们的代码库中,这种查询模式并不常见,因此这是我们愿意接受的权衡。

为了降低水平分片推出的风险,我们希望在应用程序层将准备表的过程与物理层分离。 逻辑分片和物理分片是运行分片拆分过程中的两个关键步骤。通过将这两个步骤分离,我们能够独立实现和降低风险。逻辑分片让我们能够有信心在服务堆栈中进行低风险的、基于百分比的部署。当发现错误时,回滚逻辑分片只需进行简单的配置更改。回滚物理分片操作也是可能的,但需要更复杂的协调以确保数据一致性。

一旦表进行了逻辑分片,所有的读写操作都会像表已经水平分片一样。从可靠性、延迟和一致性的角度来看,我们看起来已经进行了水平分片,尽管数据仍然物理上位于单个数据库主机上。当我们确信逻辑分片运行正常后,我们会执行物理分片操作。这个过程涉及将数据从一个数据库复制并进行分片。 使用多个后端的OSS,然后通过新的数据库重新路由读写流量。

两个数据孤岛,逻辑和物理碎片块。 两个数据孤岛,具有逻辑和物理碎片的块。

可能的查询引擎 #

为了支持水平分片,我们必须对后端堆栈进行重大重新架构。最初,我们的应用服务直接与我们的连接池层PGBouncer通信。然而,水平分片需要更复杂的查询解析、规划和执行。为了支持这一点,我们构建了一个新的golang服务,DBProxy。DBProxy位于应用层和PGBouncer之间。它包括负载分担逻辑,改进的可观测性,事务支持,数据库拓扑管理以及轻量级查询解析。 PG Bouncer, and then to the database.](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACJ0lEQVQ4jW2Sa1PaQBSG/fO1Eq3KLXibWhG1hVbH6bSOVju1dZRbjOzZBLAVUQRBfsTT2QQBLx/eycnmPc+eSya8fopx6QfztNGdRTwnR333hH9ZJ5CJPScbfjOewPtUEyHEHsAeTTa6m0L+rqJLOeT3Dup4GynkwrPuwPPwNDcEPoyBxmTO5G4BkQz6ZCeAiruF3C4Nk0feVBgboO7bgxdjMmAb6SWpdBNc3NiI8xH/YB9/7wCd/0ylsUTlPoHqJcNC+qM8oycV6l6KSjuJ04xRbkYpN2I43hJOaQ2nuIajlilfxcNv1zHcVgLphhW+bLmziK6nqZQ3Oc9ncOU9btPGvYsHiYFMfJvA9VdwixncwgbiraPvll8B3qzgF7ap7h3ifz/Ey28jzZVRW8bXt1GtBbzzLNX9H/jfjvD+7KIbq8ONj4DtRbSXQfI51FkW0ebmwQIeh9+3kc4CupZGilnUaRapbKBbYxUO196zqVzFcUozlM8sLmpR9H1Y3ePAg7hnoxpxnPIMpVOLi+o80gkX5JmlDIGdBFKwkM03SHoSObaQ2xg6+D1CYOC7T6KcafSnSXT6LepoCrmODsYyDmzF0YcRarF31Oei+F+nkYYxPgO24+hfEWr2DJezMfQXC3U59wqwnUBOLfz1KaofLPRPC7l5pULTSXEaf3OK6qqFHERQwcXPgWY2V/OokoUUIqj6XNDeC2AwwyjiWEg+gqrOhjMc+P4D9RhT0MygMvMAAAAASUVORK5CYII=)

应用层流向DB代理,PG Bouncer,然后流向数据库。 PG Bouncer,然后连接到数据库。

DBProxy的查询引擎是其核心。其主要组件包括:

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

一条查询流向“where”,然后导向“shard_key”和“other_col”。 xpUfNyF1trg/6fkdwkkk6doqUpVlPFRW8FB17FZbQoIu8vFOX+Lf07XX7yzqnOVFvrHGibVnAR4WPun5+CpSZ/OnlVqbKDY2UGhsoNjYRKnpsraDYT5yl0XngzpwZPAYP0RMTB+xUYDaF6DUdkDlQ2jSZ6jSMTf2TMkBFDsIbShwrD5i4yuHxy2oMRKhM8LONvTCKdqXebSiJdS/ZWFH8niKyWjHM1CVI9BegBMazMe9KXMJH4Oops/xeEpR/UMCCWWh7xbQOtTRiRJopT9BuwGONd4SusN1E2rprzC/5GCE81A/ZKCFs6gdFFGPZEBLn6YIDXfKbzXkhF0RihKGdncGI/EdejzGzUjEoKW+QdH3oPadlPXfETIQ2376JEJt7YDYIRS0AAp6ANTegdoO8QX1Z+E/EP6Y7NpEkwDk7ibujEXcV5ag9LZe9eYBCE7a/0boCDyOciiAtLchV3ZQkIMoykHI1RBoR5wiNH6b8lhH2hWgKB9RiV/CvLhF9XsCWuIctLIPndfgWPPRLwjdO006AmjuBPbXAqxDBeaBhFokBZUcQRu6NRdnCad1cUUoHaB6dQPzPIVq9A7adQxE33edEvE19beEbzVhTrQZBDX2QWgYCv0AUtlzTtFPInHa152y+wNrGHTgB+37IXe2eFfJ1r28GbB51ghYab0lM+ZFyDpHobGGbN1pTaxdJcgCknQBGXOFN9ystYpia52T6/MinLywVQvNddwb75FhhLbTrafMZt2b9cNl3mT1518RDgPcIW2uQOn5eOqso7OFmDnPAShdHydljVV7DkwTuneYOfD7o77Kdaq8zJYFm1MHficDy8t93JUy3hSBA5kmLJVU5T1/Nl7cJTU+Zi8iSN/HZWH6qsPphT08lYEAbSBAefIhpS8jZSxDefLz0mHGSVnaY5zc8eFec+4f0vU782OcR7ZFEHUfVDkEkQ8gFcOQSmH+TulHKPUQvxLYyVGMPX4FTHDyBEcOoVR2QToB 查询解析器

一个查询从“where”流向“shard_key”和“other_col”,然后流向“逻辑选择”。 cUHslENUJF6xo3l5me4Ik2gNEHRMk2v3MdYtMO70okgN1WSe0bJFw3kZ8rJ/p9giK1U+kzot6N0C6SWb6YQA10mG0ZOuEC1ZUz0MS3V40hw/N4SXW5iPePonWO4wstm5NYXSthxkBSb2HGuhCmegmPNpBZOw+qq8bOdiOlL6LurKFHkZXg/rJKRkL8pyVUKwOt/8O3lANYsqKvFhnqNvSKWu5LWQJRbURcayL4EAXoaddiL42pGkb0Zw6i5H7FuH6u0VX+ygvCEjeTpLdPpL2IHF7AO2xC0lpXmcZy1tK8zaSWXIHIy8KyJP3SfV4SXdOkbzvR3M+Q1abNngw+h9Sg3A91JdmxEwtwdlq/KLA1HgrU8MOAsN2/BONBGImwgvVSJla1BXzhlqDcP3+dbKpuSo8iUo88QrGtUrccgUjkRsGxpQKPLFszJuoJDh/J3fauR4aqlbMBvQEl3SVUfU6gdnbBOerCC68wZR+PV+Ff+YWI/I1RpRrhBerc/WGQmnRgpi0IcUamBKtuL01+IIWRK0BMV6PNCegLFuQlyyIqWyeGG3AGzDjnqghKNuQ4g2IaStyxkyeXqiO9JIYGCHRP0LcOUJyYJSZoXFSIy7kiB1x0Wx4MOrpIfV0lOTgKIn+LKafjjPjGiPqe0A4LZAnia3Ee92k7RHSDpHZdpm5DoWlrjgLvZIxHZE5ExGt3rjpbIfMjENirl1h4X6UTHeS5Z4kSZfLGII83bzq0BMSj8cMJPvGST3xMNs/SXrIjRxqQ9IVJm1o7j7STg+pPk82r8/DTL+P2UGfoT6c1BXOC4ixBmS1BUVtRVFbDPOKcjNhpZFQSiCyVEt43kRIsxGRmoyYrOj5LdmaaAtSsh75+aoP9aPXzapDemHiWaqS5sjfWIN/YQuVcTechf7bFiyjU77ChG6Z1RoDK5v4MPZaIJSpwTL1JwcfneLTtmN85jhqrFkc5fOO4/w48As98etvPRi0NR+uH5/AYjVXPL/ 逻辑规划

在查询中,流向“where”再到“shard_key”和“other_col”。然后流向“逻辑选择”。

逻辑规划涉及逻辑选择计划,然后是逻辑单个分片计划;接着是物理选择计划。 物理单片查询。

这导致物理单片查询。 ijLZgLNWJ2B1nDjXDFWnK1r+TAHm1Gt6cGP5ku4tupC7hszC6SqzNfglaso9CsQFLO9PItfDXxJV7/7WPsaT+OwtslKFSV4lVVSUqlKOwoxR5VCd688wmFWiNNdG1WoGn5FqrvncNrqo/wEnsUuxXHsFt5LGnTflsxXlYW442uMtRMX4Q1nANIrG2lCe2OH3HhfjUqx8/izHgVVaXhLCoNX+AMid2rQuV4Fb6ZPI9erhbOXCUTy8flmF9pxnRIRndrCskwFbwJg1QPg/c69U2pnDkkgyPaTC8yK1DMeAL8mhzcaiu4NTkc4SZMCfUwitfhXGmmMZIjcx674fUsOyTWsizDHa4WcvsVtNqvoNn6HX4xfo0G0yW02L6nMcbxAzRiHWyRps3NZC1ZiMuhla7h1Ggl3lKfxL7uE9jXVY69Ke3rLMfenhN4e+BTVE1UY8z/M91ZTiApo4urwf67p/Es+yHyG95Dfv0h5N84hPzrh5B/7SCevvU+CjqOo2SkArqFekjx7YBxOTq5GryjOYVnWouQV38QeTX7kXf1APLq3sVTtQeQ13AYz6mKUfzHZ9B5/wewh6vFYc1pvMgeRUFzEQpkHyRtyxG80FSE51uP4JWOEpTpKzC80w7JExjx3cDlyfM4oa9A2XBKxNdXoDwVOznyOa6aL2Ey0LD9GRLriLbgz0ADBefSqO8GjMGbcMdaU8CtBpPRvlItiLSx9LvaUakWltG+/gU6NlRL0/vzmgAAAABJRU5ErkJggg== 物理规划师

将**“分散-聚集”**类比为整个数据库的捉迷藏游戏:您向每个分片发送查询,然后从每个分片汇总答案。这很有趣,但如果过度使用,您的高速数据库将开始感觉更像一只蜗牛,尤其是在处理复杂查询时。

在水平分片的世界中,一些查询相对容易实现。例如,单分片查询会被过滤到单个分片键。我们的查询引擎只需提取分片键并将查询路由到相应的物理数据库。我们可以将查询执行的复杂性“下推”到Postgres。然而,如果查询缺少分片键,我们的查询引擎就必须执行更复杂的**“分散-聚集”**。在这种情况下,我们需要将查询分发到所有分片(分散阶段),然后汇总结果(聚集阶段)。在某些情况下,例如复杂的聚合、连接和 嵌套SQL,这种散射收集可能非常复杂。此外,拥有太多的散射收集会影响水平切分的可伸缩性。因为查询必须触及每个数据库,每个散射收集都会产生与未切分数据库一样多的负载。

如果我们支持完整的SQL兼容性,我们的DBProxy服务将开始看起来很像Postgres数据库查询引擎。我们想要简化我们的API以最小化DBProxy的复杂性,同时减少我们的应用开发人员需要重新编写任何不受支持的查询的工作量。为了确定正确的子集,我们构建了一个“影子规划”框架,允许用户为他们的表定义潜在的分片方案,然后在生产流量之上运行逻辑规划阶段的影子。我们将查询和相关查询计划记录到Snowflake数据库中,以便进行离线分析。通过这些数据,我们可以选择合适的方案。 需要一个支持最常见90%查询的查询语言,但又能避免在查询引擎中出现最坏情况的复杂性。例如,所有范围扫描和点查询都是允许的,但只有在连接两个位于相同colo的表时才允许连接,且连接是在分片键上进行的。

然后我们需要找出如何封装我们的逻辑分片。我们探讨了使用单独的Postgres数据库或Postgres模式对数据进行分区。不幸的是,当我们逻辑分片应用程序时,这将需要进行物理数据更改,这和进行物理分片拆分一样复杂。

因此,我们选择用Postgres视图来表示我们的分片。我们可以为每个表创建多个视图,每个视图对应于给定分片中的数据子集。这将看起来像:CREATE VIEW table_shard1 AS SELECT * FROM table WHERE hash(shard_key) >= min_shard_range AND hash(shard_key) < max_shard_range。所有读写 通过这些视图发送到表的查询。

通过在我们现有的非分片物理数据库上创建分片视图,我们可以在执行任何冒险的物理重新分片操作之前逻辑地分片。每个视图通过自己的分片连接池服务进行访问。连接池仍然指向非分片的物理实例,这给人一种被分片的外观。我们能够通过查询引擎中的功能标志逐渐减少分片读写操作的风险,并通过将流量重新路由回主表在几秒钟内随时回滚。在进行第一次重新分片时,我们对分片拓扑的安全性充满信心。 通过在非分片数据库中创建多个视图,我们可以查询这些视图,就好像数据已经被物理分片一样。

当然,依赖视图也会带来额外的风险。视图会增加性能开销,在某些情况下可能会从根本上改变Postgres的工作方式。 查询规划器优化查询。为了验证这种方法,我们收集了一组经过处理的生产查询的查询语料库,并运行了带有和不带视图的负载测试。我们能够确认在大多数情况下,视图只会增加很小的性能开销,最坏情况下不到10%。我们还构建了一个影子读取框架,可以通过视图发送所有实时读取流量,比较视图与非视图查询的性能和正确性。然后我们能够确认视图是一个可行的解决方案,几乎没有性能影响。

解决我们的拓扑问题 #

为了执行查询路由,DBProxy必须了解我们表和物理数据库的拓扑结构。由于我们区分了逻辑分区和物理分区的概念,我们需要一种方法在我们的拓扑结构中表示这些抽象。例如,我们需要能够将一个表(用户)映射到其分片键(user_id)。同样,我们需要能够将逻辑 将Shard ID(123)映射到适当的逻辑和物理数据库。在垂直分区过程中,我们依靠一个简单的硬编码配置文件,将表映射到它们的分区。然而,随着我们向水平分片迈进,我们需要更复杂的东西。我们的拓扑结构会在分片拆分期间动态变化,DBProxy需要快速更新其状态,以避免将请求路由到错误的数据库。因为拓扑结构的每一次更改都是向后兼容的,这些更改永远不会成为我们网站的关键路径。我们建立了一个数据库拓扑结构,封装了我们复杂的水平分片元数据,并可以在不到一秒的时间内提供实时更新。 这里是Markdown内容:/523p/hrc4JUcZxUcYw/5TBr4lmy2UFyueNsZT/HfbyE8zhLQYohKNYsau0aau1blGoUuXwJufRVgFSZIGtMsqJ/zWplkjXjMk+0K6yvzmBkotQyUdz07+wtabSTRTTxF4SiEqOcfUh1bRE9fRPt7jmk2ADSrQHER5+wKo0EMsmZRm5EKJR+wM4k6S5usr9Q4mnCoJdo0UluoIm/IpSXF/D6nwkb/26a+jefURobYGt8AO36Sdb+DgdCuTEdrEEr3aCZzvI8vs1B3Kc37/FfvHMkrGVS7C1YHMQ7PL1v4MeSeDdu4/10G/NeDFGMBlFlZxqlGaGoX8daTrOzVGFvyXyNm85TlGMI1UyK7oJJb75Nd87Bv2fx7GGT5/MuzWQepfBjIOxHVpozKLVraOot9Ox9KrkHAeXsHJvSb8il746E/Qn/nf+HZ49aHMQ9eolt3JT4WtifsB9Zdq4iV8cpGmE2zGE2zTBadRjFGEW2rryKbELch8QRvXgbN/lKOIlUv4rSiCDXJ9Cs89TcD3E7J3D9E9itD9iyz6LYowjllQTbf2iHu6gdYrK7aGBnlpGK37NSOqpNzrhAyR6i5YXY2Qux0w3R9o9htT5Gtb5EKBRvsiXeoZx/8AZ6fo519WdEfTYQZjYuBgV/oofR66fY7rwU+t0Q3s4xnPZHFKwvEOR6FNmapV/wd7CjyPUIOXPqZamrlxHNUfT6GVreILv7x9ndH6TthzDdk6j2MMK7p/cWzbdwLqHZn1JpnKbuDeF4Q9TcU6zb51HqYwhqc4b38v/7bkwjWReRzAso1giqPYJsjiCZY8jOFC8A5ulhgMS77poAAAAASUVORK5CYII=)

拓扑图书馆引导您进入一个正方形(S3)和一个圆柱体(ETCD)。 在拥有单独的逻辑和物理拓扑结构的情况下,我们也能简化一些数据库管理工作。例如,在非生产环境中,我们可以保持与生产环境相同的逻辑拓扑结构,但是可以从更少的物理数据库中提供数据。这样既省成本又降低复杂性,而且在不同环境中变化不会太大。拓扑结构库还可以让我们在整个拓扑结构中执行不变量(例如,每个分片 ID 应映射到一个物理数据库),这对于在构建水平分片时维护系统的正确性至关重要。

物理分片操作 #

一旦表准备好进行分片,最后一步是从未分片到已分片数据库的物理故障转移。我们能够重新使用水平分片的大部分相同逻辑,但也有一些显著的区别:与移动数据不同,我们需要重新分配数据库实例,并且需要确保数据的一致性。 从1到1的数据库,我们正在从1到N。我们需要使故障转移过程能够适应新的故障模式,其中分片操作只能在我们的数据库子集上成功。尽管在垂直分区期间许多风险最高的组件已经被降低风险。我们能够比以往更快地向我们的第一个物理分片操作迈进。

当我们开始这个旅程时,我们知道水平分片将是对Figma未来可扩展性的多年投资。我们在2023年9月发布了我们的第一个水平分片表。我们成功进行了故障转移,主要数据库仅有十秒的部分可用性,对复制品没有可用性影响。分片后,我们没有看到延迟或可用性的退化。从那时起,我们一直在处理我们写入速率最高的数据库的相对简单的分片。今年,我们将越来越多地分片处理更复杂的数据。 在Figma,我们将需要对每个表进行水平分片,以消除我们最后的扩展限制,真正展翅高飞。完全水平分片的世界将带来许多其他好处:提高可靠性、节省成本和提高开发速度。在这个过程中,我们需要解决所有这些问题:

  • 支持水平分片模式的模式更新
  • 为水平分片的主键生成全局唯一ID
  • 用于业务关键用例的原子交叉分片事务
  • 分布式全局唯一索引(目前唯一索引仅在包含分片键的索引上受支持)
  • 一种增加开发速度并与水平分片无缝兼容的ORM模型
  • 可以通过点击按钮运行分片拆分的完全自动化的分片操作

一旦我们拥有足够的时间,我们还将重新评估我们最初的本地RDS方法。 水平分片。我们在18个月前开始了这个旅程,时间非常紧迫。NewSQL数据库一直在不断发展和成熟。我们终于有时间重新评估继续走目前道路的权衡,还是转向开源或托管解决方案。

我们在水平分片旅程中取得了许多令人振奋的进展,但我们的挑战才刚刚开始。请继续关注我们对水平分片堆栈不同部分的深入探讨。如果您对参与这样的项目感兴趣,请联系我们!我们正在招聘 (opens new window)

没有当前和前数据库团队成员的支持,我们无法完成水平分片的上线:Anna Saplitski、David Harju、Dinesh Garg、Dylan Visher、Erica Kong、Gordon Yoon、Gustavo Mezerhane、Isemi Ekundayo、Josh Bancroft、Junhson Jean-Baptiste、Kevin Lin、Langston Dziko、Maciej Szeszko、Mehant Baid、Ping-Min Lin、Rafael Chacon Vivas、Roman Hernandez、Tim Goh、Tim。 我们还要感谢所有跨职能合作伙伴团队,特别是Amy Winkler,Braden Walker,Esther Wang,Kat Busch,Leslie Tu,Lin Xu,Michael Andrews,Raghav Anand和Yichao Zhao。