暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

auto_explain 的开销有多大?

原创 xiongcc 2024-09-27
501

前言

最近在翻译 Postgres-howto 系列时看到的一篇不错的文章,翻译一下。简而言之,如果系统时钟开销很高,那么 auto_explain,包括 explain analyze 都有可能会欺骗你,最好使用 pg_test_timing 测量一下。

the measurement overhead added by EXPLAIN ANALYZE can be significant, especially on machines with slow gettimeofday() operating-system calls.

介绍

auto_explain 是一个非常酷的模块,可以自动记录最慢查询的执行计划。它通过跟踪每个查询 (或其中的一部分),仅记录那些超过所设定阈值的查询结果。

图片

这篇文章并不是关于 auto_explain 的介绍,但这可能是未来一个不错的主题。目前,如果你对它不熟悉,我建议阅读 PostgreSQL 文档中的 auto_explain 部分。

我们在此将探讨 auto_explain 的开销。很多地方都提到它的开销,但具体的信息难以获取。

通常提到的主要问题是 log_timing 参数,但我听说 log_buffers 也可能带来一定开销。听到的数字大约从 "约 3%" 到 "接近 2 倍" (这个范围相当大),但我没见过有明确的基准测试数据。

基于这些警告,我怀疑很多人不会在生产环境中使用 auto_explain,或者至少会关闭 log_timing 功能。但我也看到一些严谨的组织在生产环境中开启了 auto_explain 的 log_timing,这让我开始怀疑在某些条件下其开销是否会相对较低。

当然,即使没有每个操作的时间信息,记录执行计划也是有用的。不过,这在分析查询为何变慢时会有些限制。pgMustard 目前要求每个操作的时间数据,这也让我更有动机深入研究这个问题。

Ongres PostgreSQL 咨询公司的创始人 Álvaro Hernández 去年做了很好的工作,他发布了系统时钟缓慢对 EXPLAIN ANALYZE 开销的影响。在他的文章末尾,他还指出了不同操作的开销差异,举例说明了连接操作的高开销。

因此,我的假设是,在 1) 系统时钟开销较低的系统上,以及 2) 事务型负载下,auto_explain.log_timing 可能具有较低的开销。

研究不同系统时钟开销和工作负载是很有趣的,但这只能等到以后再做了。

设置

以下是我进行测试的设置:

  • PostgreSQL 13.2,安装于 macOS 10.15.7,使用 Postgres.app 2.4.2

  • 2 核心,4GB 内存,SSD

  • postgresql.conf 的修改

    • shared_buffers = 1GB
    • effective_cache_size = 3GB
    • maintenance_work_mem = 256MB
  • pgbench

    • 默认的 TPC-B 类似负载
    • 10 倍缩放 (~160MB) – 小于 shared_buffers
    • 4 个客户端 – 在此设置下,每秒的事务数大致最佳
    • 1800 秒 (30 分钟) 的测试时长
  • pg_test_timing

首先,我使用了我所熟悉的一个系统进行测试。

此处是 pg_test_timing 的结果:

michael$ pg_test_timing
Testing timing overhead for 3 seconds.
Per loop time including overhead: 90.61 ns
Histogram of timing durations:
  < us   % of total      count
     1     90.95840   30113986
     2      9.03888    2992542
     4      0.00080        264
     8      0.00010         33
    16      0.00168        555
    32      0.00013         42
    64      0.00002          5
   128      0.00000          1

根据 pg_test_timing 文档所说,"好的结果是大多数 (>90%) 的单次时间测量在 1 微秒以内",所以从这一点来看,我们没问题。

结果

基准测试

在新安装好 PostgreSQL 并修改了参数之后,是时候获得一个基线进行比较了。

michael$ pgbench -i -s 10
...

michael$ pgbench -c 4 -T 1800
...
latency average = 1.585 ms
tps = 2523.482861 (including connections establishing)
tps = 2523.488970 (excluding connections establishing)

这给出了一个平均延迟为 1.585ms 的基准值。

记录每个查询计划

auto_explain 默认是关闭的。它还有一个 log_min_duration 参数,默认为 -1 (不记录)。所以通常会将 log_min_duration 设置为一个查询长度的阈值 (以毫秒为单位),用于想要调查的查询。作为起点,我想测试记录每个查询的执行计划,因此我将最小持续时间设为 0。注意,我们还没有跟踪时间或与此相关的任何其他执行细节。sample_rate 参数默认为 1 (所以 100% 的查询会被"采样"),但我希望明确说明这一点。

所以我将以下内容添加到 postgresql.conf 文件中并重启:

shared_preload_libraries = 'auto_explain'
auto_explain.sample_rate = 1
auto_explain.log_min_duration = 0

注意:可以降低 sample_rate 参数以对随机查询样本运行 auto_explain (例如,0.1 表示 10%)。我预计这是一种非常有效的方式来降低开销,但代价是信息的不完整。

我用之前的设置运行了 pgbench:

michael$ pgbench -i -s 10
...
michael$ pgbench -c 4 -T 1800
...
latency average = 1.994 ms
tps = 2005.883159 (including connections establishing)
tps = 2005.887957 (excluding connections establishing)

结果显式,平均延迟为 1.994ms,比基准值慢了约 26%。这让我很惊讶,因为据我所知的数字包括了跟踪时间,而这次并没有!我相信这是由于此系统的日志记录开销 (默认是 stderr)。此测试生成了约 6GB 的日志,这是不记录所有内容的另一个好理由!

仅记录最慢的查询

我决定尝试一个更合理的阈值,这次仅记录查询执行时间超过 10 毫秒的输出。

