figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; margin-top: 0px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在2020年,由于新功能的结合,菲格玛的基础设施遭遇了一些增长的痛苦,准备推出第二个产品,以及更多的用户(数据库流量每年大约增加3倍)。我们知道,在早期支持菲格玛的基础设施无法满足我们的需求。我们还在使用一个大型亚马逊 RDS 数据库可以保持我们的大多数元数据--比如权限、文件信息和注释--虽然它无缝地处理了我们的许多核心协作特性,但一台机器有其局限性。最明显的是,由于一个数据库服务的查询量,我们观察到在高峰流量期间,CPU利用率上升了65%。随着使用边缘越来越接近极限,数据库延迟变得越来越不可预测,影响核心用户体验。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">如果我们的数据库完全饱和,菲格玛就会停止工作。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">我们还远远没有做到这一点,但是作为一个基础设施团队,我们的目标是在可伸缩性问题接近迫在眉睫的威胁之前,积极主动地确定和解决这些问题。我们需要设计一种解决办法,以减少潜在的不稳定,为今后的规模铺平道路。此外,当我们实现这个解决方案时,性能和可靠性将继续占据首要位置;我们的团队的目标是建立一个可持续的平台,使工程师能够在不影响用户体验的情况下快速地迭代菲格玛的产品。如果菲格玛的基础设施是一系列道路的话,我们就不能在路上工作时就关闭公路。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">我们首先进行了一些战术性的调整,以确保跑道延长一年,同时我们为更全面的方法奠定了基础:
A 读取副本 是在正常情况下几乎实时地反映主实例更改的复制件。
大惊小怪 是一个轻量级连接池。
更新我们的数据库到最大的可用实例( R5.12x至5.24x )以最大限度地利用中央处理器
创造多个 阅读副本 按比例计算流量
为新的使用案例建立新的数据库,以限制原始数据库的增长
加起来 大惊小怪 作为一个连接池来限制越来越多的连接的影响(以千计)
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">当这些修复移动针头时,它们有局限性。通过分析我们的数据库流量,我们了解到编写--比如收集、更新或删除数据--对数据库利用率的很大一部分作出了贡献。此外,由于应用程序对复制延迟的敏感性,并非所有的读取或数据获取都可以移动到副本。因此,从读写的角度来看,我们仍然需要从原始数据库中卸载更多的工作。现在是摆脱渐进式变化,寻找长期解决办法的时候了。
figma-y6citn" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(5vw, 72px); font-variation-settings: "wght" 750, "wdth" 70; font-feature-settings: "ss01"; line-height: 1.04; letter-spacing: 0.005em; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">探索我们的选择
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">我们首先探索了横向扩展数据库的选项。许多流行的管理解决方案与 波斯特格雷斯 我们在菲格玛使用的数据库管理系统。如果我们决定建立一个水平可伸缩的数据库,那么我们将不得不找到一个与后兼容的托管解决方案,也不得不自己主持。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">迁移到 Nosql数据库 或 活力 (mysql)将需要复杂的双读写迁移,尤其是Nosql还需要在应用方面进行重大更改。适合事后的 新闻ql,我们将拥有一个最大的单集群足迹云管理分布式邮政。我们不想承担作为第一个遇到某些规模化问题的客户的负担;我们对托管解决方案几乎没有控制权,因此,如果在我们的规模上不进行压力测试,那么依靠它们将使我们面临更大的风险。如果不是托管解决方案,我们的另一个选择就是自存。但是,由于我们迄今为止一直依赖托管解决方案,我们的团队需要大量的前期工作来获得培训、知识和技能,以支持自我主持。这将意味着巨大的操作成本,这将使我们不再关注可伸缩性--一个更现实的问题。
垂直分区 是指数据库通过将表和/或列移动到一个单独的数据库中来分离。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在决定了两个方向的水平分叉后,我们不得不转向。不是水平碎片,我们决定 垂直分区 按表分列的数据库。我们不把每个表在许多数据库中分开,而是移动 表格组 在他们自己的数据库里。这被证明具有短期和长期的好处:现在垂直分区解除了我们原来的数据库,同时为将来水平共享表的子集提供了一条前进的道路。
figma-81kuz0" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(3.8889vw, 56px); font-variation-settings: "wght" 750, "wdth" 74; font-feature-settings: "ss01"; letter-spacing: 0.01em; line-height: 1.12; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">我们划分的方法
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">然而,在开始这个过程之前,我们首先必须识别要将表划分到它们自己的数据库中。有两个重要因素:
影响:移动表格应移动工作量的很大一部分
隔离:表格不应与其他表格紧密连接
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">为了衡量影响,我们查看了查询的平均活动会话(AAS),该会话描述了在某个时间点上专用于给定查询的活动线程的平均数量。我们通过查询计算这个信息figma-1j7zr0j" style="box-sizing: inherit; font-family: apercu-mono-vf; font-size: 16px; cursor: var(--cursorPointer); letter-spacing: -0.05em; line-height: 1.64; border: 1px solid var(--code-border-color); background: var(--code-bg-color); padding: 2px 4px; word-spacing: -4px;">pg_stat_activity
在10毫秒的间隔中识别与查询相关的CPU等待,然后按表名将信息聚合。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">每个表的"隔离"程度被证明是划分是否容易的核心。当我们将表迁移到不同的数据库时,我们将失去重要的功能,比如 原子交易 表、外键验证和连接之间。因此,移动表对于开发人员必须改写多少菲格玛应用程序会有很高的成本。我们必须注重于识别容易划分的查询模式和表,从而具有战略性。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">这在我们的后端技术堆栈中证明是很困难的。我们用的 红宝石 对于应用程序后端,它为我们的大多数Web请求提供服务。这些反过来又生成了大部分数据库查询。我们的开发人员 活动记录 写这些查询。由于Ruby和活动记录的动态性质,很难单独通过静态代码分析来确定哪些物理表受到活动记录查询的影响。作为第一步,我们创建了连接到活动记录中的运行时验证器。这些验证器将生产查询和事务信息(例如所涉调用方位置和表)发送到 雪花,我们的数据仓库在云端。我们使用此信息来查找一致引用同一组表的查询和事务。如果这些工作负载被证明是昂贵的,那么这些表将被确定为垂直分区的主要候选。
figma-y6citn" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(5vw, 72px); font-variation-settings: "wght" 750, "wdth" 70; font-feature-settings: "ss01"; line-height: 1.04; letter-spacing: 0.005em; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">管理移徙
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">一旦我们确定了要划分的表,我们就不得不提出一个在数据库之间迁移表的计划。虽然脱机操作很简单,但是脱机操作并不是菲格玛的一个选择--菲格玛需要时刻站起来,发挥作用,以支持用户的实时协作。我们需要协调数以千计的应用程序后端实例的数据移动,以便它们能够在正确的时刻将查询路由到新数据库。这将使我们能够在不使用维护窗口或停机时间的情况下对数据库进行分区,这将对我们的用户造成干扰(也需要工程师的下班工作)!)。我们希望找到一个实现以下目标的解决办法:
将潜在可用性影响限制在1分钟
程序自动化,易于重复
有能力撤销最近的分区
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">我们找不到一个符合我们要求的预先构建的解决方案,我们也希望有灵活性来适应未来的用例。只有一个选择:建立我们自己的。
figma-81kuz0" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(3.8889vw, 56px); font-variation-settings: "wght" 750, "wdth" 74; font-feature-settings: "ss01"; letter-spacing: 0.01em; line-height: 1.12; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">我们定制的解决方案
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在高级别,我们实施了以下操作(步骤3-6在几秒钟内完成,故障时间最少):
准备客户端应用程序从多个数据库分区查询
将表从原始数据库复制到新数据库,直到复制延迟接近0
原始数据库暂停活动
等待数据库同步
重新查询到新数据库的流量
简历活动
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">正确准备客户机应用程序是一个重要的问题,我们的应用程序后端的复杂性使我们感到焦虑。如果我们错过了一个分区后破裂的边缘案例呢?为了降低操作的风险,我们利用PGBBUER层来获得运行时的可见性和对我们的应用程序配置正确的信心。在与产品团队合作,使应用程序与分区数据库兼容之后,我们创建了单独的PGBBERER服务,以实现实际上的流量分割。 保安组确保只有PGBAN程序可以直接访问数据库,这意味着客户端应用程序总是通过PGBAN程序连接。先划分Pg安保er层将给客户端错误路由查询的回旋余地。我们将能够检测到路由错配,但是由于两个PGBBIN具有相同的目标数据库,客户机仍然可以成功地查询数据。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">一旦我们验证了应用程序是为每个PGBBERR(并适当地发送流量)准备了单独的连接,我们就会继续进行。
figma-81kuz0" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(3.8889vw, 56px); font-variation-settings: "wght" 750, "wdth" 74; font-feature-settings: "ss01"; letter-spacing: 0.01em; line-height: 1.12; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">合理的选择
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在波斯特格雷斯,有两种复制数据的方法: 流复制 或 逻辑复制 .我们选择逻辑复制是因为它允许我们:
在子集表格上的端口 因此,我们可以从目标数据库中更小的存储足迹开始(减少存储硬件足迹可以提高可靠性)。
复制到数据库 这意味着我们可以使用这个工具进行最小的停机时间的主要版本升级。aws为主要版本的升级提供了蓝色/绿色的部署,但是这个功能还没有提供给rds邮政。
设置反向复制 使我们可以撤回行动。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">使用逻辑复制的主要问题是 千兆字节 因此,最初的数据副本可能需要几天,如果不是几周的话,才能完成。我们希望避免这种情况,这不仅是为了最小化复制失败的窗口,而且也是为了重新启动的成本。我们仔细考虑了 快速恢复 并且在正确的点开始复制,但是还原消除了存储足迹较小的可能性。我们决定调查 为什么 逻辑复制的性能非常缓慢。我们发现,缓慢的拷贝是由于后Gres如何维护目标数据库中的索引。逻辑复制大量复制行,但更新效率低 一次一行的索引 .通过删除目标数据库中的索引并在数据的初始复制之后重建索引,我们将复制时间减少到了小时数。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">通过逻辑复制,我们能够从新分区的数据库建立一个反向复制流,然后返回到原来的数据库。此复制流被激活 紧接着 原来的数据库停止接收流量(更多关于下面)。对新数据库的修改将被复制回旧数据库,如果我们回滚,旧数据库将有这些更新。
figma-81kuz0" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(3.8889vw, 56px); font-variation-settings: "wght" 750, "wdth" 74; font-feature-settings: "ss01"; letter-spacing: 0.01em; line-height: 1.12; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">关键步骤
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">随着复制解决,我们发现自己处于协调查询重新路由的关键阶段。每天,成千上万的客户服务在任何给定的时间查询数据库。跨许多客户端节点的协调很容易失败。通过分两个阶段执行分片操作(然后是数据分块),分块数据的关键操作只需要在为分块表服务的几个分块节点之间进行协调。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">下面是对正在进行的操作的概述:为了使逻辑复制能够同步新的数据库,我们在节点之间进行协调,只是简单地停止所有相关的数据库流量。(PGBBER支持暂停新的连接和重新路由。)当PGBBERR暂停新的连接时,我们撤销了客户端在原始数据库中的分区表上的查询权限。在短暂的宽限期之后,我们取消了所有剩余的飞行查询。由于我们的应用程序大多会发出短期查询,所以我们通常会取消不到10个查询。此时,由于流量暂停,我们需要验证我们的数据库是否相同。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在重新路由客户机之前确保两个数据库相同是防止数据丢失的基本要求。我们曾经 Lsns 以确定两个数据库是否同步。如果一旦我们确信没有新的写入,我们就从原始数据库中对一个ln进行采样,那么我们就可以等待副本重放过这个ln。此时,数据在原件和副本中是相同的。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">在我们检查了复制是同步的之后,我们停止复制,并将复制推广到一个新的数据库。如前所述,设置了反向复制。然后,我们恢复了PGBBERR的流量,但是现在查询被路由到新的数据库。
figma-y6citn" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-family: hex-franklin-variable; font-size: min(5vw, 72px); font-variation-settings: "wght" 750, "wdth" 70; font-feature-settings: "ss01"; line-height: 1.04; letter-spacing: 0.005em; color: rgb(19, 19, 19); text-wrap: wrap; background-color: rgb(255, 255, 255);">规划我们的横向未来
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">此后,我们在生产中多次成功地执行了分区操作,每次,我们都达到了我们最初的目标:在不影响可靠性的情况下解决可伸缩性问题。我们的第一次行动涉及移动两个高流量的桌子,而我们在2022年10月的最后一次行动涉及50个。在每次操作中,我们观察到一个~30秒的部分可用性影响(约2%的请求下降)。如今,每个数据库分区的运行空间大大增加。我们最大的分区有CPU利用率在10%左右,我们减少了分配给一些较低流量分区的资源。
figma-1w80yjd" style="box-sizing: inherit; cursor: var(--cursorText); color: rgb(19, 19, 19); margin-block: 24px; font-family: hex-franklin-variable; font-size: 18px; text-wrap: wrap; background-color: rgb(255, 255, 255);">但是,我们的工作还没有完成。现在有了很多数据库,客户机应用程序就必须维护每个数据库的知识,并且随着我们添加了更多的数据库和客户机,路由复杂度将会增加。此后,我们引入了一个新的查询路由服务,随着我们扩展到更多的分区,它将集中和简化路由逻辑。我们的一些表有很高的写入流量或数十亿行和千兆字节的磁盘足迹,这些表将单独地击中磁盘利用率、CPU和I/O瓶颈。我们一直都知道如果我们 只依靠垂直分区,我们最终会达到扩展极限。回到我们最大化杠杆的目标,我们为垂直分区创建的工具将使我们能够更好地配置水平碎片表和高写流量。它为我们提供了足够的跑道,以解决我们当前的项目,并保持菲格玛的"高速公路"的开放,同时也看到转弯处。