从5年的缩放PostgreSQL中汲取的经验教训

2021-04-16 04:15:53

对于近十年来,开源关系数据库PostgreSQL一直是Inaleignal的核心部分。多年来,我们在近40台服务器上缩放了高达75岁的存储数据(TB)。我们的实时细分功能从PostgreSQL的表现中受益匪浅,但我们'在我们的繁重写入负载和PostgreSQL升级路径的局限性引起的膨胀时,我们也陷入了困境。

在本文中,我会解释我们在PostgreSQL和解决方案上进行扩展时,我们解释了我们处理的一些挑战。我们很高兴在这个领域分享我们的经验教训,因为我们必须弄清楚这一点很多艰难的方式,我们希望分享我们的经验将使事情更容易缩放PostgreSQL的其他人。

在本文中有很多信息 - 您可以根据您的兴趣键入订单或跳转到不同的部分。我建议首先读取数据部分的高级概述,然后检查下面列出的其他部分:

最后一个注意事项:我们的目标是将我们的经验教训分享到高水平,而不是提供详细的方式指导。我们涵盖的主题的参考资料是适当的。

作为多通道消息平台,我们的主要数据集是订阅者。在推送通知方面,用户被识别为推送令牌,订阅状态和数据标签(自定义密钥:可以通过我们的SDK添加到设备的字符串或数字数据的值对),支持用户分段。

我们每月有超过100亿积极的订阅,并且数十亿订阅者,其订阅状况取消订阅。这些记录是非常频繁的 - 每次打开应用程序时,我们都会更新我们最后一次看到订阅者。在读取方面,我们支持交易发送(即,在特定参与里程碑处发送到特定用户),并向具有特定特征的大受众(即段)。当通知被发送到由各种参数定义的大段时,查询可以快速变得复杂,需要几分钟时间来执行,因为它们可能返回数百万次数百万的记录。