所以新的设置是:

shared_preload_libraries = 'auto_explain'
auto_explain.sample_rate = 1
auto_explain.log_min_duration = 10

然后再次重启:

michael$ pgbench -i -s 10
...
michael$ pgbench -c 4 -T 1800
...
latency average = 1.597 ms
tps = 2504.327824 (including connections establishing)
tps = 2504.334064 (excluding connections establishing)

结果显式,平均延迟为 1.597ms,仅比基准慢 0.8%。对于这种工作负载,10ms 的阈值意味着我们避免了记录大约 99.9% 的查询计划,只记录了最慢的查询及其计划 (由于测试运行了半小时,仍然记录了几百个计划)。值得注意的是,每个查询的执行计划的细节仍然会被跟踪,因为它事先不知道查询是否会超过我们的阈值 (auto_explain source code)。

总的来说,在我看来,这对于 log_timing 测试来说是一个更现实的起点。

不带时间的 analyze 的开销

开启 log_analyze 将跟踪每个查询的 EXPLAIN ANALYZE 细节。默认情况下,这会跟踪每个操作的时间数据,因此我们先关闭 log_timing。

以下是我们的新设置:

shared_preload_libraries = 'auto_explain'
auto_explain.sample_rate = 1
auto_explain.log_min_duration = 10
auto_explain.log_analyze = true
auto_explain.log_timing = false

再次重启后:

michael$ pgbench -i -s 10
...
michael$ pgbench -c 4 -T 1800
...
latency average = 1.590 ms
tps = 2515.130430 (including connections establishing)
tps = 2515.136821 (excluding connections establishing)

结果显式,平均延迟为 1.590ms,仅比基准慢 0.3%。虽然比上一次测试略快,但我怀疑这小于误差范围,因为我们现在跟踪了更多信息 (它变快对我来说没什么道理)。

时间记录的开销

我们再试一次,不过这次开启时间记录:

shared_preload_libraries = 'auto_explain'
auto_explain.sample_rate = 1
auto_explain.log_min_duration = 10
auto_explain.log_analyze = true
auto_explain.log_timing = true

再次重启后:

michael$ pgbench -i -s 10
...
michael$ pgbench -c 4 -T 1800
...
latency average = 1.620 ms
tps = 2469.825217 (including connections establishing)
tps = 2469.831173 (excluding connections establishing)

结果显式,平均延迟为 1.620ms,比基准慢了 2.2%,比不开启 log_timing 慢了 1.9%。这比我听到的最低的非正式数据还低——有趣!

buffers、verbose 等等的开销

因为我听说 buffers 也可能有开销,所以我也看了一下。事实证明,它对这种工作负载没有可测量的影响,因此为了简洁起见,我将所有其他可用的设置归为一组,看看它们是否有可测量的影响。这包括切换到更详细的 JSON 格式,pgMustard 目前需要这种格式来审查计划。

因此,postgresql.conf 文件中的新 auto_explain 参数是:

shared_preload_libraries = 'auto_explain'
auto_explain.sample_rate = 1
auto_explain.log_min_duration = 10
auto_explain.log_analyze = true
auto_explain.log_timing = true
auto_explain.log_buffers = true
auto_explain.log_verbose = true
auto_explain.log_triggers = true
auto_explain.log_settings = true
auto_explain.log_nested_statements = true
auto_explain.log_format = 'JSON'

再次重启后:

michael$ pgbench -i -s 10
...
michael$ pgbench -c 4 -T 1800
...
latency average = 1.613 ms
tps = 2479.817711 (including connections establishing)
tps = 2479.823888 (excluding connections establishing)

结果平均延迟为 1.613ms,比基准慢了 1.7%。与之前相似,尽管这次测量和记录了更多的内容,但结果却比上次快,因此我认为可以假定至少有几个基点的误差范围。但总体来说,我很惊讶 buffers 等等在这次工作负载上并没有增加可测量的开销。

总结

因此,当我们设置了一个合理的最小持续时间后,在这个小型事务型工作负载 (系统时钟开销较低) 上,关闭 timing 时,auto_explain 的开销不到 1%,而开启 timing 时,开销约为 2%。这是在没有进行采样的情况下完成的,因此每个查询的细节都被跟踪,但只记录了最慢的查询。

当我们将 min_duration 设置为 0 毫秒 (记录所有内容) 时,即使没有启用 analyze 和 timing 参数,开销也要高得多,超过 25%。我认为这是由于日志记录的开销所致。

图片

图片

结论和后续步骤

在这个小型事务型工作负载上,系统时钟开销较低的情况下,auto_explain.log_timing 的开销仅约为 2%。考虑到我听到的警告和数据,这比我预期的要低,我认为对很多人来说这是一项值得的投资。请记住,这还是在没有进行采样的情况下,因此每个查询的计划细节都被跟踪。

在这台系统/工作负载上,我发现唯一有可测量开销的参数是 timing,但请记住,我们的系统时钟开销较低,查询也不复杂。

作为下一步,我们可以在更接近生产环境的机器上检查这些结果。我还认为研究分析型工作负载的开销也非常有趣,或者尝试测量不同系统时钟开销的影响。最后,我相信大幅降低 sample_rate 会显著减少开销 (以错过相应比例的慢查询计划为代价),但这值得进一步测量。

很想听听大家是否对这些方面的研究感兴趣,以及接下来应该研究哪些。

非常感谢 Nikolay Samokhvalov 和 Haki Benita 对本文草稿的审阅。

原文译自:https://www.pgmustard.com/blog/auto-explain-overhead-with-timing

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论