翻译自:https://www.2ndquadrant.com/en/blog/autovacuum-tuning-basics/
autovacuum调优的基本知识
几周前,我介绍了调优检查点的基础知识,在那篇文章中,我还提到性能问题的第二个常见来源是autovacuum(基于邮件列表和我们支持的客户)。因此,让我通过这篇关于autovaccum
调优基础的文章来跟进。我将非常简要地解释必要的理论(死元组,膨胀以及autovacuum
是如何处理这些问题的),但这篇博文的主要重点是调优 - 有哪些配置选项,经验法则等等。
死元组
首先,让我们简单解释一下什么是“死元组”和“膨胀”。(如果你想要更详细的解释,也许可以阅读Joe Nelson的帖子,其中更详细地讨论了这个问题。
当你在PostgreSQL中执行delete
时,该行(又名元组)不会立即从数据文件中删除。相反,它仅通过在header中设置xmax
字段来标记为已删除。对于UPDATE
s也是如此,在PostgreSQL中可以看作是DELETE + INSERT
。
这是PostgreSQL MVCC背后的基本思想之一,因为它允许更大的并发性,而不同进程之间的锁定最小。当然,此MVCC实现的缺点是它会留下已删除的元组,即使在可能看到这些版本的所有事务完成之后也是如此。
如果不清理,这些“死元组”(实际上对任何事务都不可见)将永远保留在数据文件中,浪费磁盘空间,对于有大量的DELETE
和UPDATE
的表,死元组可能很容易占据绝大多数磁盘空间。当然,索引也会引用这些死元组,从而进一步增加浪费的磁盘空间量。这就是我们在PostgreSQL中说的“bloat”。当然,查询时扫描的数据越多(即使 99% 的数据被立即丢弃成为“死元组”),查询速度就越慢。
vacuum和autovacuum
回收死元组占用的空间(并使其可用于新行)的最直接方法是手动执行VACUUM
命令。此维护命令将扫描表并从表和索引中删除死元组 - 它通常不会将磁盘空间返回到操作系统,但会使其可用于新行。
注意:VACUUM FULL
会回收空间并将其返回到操作系统,但有许多缺点。首先,它获取表上的独占锁,阻止所有操作(包括 SELECT
s)。其次,它实质上是创建表的副本,使所需的磁盘空间加倍,因此当磁盘空间已经用完时,它不是很实用。
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
,除非你真的知道自己在做什么。认真地。 - 在繁忙的数据库(执行大量
UPDATE
和DELETE
)上,尤其是大型数据库,你可能应该降低scale factor,以便更频繁地进行清理。 - 在合理的硬件(良好的存储、多核)上,你可能应该增加限流参数,以便清理可以跟上。
- 在大多数情况下,仅增加`autovacuum_max_workers 并不能真正有所帮助。你会得到更多更慢的进程。
- 你可以使用
ALTER TABLE
为每个表设置参数,但如果确实需要,请三思而后行。它使系统更复杂,更难检查。
我最初囊括了几个部分来解释autovacuum
不起作用的情况,以及如何检测它们(以及最佳解决方案是什么),但博客文章已经太长了,所以我将在几天内单独发布。