第二到订阅者,通知是我们的下一个最大数据集。记录大小从非常小的(例如,“发送给所有用户的”通知“,并且在包括特定订户ID列表时非常大。这些记录的大部分是在创建时写一次,然后在整个交付过程中添加或更新各种计数器和时间戳。此数据读取很少 - 几乎所有访问后创建都是目标更新或来自alyignal仪表板的查询,以概述最新通知。还有偶尔出口客户端应用程序的通知数据,但这些都构成了很少的访问部分。最后,我们运行批量删除此数据以强制执行保留策略。通知数据集与订阅者类似地分区和分离。

还有一些其他数据集消耗相对大量的空间(存储的整体数据的10%),但它们从动态透视不太有趣。

订阅者在插入和更新方面是写沉重的,并且具有频繁,长期运行的分析查询的额外挑战,以支持提供给段。

除了繁忙的更新负载和频繁批量删除来强制执行保留策略,通知通常还具有相当大的记录。

让我们谈谈臃肿。首先,它是什么?在广泛的意义上,软件膨胀是一个用于描述程序变慢的过程的术语,需要更多的硬件空间,或者使用每个连续版本使用更多的处理能力。 PostgreSQL中有几种不同类型的膨胀。

表展开是由该表中可能也可能不可重复使用的表中消耗的磁盘空间,并且不可通过其他表或索引重复使用。

想象一下,您创建一个表并插入10个记录,占据每条记录的一页磁盘空间的记录,而不结转。如果删除前九个记录,则占用的空间不会可用于重用!这些条目现在被认为是“死元素”,因为它们不可观察到任何交易。

现在,在此表上运行真空将允许该表中的空间重用以用于将来的插入或更新重复使用,但例如,如果您有一个可以使用一些额外空间的第二个大表,则不会使用这些页面。更新是PostgreSQL中的另一个膨胀源,因为更新是作为删除加上插入的更新。即使删除在数据集上不常见,也可以成为受害者的重新更新。

所以当真空没有足够好的解决方案时?这将取决于数据的形状和相应的访问模式。对于我们的一些数据集,我们开始无限期地持续或长时间持续,后来决定添加保留策略。如果这样的策略导致表中的存储数据的卷从300GB减少到10GB,则运行真空将允许表格中的所有空间重用。如果稳态存储约为10到15GB,大部分是空间的浪费空间。在这种情况下,使用真空不会帮助您。有关如何解决此问题的详细信息,请跳过关于PG_Repack的讨论。

在尝试了解如何生成索引遍地之前,让我们先查看PostgreSQL索引如何在非常高的级别工作。

PostgreSQL索引是直接指数 - 索引条目包含有关其关联元组在磁盘上的信息。根据每个更新有效地删除加号,这意味着每次更新列,都必须更新索引条目,而不管索引值是否已更改。

但等等,还有更多!由于PostgreSQL的MVCC方法,索引条目不能简单地删除或更新;还必须添加新的索引条目。这会导致我们遇到的挑战与表膨胀 - 死亡索引条目随时间累计,因为更新和删除了行。因为表可能有许多指标,所以每个写入都可以级联到许多指标写入中,这是一种称为写放大的现象。由表更新引起的索引中的浪费空间是索引截端。

在进一步之前,我想调用案例和优化未创建死区,例如堆积元组(热)优化,这允许将元组存储在其先前版本和索引附近并不总是需要更新。但是,Hot伴随着一些性能权衡,影响索引扫描的阅读性能。

回到我们的用例。我们说,我们的订阅者数据集是大量更新和重大读取。有21个指数,这意味着每个更新都会创建大约20个死区条目。其的最新结果是桌面的磁盘脚印及其指标。

对于通知,我们没有相当多的指数,但一旦进入交付阶段,记录会更新。再加上保留策略执行,这是许多膨胀的食谱!

“最好的罪行是一个良好的防守”在处理臃肿时戒指是真的。如果您可以避免首先创建它,您将不需要任何花哨的解决方案来摆脱它。

Autovacuum是一个功能,其中数据库将代表您自动产生真空流程。虽然是什么吸尘?从文档中:

真空回收尸体占用的储存。在正常的PostgreSQL操作中,通过更新删除或已删除的元组不会从其表格上删除;它们仍然存在,直到真空完成。因此,'必须定期做真空,特别是在常常更新的表格上。

从本说明书来看,我们可以推动,桌子越频繁地被吸尘,该关系的总存储越低。虽然吸尘不是免费,但数据库经常有许多需要注意的关系。重要的是,您调整Autovacuum经常运行,以便在可接受的水平下保持死亡空间。

调整Autovacuum是一个值得自己的文章的大型主题,谢天谢地,2ndquadrant的伟大人士已经写了一个详细的博客文章,涵盖了这个确切的话题。

第一个优化I' LL封面地址如何避免如何创建由数据保留策略引起的膨胀。使用PostgreSQL表分区,您可以将一个表称为多个表,仍然将单个表的外观显示到您的应用程序。在执行表分区时需要考虑一些性能考虑,因此在开始之前您的研究也会如此。

例如,您在数据表中有一个日期列,例如Created_At,您只需要保留最近30天的数据。为此,您可以创建多达30个分区,每个分区都有一个将保留的特定日期范围。执行保留策略时,使用一个简单的删除表来从数据库中删除单个分区表,而不是尝试从表中的目标删除。此策略可以防止膨胀于首先创建。 PG_Partman扩展甚至可以为您提供自动化此过程!

下一个优化有点细微。假设您有一个具有两个数据列的表,big_column和int_column。存储在BIG_COLUMN中的数据通常为每条记录大约一千字节,并且int_column经常更新。每次更新到INT_COLUMN都会导致BIG_COLUMUME复制。由于这些数据列已链接,因此更新将创建大量浪费空间,按1KB每更新(Modulo Disk Paging Memence)。

在这种情况下,您可以做的是将INT_COLUMN拆分为单独的表。在将其更新在该单独的表格中时,将不会生成重复的BIG_COLUMN。虽然拆分这些列意味着您' ll需要使用连接来访问两个表,这可能值得折衷,具体取决于您的用例。我们对订阅者和通知数据集使用此技巧。订阅者上的数据标签可以是多千字节,并且像Last_Seen_time这样的列经常更新。这显着降低了膨胀率。

虽然我们最好的尝试避免它,但有时会膨胀。值得庆幸的是,有一系列用于处理它的第一个和第三方。

FULL是真空命令的可能参数之一。根据PostgreSQL文档:

选择“完整”真空,可以回收更多空间,但需要更长,并且完全锁定表格。此方法还需要额外的磁盘空间,因为它写了表的新副本,并且不会释放旧副本,直到操作完成。

他们说,这个选项只应该在需要回收很多空间的极端情况下使用。实际上,这是核选项。它与所有索引一起重写整个表。结果表将没有浪费的空间,但它以桌面上的独占锁定的成本在进行中,这可以很容易地导致来自HTTP层的503响应,因为在等待时查询定时锁定被释放。除非你绝对知道你在做什么,否则不要这样做。

PG_Repack是一个专门用于打击膨胀问题的第三方工具,用于自动吸尘无法解决。有几种不同的pg_repack模式,包括:

PG_REPACK的表重新包装功能同时有效地真空。这意味着,而不是在表的整个持续时间内保持表格上的独占锁,只有在使用新创建的膨胀表格替换原始表时,只能在最终中使用独占锁。

索引重新包模式涉及同时创建新的相同索引到被重新包装的新索引,然后替换新索引一旦新建准备就绪。您可以同时认为它像reindex一样。

大多数情况下,如果不是全部,则使用统计数据等是从两个模式的原始关系中复制。但是,有一些缺点:

尽管锁定有限,但表重新包装仍然可以在具有长时间运行查询和经常交易访问的表中存在问题。如果您不小心,此工具仍可限制表可用性。

重新包装类似于吸尘,因为您可以在关系中指出并运行它。但是,与吸尘不同,没有重新包装守护程序,可以自动重新包装膨胀关系。这意味着默认情况下是手动过程。

作为表重新包的一部分,原始表的更新存储在日志表中并稍后应用。此过程的结尾是单线程,因此如果您的写入速度足够高,您最终可能会在重新包的情况下最终完成,因为日志填充比可以处理的速度更快。

我们在insignal中使用pg_repack来管理膨胀。我们建立了一个缺乏重新包装守护程序。该守护程序目前仅自动化索引重新包,但我们希望将其扩展到将来支持表格。索引重新包装通常是安全的,因为它们不会在索引正在重新包装的表格上施加重型锁定。守护程序通过扫描索引似乎被膨胀的索引,然后只需在列表中重新包装下一个信息,AD Infinitum。

我们不自动化表重新包装,因为我们的用户表是非常危险的,以便重新包装频率,因为长时间运行的分析查询。但是,我们很乐意为我们的通知表使用这种方法,这通常是安全重新包装的。为了安全地这样做,我们需要更改我们当前的表配置,以防止特定表正在重新包装。

我们希望有一个其他功能,但它被PG_Repack或PostgreSQL本身阻止了。我们发现并发索引重新包将彼此阻止并导致其中一个重新包失败。虽然这很容易从中恢复,但这意味着重新包装并发很限于一个。

我们创建的守护程序检测到表何时阻止流量(即,高等待查询)或数据库备份正在运行,并且在这些时间内将自动取消/空闲。我们希望将来能够开源这一守护进程,但我们觉得它通常准备好外部消费。

最后,我们来到pgCompacttable。该工具优异的是以完全非阻塞方式减少表膨胀。它通过将桌子的末端重新排序到表的前部,这允许表尺寸缩小。我们在订阅者表上使用它代替PG_Repack,因为后者通常会导致可用性问题。 PGCompacttable比PG_Repack慢,这就是为什么我们没有成为第一道防线。

PostgreSQL的主要升级用作改变磁盘数据格式的机会。换句话说,它无法简单地关闭版本12并打开版本13.升级要求以新格式重写数据。

有两种升级方法,可提供不同的服务可用性。第一个选项是pg_upgrade。此工具将数据库从旧格式重写为新的。它要求数据库在升级时脱机。如果您甚至具有适度大小的数据集和可用性要求,这一要求是一个很大的问题 - 这就是为什么我们' ve从未使用此方法升级我们的数据库的原因。

相反,我们使用逻辑复制来执行我们的主要版本升级。逻辑复制是通常用于热备杂的流复制的扩展。通过将原始磁盘块从上游服务器的更改从上游服务器进行写入副本,流式传输复制,这使得它不适合执行重大升级。可以使用逻辑复制的原因是解码和应用的更改,就好像发送到副本的SQL语句流(而不是简单地将页面写入磁盘)一样)。

