autovacuum调优的基本知识

翻译自:https://www.2ndquadrant.com/en/blog/autovacuum-tuning-basics/

autovacuum调优的基本知识

几周前,我介绍了调优检查点的基础知识,在那篇文章中,我还提到性能问题的第二个常见来源是autovacuum(基于邮件列表和我们支持的客户)。因此,让我通过这篇关于autovaccum调优基础的文章来跟进。我将非常简要地解释必要的理论(死元组,膨胀以及autovacuum是如何处理这些问题的),但这篇博文的主要重点是调优 - 有哪些配置选项,经验法则等等。

死元组

首先,让我们简单解释一下什么是“死元组”和“膨胀”。(如果你想要更详细的解释,也许可以阅读Joe Nelson的帖子,其中更详细地讨论了这个问题。

当你在PostgreSQL中执行delete时,该行(又名元组)不会立即从数据文件中删除。相反,它仅通过在header中设置xmax字段来标记为已删除。对于UPDATEs也是如此,在PostgreSQL中可以看作是DELETE + INSERT

这是PostgreSQL MVCC背后的基本思想之一,因为它允许更大的并发性,而不同进程之间的锁定最小。当然,此MVCC实现的缺点是它会留下已删除的元组,即使在可能看到这些版本的所有事务完成之后也是如此。

如果不清理,这些“死元组”(实际上对任何事务都不可见)将永远保留在数据文件中,浪费磁盘空间,对于有大量的DELETEUPDATE的表,死元组可能很容易占据绝大多数磁盘空间。当然,索引也会引用这些死元组,从而进一步增加浪费的磁盘空间量。这就是我们在PostgreSQL中说的“bloat”。当然,查询时扫描的数据越多(即使 99% 的数据被立即丢弃成为“死元组”),查询速度就越慢。

vacuum和autovacuum

回收死元组占用的空间(并使其可用于新行)的最直接方法是手动执行VACUUM命令。此维护命令将扫描表并从表和索引中删除死元组 - 它通常不会将磁盘空间返回到操作系统,但会使其可用于新行。

注意VACUUM FULL会回收空间并将其返回到操作系统,但有许多缺点。首先,它获取表上的独占锁,阻止所有操作(包括 SELECTs)。其次,它实质上是创建表的副本,使所需的磁盘空间加倍,因此当磁盘空间已经用完时,它不是很实用。

VACUUM的问题在于它完全是手动操作——它只发生在你决定运行它时,而不是在需要时。你可以将其放入cron中并在所有表上每 5 分钟运行一次,但大多数运行实际上可能不会清理任何内容,而且有一个唯一的影响是系统上的 CPU 和 I/O 使用率更高。或者,你可能每天只在晚上运行一次,在这种情况下,你可能会积累更多你想要的死元组。

这是我们使用autovacuum的主要目的:根据需要进行清理,以控制浪费的空间大小。数据库确实知道随着时间的推移产生了多少死元组(每个事务报告它删除和更新的元组数),因此当表累积一定数量的死元组时可以触发清理(默认情况下,我们将看到是表的 20%)。因此,在繁忙时段执行的频率会更高,而在数据库大部分空闲时执行的频率会更低。

autoanalyze

清理死元组并不是autovacuum的唯一任务。它还负责更新优化程序在规划查询时使用的数据分布统计信息。你可以通过运行ANALYZE来手动收集它们,但它会遇到与VACUUM类似的问题——你可能会过于频繁或不够频繁地运行它。

解决方案也类似 - 数据库可以观察表中更改了多少行,并自动运行ANALYZE

注意:对于ANALYZE来说,负面影响要大一些,因为虽然VACUUM的成本与死元组的数量成正比(当死元组很少/没有时相当低),但ANALYZE必须在每次执行时从头开始重建统计信息。另一方面,如果你运行的频率不够高,那么选择糟糕的计划的成本可能同样严重。

为了简洁起见,我基本上将在本文的其余部分忽略此autovacuum任务——无论如何,配置与清理非常相似,并且遵循大致相同的原理。

监控

在进行任何类型的调优之前,你需要能够收集相关数据——否则你怎么能知道你需要进行任何调优,或评估配置更改的影响?

换句话说,你应该进行一些基本的监控,从数据库中收集指标。对于清理,你至少需要查看以下值:

  • pg_stat_all_tables.n_dead_tup – 每个表中的死元组数(包括用户表和系统目录)
  • (n_dead_tup / n_live_tup)– 每个表中死元组/活元组的比例
  • (pg_class.relpages / pg_class.reltuples) – “每行”的空间

如果你已经部署了监控系统(并且应该部署),那么你很可能已经在收集此类指标。总体目标是获得稳定的行为,不会突然/显着改变任何这些指标。

还有一个方便的pgstattuple扩展,允许你对表和索引进行分析,包括计算可用空间大小,死元组等。

调优的目标

在查看实际参数配置之前,让我们简要讨论一下高级调优目标是什么,即更改参数时我们想要实现的目标:

  • 清理死元组——保持合理的磁盘空间,不要浪费不合理的磁盘空间,防止索引膨胀并保持快速查询。
  • 尽量减少清理影响——不要过于频繁地执行清理,因为这会浪费资源(CPU、I/O 和 RAM),并可能严重损害性能。

也就是说,你需要找到适当的平衡——过于频繁地运行它可能与运行频率不够高一样糟糕。平衡很大程度上取决于你管理的数据量、你正在处理的工作负载类型(删除/更新的数量)。

postgresql.conf中的大多数默认值都非常保守,原因有两个。首先,默认值是几年前根据当时常见的资源(CPU,RAM等)确定的。其次,我们希望默认配置可以在任何地方工作,包括像Raspberry Pi这样的小型机器或小型VPS服务器。对于许多部署(特别是较小的部署和/或处理只读工作负载),默认配置参数可以正常工作。

随着数据库大小和/或写入量的增加,问题开始出现。典型的问题是清理发生的频率不够高,然后当它发生时,它会显着破坏性能,因为它必须处理大量垃圾。如果有这些情况,你应该遵循以下简单规则:
如果它很糟糕,说明你做得不够频繁。

也就是说,调整参数,以便更频繁地进行清理,并且每次处理较少数量的死元组。

注意:人们有时会遵循不同的规则——如果很糟糕,就不要这样做。——并完全禁用autovacuum。请不要这样做,除非你真的(真的真的)知道你在做什么,并且有定期的清理脚本。否则,你将把自己放在没有退路的位置,不是一点点的性能下降,你将不得不处理严重的性能下降甚至可能运行不动。
所以现在我们知道了我们想要通过调优实现什么,让我们看看配置参数…

Thresholds and Scale Factors

当然,你第一件可以调整的事是何时触发清理,这受两个参数的影响:

  • autovacuum_vacuum_threshold = 50
  • autovacuum_vacuum_scale_factor = 0.2

并且每当死元组的数量(你可以看到为 pg_stat_all_tables.n_dead_tup)超过下面阈值时
threshold + pg_class.reltuples * scale_factor
该表将被视为需要清理。该公式基本上是说,在清理之前,表的死元组到达 20% 时进行清理(50行这个阈值是为了防止非常频繁地清理小表)。

默认scale factor适用于中小型表,但不适用于非常大的表——在 10GB 表上,这大约是 2GB 的死元组,而在 1TB 表上是~200GB。

这是一个积累大量死元组并一次处理所有元组的示例,这将很痛苦。根据前面提到的规则,解决方案是通过显着降低scale factor来更频繁地执行此操作,甚至如下所示:
autovacuum_vacuum_scale_factor = 0.01
这会将限制减少到仅表的 1%。另一种解决方案是完全放弃scale factor,仅使用threshold

autovacuum_vacuum_scale_factor = 0
autovacuum_vacuum_threshold = 10000

这应该在生成10000个死元组后触发清理。

有个问题是postgresql.conf中的这些更改会影响所有表(实际上是整个集群),并且可能会不希望影响小表的清理,例如系统目录。

当更频繁地清理小表时,最简单的解决方案是完全忽略死元组问题。清理小表将相当廉价,即使你忽略小表的低效率,大表的改进通常也非常显着,而整体效果仍然非常显著。

但是,如果你决定更改配置,从而显著延迟对小表的清理(例如,设置 scale_factor=0 和 threshold=10000),则最好使用ALTER TABLE仅将这些更改应用于特定表:
ALTER TABLE t SET (autovacuum_vacuum_scale_factor = 0);
ALTER TABLE t SET (autovacuum_vacuum_threshold = 10000);

尽量保持配置简单,并修改尽可能少的表的参数。将其包含在内部文档中是一个好主意,包括设定该值的理由。

节流

autovacuum内置的一个很好的功能是节流。清理任务旨在成为在后台运行的维护任务,对用户查询等的影响最小。换句话说,它不应该消耗太多资源(CPU 和磁盘 I/O),这正是autvacuum内置限流的目的。

清理过程相当简单——它从数据文件中读取页面(8kB数据块),并检查是否需要清理。如果没有死元组,则页面将被简单地丢弃而不进行任何更改。否则,它将被清理(死元组被删除),被标记为“脏”并最终写出。成本核算基于定义的三个基本操作的成本:

vacuum_cost_page_hit = 1
vacuum_cost_page_miss = 10
vacuum_cost_page_dirty = 20

也就是说,如果从shared_buffers读取页面,则计为1。如果在shared_buffers中找不到它并且需要从操作系统中读取,则计为10(它可能仍从RAM提供,但我们并不知道)。最后,如果页面因清理而变脏,则计为 20。这使我们能够计算autovacuum完成的“工作成本”。

然后通过限制一次可以完成的工作量来实现节流,默认情况下设置为 200,每次清理完这么多工作时,它将休眠 20 毫秒:

autovacuum_vacuum_cost_delay = 20ms
autovacuum_vacuum_cost_limit = 200

那么,这实际上允许多少工作?延迟 20 毫秒时,清理每秒可以执行50轮,每轮200个读/写页面意味着每秒10000个读/写页面。这代表着:
shared_buffers读取80 MB/s (假设它不是dirty的)

  • 从操作系统读取8 MB/s(可能从磁盘读取)
  • 写入4 MB/s (autovacuum变成dirty的页面)
  • 考虑到当前硬件的承载能力,并且读/写大多是顺序的,这些限制太低了。

我们通常做的是增加cost_limit参数,例如增加到1000(或 2000),这会将吞吐量提高 5 倍(或 10 倍)。你当然可以调整其他参数(每页操作的成本、睡眠延迟),但我们很少这样做——更改ost_limit效果很好。

worker进程的数量

一个尚未提及的配置选项是autovacuum_max_workers,那么这是怎么回事呢?好吧,清理不会在单个autovacuum过程中发生,但允许数据库启动多达autovacuum_max_workers个实际不同数据库/表的清理进程。

这很有用,因为例如,你不希望在完成对单个大表的清理之前停止清理小表(由于限流,这可能需要相当长的时间)。

问题是用户认为工作人员的数量与可能发生的清理量成正比。如果将autovacuum的数量增加到6个,与默认的3个worker进程相比,它肯定会完成两倍的工作,对吧?

好吧,没有。前几段描述的成本限流是全局的,由所有autovacuum workers共享。每个工作进程只能获得总成本限制的1/autovacuum_max_workers,因此增加工作进程的数量只会使它们变慢。

这有点像高速公路——将汽车数量增加一倍,但让它们的速度减半,只会让你每小时到达目的地的人数大致相同。

因此,如果数据库的清理无法跟上用户活动,则增加worker数量不是一个解决方案,除非你调整其他参数。

每个表的限流

实际上,当我说成本限制是全局的并且由所有autovacuum worker共享时,我一直在撒谎(有点)。与scale factor和threshold类似,可以设置每个表的成本限制和delay:

ALTER TABLE t SET (autovacuum_vacuum_cost_limit = 1000);
ALTER TABLE t SET (autovacuum_vacuum_cost_delay = 10);

然后,处理此类表的worker不包括在全局成本核算中,并且会独立进行限制。

这给了你相当多的灵活性和力量,但不要忘记 - 权力越大,责任越大!

实际上,出于两个基本原因,我们几乎从不使用此功能。首先,你通常希望对后台清理使用单个全局限制。其次,如果有多个工作线程有时被限制在一起,有时是独立的,这使得监视和分析系统的行为变得更加困难。

总结

这就是你调整autovacuum的方式。如果我必须将其总结为一些基本规则,那就是这五条:

  • 不要禁用autovacuum,除非你真的知道自己在做什么。认真地。
  • 在繁忙的数据库(执行大量UPDATEDELETE)上,尤其是大型数据库,你可能应该降低scale factor,以便更频繁地进行清理。
  • 在合理的硬件(良好的存储、多核)上,你可能应该增加限流参数,以便清理可以跟上。
  • 在大多数情况下,仅增加`autovacuum_max_workers 并不能真正有所帮助。你会得到更多更慢的进程。
  • 你可以使用ALTER TABLE为每个表设置参数,但如果确实需要,请三思而后行。它使系统更复杂,更难检查。

我最初囊括了几个部分来解释autovacuum不起作用的情况,以及如何检测它们(以及最佳解决方案是什么),但博客文章已经太长了,所以我将在几天内单独发布。

猜你喜欢

转载自blog.csdn.net/qq_40687433/article/details/130774225