数据库规模四年增长近100倍,Figma 如何“活”下来?
随着数据库集群变得越来越大,越来越多样化,我们开始监控一系列瓶颈。我们结合了历史数据和负载测试来量化数据库扩展的限制,比如CPU、IO乃至表的大小和写入的行数。找出这些限制对于预测每个分片还剩多少空间至关重要。这样,我们就可以在这些限制演变成重大可靠性风险之前,优先处理扩展问题。
原文链接:https://www.figma.com/blog/how-figmas-databases-team-lived-to-tell-the-scale/
未经允许,禁止转载!
作者 | Sammy Steele 译者 | 弯月
责编 | 夏萌
出品 | CSDN(ID:CSDNnews)
自2020年以来,Figma的数据库规模增长了近100倍。这是一个好消息,因为这意味着我们的业务一直在扩展,但这也带来了一些棘手的技术挑战。在过去的四年里,为了保持领先地位,我们付出了很大的努力,同时也避免了一些潜在的增长烦恼。2020年,我们只有一个Postgres数据库,运行在AWS最大的物理实例之上。2022年底,我们构建了一个具有缓存、读取副本和十几个垂直分区数据库的分布式架构。我们将相关表的组(比如“Figma文件”或“组织”)分成了垂直分区,这样就可以实现增量式扩展,并为保持增长的领先地位留下足够的空间。
尽管增量式扩展进行得很顺利,但我们知道垂直分区只能让我们止步于此。我们最初的扩展工作集中在减少Postgres的CPU利用率上。随着数据库集群变得越来越大,越来越多样化,我们开始监控一系列瓶颈。我们结合了历史数据和负载测试来量化数据库扩展的限制,比如CPU、IO乃至表的大小和写入的行数。找出这些限制对于预测每个分片还剩多少空间至关重要。这样,我们就可以在这些限制演变成重大可靠性风险之前,优先处理扩展问题。
数据显示,我们的一些表已经变得过于庞大,包含数TB乃至数十亿行数据,超出了单个数据库的承受力。鉴于这个规模,我们在清理Postgres期间已经看到了可靠性问题,而清理是关键的后台操作,可确保Postgres不会耗尽事务ID并崩溃。写入表的增长速度如此之快,以至于我们很快就会超过亚马逊关系数据库服务支持的IOPS(每秒IO操作数)的最大值。垂直分区已经无能为力了,因为分区的最小单位是单个表。为了避免数据库崩溃,我们需要更好的办法。
规模支撑结构
我们列出了一些目标,以及为解决短期挑战、同时为长期发展做好准备而必须完成的工作。我们的目标是:
减少对开发者的影响:我们希望处理好应用程序支持的大部分复杂的关系数据模型。这样,应用程序开发人员就可以专心在Figma中构建令人兴奋的新功能,而无需重构代码库中的大部分代码。
透明地扩展规模:在未来的扩展中,我们不希望应用程序层有任何额外的改动。这意味着,任何前期的工作都要保证表的兼容性,未来的扩展应对产品团队保持透明。
跳过昂贵的回填:尽可能避免需要大量回填表的方案。考虑到表的大小和Postgres的吞吐量约束,这些回填可能需要花费数月的时间。
增量式扩展:我们确定了一些方法,可以在逐步推出的过程中降低重大生产变更的风险。这样可以降低重大故障的风险,并允许数据库团队在迁移过程中保证Figma的可靠性。
避免单向迁移:在物理分片操作完成后,我们依然保留了回滚的能力。这降低了在未知情况发生时陷入困境的风险。
保持强大的数据一致性:我们希望避免复杂的解决方案,比如双重写入,因为这些解决方案很难在不停机或不牺牲一致性的情况下实现。此外,我们也希望找到一种解决方案,使我们能够实现几乎零停机时间的规模扩展。
发挥我们的优势:由于我们面临着紧迫的截止日期,因此在可能的情况下,我们倾向于采用可以在增长速度最快的表上逐步推出的方法。我们的目标是利用现有的专业知识和技术。
探索可能的解决方案
流行的支持Postgres或MySQL水平分片数据库的开源以及托管解决方案有很多。在评估的过程中,我们探索了CockroachDB、TiDB、Spanner和Vitess等解决方案。然而,为了确保两种不同的数据库存储之间的一致性和可靠性,切换到这些数据库需要进行复杂的数据迁移。另外,在过去几年中,我们积累了大量有关可靠地、高性能地在公司内部运行RDS Postgres的经验。如果要数据迁移,我们将不得不从头开始学习专业的领域知识。由于Figma的增长非常迅速,我们只剩下几个月的时间了。在有限的时间内,使用全新的存储层,并完成关键业务用例的端到端迁移是非常危险的。我们更倾向于选择已知的低风险解决方案,而不是看似很容易、但具有更高不确定性的方案,因为对于后者,我们很难控制最终结果。
NoSQL数据库是各家公司在发展过程中采用的最常见的另一种默认的可扩展解决方案。然而,我们在当前的Postgres架构上建立了一个非常复杂的关系型数据模型,NoSQL API并没有提供这种灵活性。我们希望让工程师们专心发布出色的功能并构建新产品,而不必重写几乎整个后端应用程序,因此NoSQL并不是一个可行的解决方案。
考虑到这些权衡因素,我们开始探索在现有的垂直分区RDS Postgres基础架构上构建水平分片解决方案。我们是一个小团队,在内部重新实现一个通用的水平分片关系型数据库没有任何意义,因为在这种方案下,我们将与大型开源社区或专业数据库供应商建立的工具竞争。然而,由于我们的水平分片是根据Figma的特定架构定制的,因此我们需要提供的功能比较少。例如,我们选择不支持原子交叉分片事务,因为我们可以解决交叉分片事务失败的问题。我们选择了一种能够最大程度地减少修改应用程序层的必要性的协同定位策略。这样,我们只需支持与产品逻辑大部分兼容的一部分Postgres就可以了。此外,我们还能轻松地在分片和非分片的Postgres之间保持向后兼容。如果遇到未知的情况,还可以轻松地回滚到非分片Postgres。
水平分片
虽然缩减了需求范围,但我们知道水平分片将是迄今为止我们遇到的规模最大、最复杂的数据库项目。幸运的是,过去几年的增量式扩展法为我们提供了足够的时间来进行这项投资。2022年末,我们开始着手实现几乎没有上限的数据库可扩展性,而水平分片(拆分一个表或一组表拆并将数据分布到多个物理数据库实例之间的过程)是关键所在。只要在应用程序层对表进行了水平分片,就可以支持任意数量的物理层分片。我们只需简单地运行一个物理分片拆分,就可以进一步扩展。这些操作在后台透明地进行,几乎没有停机时间,也不需要应用程序级的变更。这种能力可以让我们继续保持领先,摆脱剩余的数据库扩展瓶颈,消除Figma面临的最后一个主要扩展挑战。如果说垂直分区让我们加速到了高速公路,那么水平分片就相当于让我们摆脱了速度的限制,展翅高飞。
相较于之前的扩展工作,水平分片的复杂度又高了一个数量级。当一个表被分割到多个物理数据库中时,我们将失去许多ACID SQL数据库中视为理所当然的可靠性和一致性属性。例如:
某些SQL查询的效率降低,或无法支持。
必须更新应用程序代码才能提供足够的信息,以便尽可能高效地将查询路由到正确的分片。
必须在所有分片之间协调表结构变更,以确保数据库保持同步。Postgres无法再保证外键和全局唯一索引。
事务跨多个分片,这意味着Postgres无法再保证事务性。可能会出现写入某些数据库成功而写入某些数据库失败的情况。必须小心确保产品逻辑拥有一定的弹性,能够处理这类的“部分提交失败”(想象一下在两个组织之间移动某个团队,结果发现一半的数据丢失!)
我们知道完全实现水平分片需要付出多年的努力。我们需要尽可能降低项目的风险,同时提供增量价值。我们的第一个目标是尽快在生产环境中针对一个相对简单但非常高流量的表进行分片。这可以验证水平分片的可行性,同时延长处理负载最重的数据库的时间。然后,针对更多复杂的表进行分片,同时构建其他功能。虽然我们需要实现的功能非常少,但任务仍然很重。我们前后大约花费了九个月的时间才完成了第一个表的分片。
独特的方法
我们的水平分片的工作借鉴了许多其他人的做法,但也有一些不同寻常的设计选择。重点如下:
Colocation:我们将相关的表进行水平分片,分成许多组Colocation,同一组共享同一个分片键和物理分片布局。这为开发人员与水平分片表交互提供了友好的抽象。
逻辑分片:我们将应用层的“逻辑分片”概念与Postgres层的“物理分片”分开。利用视图执行较为安全且成本较低的逻辑分片部署,然后再执行比较危险的分布式物理故障转移。
DBProxy 查询引擎:我们构建了一个名为DBProxy的服务,用于拦截应用层生成的SQL查询,并动态地将查询路由到各种Postgres数据库。DBProxy包含一个查询引擎,能够解析和执行复杂的水平分片查询。此外,我们还可以通过DBProxy实现动态负载分担和请求保护等功能。
影子应用程序就绪:我们添加了一个“影子应用程序就绪”框架,能够预测不同潜在分片键下生产环境真实的流量行为。这样,产品团队就可以清楚地看到为使用水平分片应用程序需要重构或删除的逻辑。
完整的逻辑复制:我们避免了“过滤逻辑复制”(这种方法只能将一部分数据复制到每个分片)的实现。相反,我们复制了整个数据集,且只允许读写到属于特定分片的部分数据。
实现分片
水平分片中最重要的决定是使用哪个分片键。水平分片引入了许多有关分片键的数据模型约束。例如,大多数查询需要包含分片键,才能将请求路由到正确的分片。某些数据库约束(比如外键),只有在外键是分片键时才起作用。此外,分片键还需要将数据均匀地分布在所有分片上,以避免引起可靠性问题或影响可伸缩性的热点。
我们考虑过每个表使用相同的分片键,但我们现有的数据模型中没有好的备选。如果要添加一个统一的分片键,我们就不得不创建一个复合键,然后将这一列添加到每个表中,再运行昂贵的回填以填充值,最后还需要大量重构产品逻辑。于是,我们根据 Figma 独特的数据模型量身定制了我们的方法,并选择了一些分片键,如UserID、FileID或OrgID。几乎 Figma 的每个表都可以使用其中一个键进行分片。
我们引入了Colocation的概念,为产品开发人员提供了友好的抽象:同一个Colocation中的表,只需使用一个分片键,就可以支持跨表连接和完整的事务。大多数应用程序代码都是以这种方式与数据库交互,这最大程度地减少了应用程序开发人员为使用水平分片所需承担的工作量。
选择完分片键后,我们需要确保数据在所有后端数据库中均匀分布。不幸的是,我们选择的许多分片键使用了自动递增或带有Snowflake时间戳前缀的ID。结果导致大部分数据都集中到了一个分片上。我们曾考虑过迁移到更随机的ID,但这需要进行昂贵且耗时的数据迁移。最后,我们决定使用分片键的哈希值进行路由。只要选择足够随机的哈希函数,就可以确保数据的均匀分布。但有一个缺点是,对分片键的范围扫描效率较低,因为连续的键将被散列到不同的数据库分片上。然而,这种查询模式在我们的代码库中并不常见,因此我们愿意接受这点不足。
逻辑解决方案
为了降低水平分片的推出风险,我们希望将准备应用层的表的过程与运行分片拆分的物理过程隔离开来。为此,我们分离了“逻辑分片”与“物理分片”。然后,解耦迁移的两个部分,以便独立地实现并降低风险。逻辑分片可以降低风险,并按照百分比推出,因此我们对服务栈更有信心。发现错误时,只需修改配置就可以回滚逻辑分片。虽然物理分片操作也可以回滚,但需要更复杂的协调来确保数据一致性。
表上建立逻辑分片后,所有读写操作都相当于水平分片。从可靠性、延迟和一致性的角度来看,虽然数据仍居于同一个数据库主机,但看起来也是水平分片。等到我们确信逻辑分片正常工作后,再执行物理分片操作。物理分片操作主要是将数据从一个数据库复制到多个后端,然后通过新数据库重新路由读写流量。
查询引擎
为了支持水平分片,我们必须大幅修改后端技术栈的架构。最初,我们的应用服务直接与连接池层PGBouncer通信。然而,水平分片需要更复杂的查询解析、计划和执行。为了支持这些,我们创建了一个新的golang服务:DBProxy,其位于应用层和PGBouncer之间,包括负载均衡、更好的可观察性、事务支持、数据库拓扑管理以及轻量级查询引擎的逻辑。
查询引擎是DBProxy的核心,主要组件包括:
查询解析器:负责读取应用程序发送的SQL,并将其转换为抽象语法树(AST)。
逻辑计划器:负责解析AST,并从查询计划中提取查询类型(插入、更新等)和逻辑分片ID。
物理计划器:负责将查询从逻辑分片ID映射到物理数据库,重写查询,以便在合适物理分片上执行查询。
在水平分片的世界中,有些查询相对容易实现。例如,单分片查询只有一个分片键,查询引擎只需提取该分片键,并将查询路由到合适的物理数据库。我们可以将执行查询的复杂性“推给”Postgres。然而,如果查询缺少分片键,查询引擎就必须执行更复杂的“散列与聚集”操作。在这种情况下,我们需要将查询散列到所有分片(散列阶段),然后聚集返回结果(聚集阶段)。在某些情况下,比如复杂的聚合、连接和嵌套SQL,这种“散列与聚集”的操作可能会非常复杂。此外,“散列与聚集”过多会影响水平分片的可扩展性。因为这些查询必须连接到每一个数据库,每个“散列与聚集”都会加重水平分片的负载,数据库就如同没有分片一样。
如果我们支持完整的SQL兼容性,DBProxy服务就与Postgres数据库查询引擎别无二样了。我们希望简化API,以尽量降低DBProxy的复杂性,同时减少应用程序开发人员所需承担的工作量,因为他们可能需要重写所有不受支持的查询。为了确定应该支持哪些,我们建立了一个“影子计划”框架,用户可以通过这个框架定义需要分片的表的结构,然后在实际生产流量的基础上对真正的逻辑计划阶段实行影子计划。我们将查询和相关的查询计划记录到一个Snowflake数据库中,然后进行离线分析。我们根据这些数据,选择了一种支持最常见90%的查询的查询语言,同时还避免了查询引擎遇到最坏情况时的复杂性。例如,允许范围扫描和点查询,但只有当两个表位于同一个Colocation并且通过分片键连接,才允许使用连接。
未来的展望
接下来,我们还需要弄清楚如何封装逻辑分片。我们探索了使用单独的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必须掌握表和物理数据库的拓扑结构。由于我们分离了逻辑分片与物理分片的概念,因此我们需要一种方法在拓扑结构中表示这些抽象。例如,我们需要能够将一个表(users)映射到其分片键(user_id)。同样,我们需要能够将逻辑分片ID(123)映射到正确的逻辑和物理数据库。在垂直分区中,我们依赖于一个简单的硬编码配置文件,将表映射到分区。然而,随着我们朝着水平分片的方向迈进,我们需要更复杂的方式。我们的拓扑结构在分片拆分期间会动态变化,DBProxy需要快速更新其状态,避免将请求路由到错误的数据库。因为拓扑的每次变更都是向后兼容的,所以这些变更来不会成为网站的关键路径。我们构建了一个数据库拓扑结构,封装了复杂的水平分片元数据,并可以在一秒钟内实时更新。
拥有独立的逻辑和物理拓扑结构可以简化一些数据库管理工作。例如,非生产环境也可以保持与生产环境相同的逻辑拓扑结构,但提供数据的物理数据库更少。这样可以节省成本,降低复杂性,环境之间也不需要太多变化。此外,有了拓扑结构库,我们就可以在整个拓扑结构中强制执行不变性(例如,每个分片ID都应该映射到一个确切的物理数据库),这对于在构建水平分片时维护系统的正确性至关重要。
物理分片操作
在表准备好进行分片操作后,最后一步是做好从未分片的数据库到分片数据库的物理故障转移。大部分水平分片都可以使用相同的逻辑,但还是有一些值得注意的区别:我们不是将数据从一个数据库移动到另一个数据库,而是从一个数据库移动到多个数据库。我们需要确保故障转移能够处理新的故障模式,因为分片操作可能只在一部分数据库上成功。尽管如此,许多危险系数较高的组件已经历过垂直分区,因此风险已经降低了。我们能够更快地朝着第一次物理分片操作迈进。
总结
开始这段旅程之际,我们心中明白水平分片是一项需要付出多年努力的可扩展性投资。我们于2023年9月发布了第一个水平分片表。我们成功地进行了故障转移,数据库主节点的部分可用性受影响只持续了十秒,而副本完全没有受影响。分片后,我们没有看到延迟或可用性方面的任何退步。从那以后,我们一直在努力针对写入率最高的数据库进行相对简单的分片操作。今年,我们的分片将逐步推进到越来越复杂的数据库,这些数据库拥有成百上千乃至成千上万个代码调用点。
为了消除最后一个扩展限制,并展翅高飞,我们将需要对 Figma 的每个表进行水平分片。完全水平分片将带来许多其他好处:提高可靠性、节省成本以及提升开发速度。在这个过程中,我们还需要解决下列问题:
支持水平分片表结构的更新。
为水平分片主键生成全局唯一ID。
为业务关键用例实现原子跨分片事务。
分布式全局唯一索引(目前唯一索引仅支持包含分片键的索引)。
提升开发速度,与水平分片无缝兼容的 ORM 模型。
全自动化分片操作:只需点击一个按钮即可运行分片操作。
如果时间充裕,我们还将重新评估最初的 RDS 水平分片方法。18个月前我们开始了这个旅程,时间非常紧迫。NewSQL 存储系统在不断发展和和成熟。我们终于有了时间去重新评估继续沿着当前道路前进,还是改为使用开源或托管解决方案。
回顾这段旅程,我们取得了很大的进步,但我们的挑战才刚刚开始。
推荐阅读:
▶微软前工程师:Windows 11 现在还在用我 30 年前开发的“临时” UI!
4 月 25 ~ 26 日,由 CSDN 和高端 IT 咨询和教育平台 Boolan 联合主办的「全球机器学习技术大会」将在上海环球港凯悦酒店举行,特邀近 50 位技术领袖和行业应用专家,与 1000+ 来自电商、金融、汽车、智能制造、通信、工业互联网、医疗、教育等众多行业的精英参会听众,共同探讨人工智能领域的前沿发展和行业最佳实践。欢迎所有开发者朋友访问官网 http://ml-summit.org、点击「阅读原文」或扫码进一步了解详情。
更多推荐
所有评论(0)