切割或优雅地切换到热备用。为了实现优雅的开关,PGlogical Extension提供更多旋钮来调整应用复制流的应用以及如何处理冲突而不是内置逻辑复制功能。

但是,有一个主要的警告。目的地数据库上的解码过程是单线程的。如果数据库上的写入负载足够高,则会压倒解码过程并导致滞后增加,直到达到某些限制(通常,可用磁盘空间)。

如果您发现自己在逻辑复制不能“跟上”的情况下,您基本上有一个选项:一次将数据移动到另一个数据库一点(使用逻辑复制,因为它支持此类类型的细粒度复制)。复制目标可以是PostgreSQL的升级版本。这意味着您的应用程序必须能够为不同的表选择不同的数据库,并要求您在应用程序代码中处理切换。

为了开始逻辑复制,我建议首次审查官方PostgreSQL手册,并查看PGlogical扩展,该扩展程序在逻辑复制下提供更复杂的冲突解决方案。

微小升级几乎是主要升级部分后的脚注。只需更新PostgreSQL二进制文件并重新启动过程即可执行次要升级。大多数SLA都将提供大量缓冲区,以支持快速重启数据库进程。在您需要极端可用性的情况下,流式复制和切换可能允许零停机次要升级。

流程重启按照我们的数据集,服务器大小和加载量的数十秒,并且我们采取了这种简单的方法来保持我们的数据库在最新的次要版本上。

在我们的旅程中早期,另一个问题造成了一些服务损失:称为事务ID(也称为TxID或XID)环绕预防的故障模式。

PostgreSQL的MVCC实现依赖于32位事务ID。 xid用于跟踪行版本,并确定特定事务可以看到哪个行版本。如果您每秒处理成千上万的交易,则不需要很长时间才能接近XID最大值。如果XID计数器缠绕在周围,则过去的交易似乎将来是在未来的,这将导致数据损坏。

措辞“最大值”很简单,但概念有点细微。 xids可以被视为躺在圆圈或圆形缓冲区上。只要该缓冲区的结尾不会跳过前面,系统将正常运行。

为防止XIDS耗尽并避免环绕,真空过程也负责“默认情况下冻结”的“冻结”行版本(默认情况下旧的数百万个交易)。但是,存在失败模式,防止它冻结极端旧元组,最古老的联接元组限制了交易所见的过去ID的数量(仅有20亿过去ID可见)。如果剩余的xid计数达到一百万,则数据库将停止接受命令,必须以单用户模式重新启动以恢复。因此,监视剩余的xids是非常重要的,以便您的数据库从未进入这种状态。

如果此价值低于2.5亿,我们触发警报。影响真空冻结的最重要的Autovacuum参数之一是autovacuum_freeze_max_age。这是

......