在工作中,我们遇到了一种神秘的问题。每隔几个小时,我们的一个表(tasks
)就会出现查询延迟增长(由 CPU 使用率引起),随后出现爆发式增长LWLock:BufferContent
,然后又下降到正常水平。以下是 RDS 性能洞察的屏幕截图:
我们做了大量工作来尝试缓解这种情况,这些工作已经导致事故发生的频率(据传闻)降低,严重程度也降低。然而,问题并没有消失。
首先,我们注意到有些查询没有命中索引。我花了一些时间确保针对此表的所有查询都执行索引和仅索引扫描。查询通常如下所示:
SELECT COUNT(1) FROM tasks WHERE status = 'PENDING' AND NOT deleted
status
并且我们在条件上有相应的索引status = 'PENDING' AND deleted = FALSE
。EXPLAIN ANALYZE
表明索引正在被正确使用。
我们确实发现,当用户有大量记录时,一些在我们的列上指定条件的查询userId
会有一个糟糕的查询计划。这个问题通过修复查询和索引改进得到了解决。据我们所知,没有任何查询对任何值都有糟糕的查询计划(但问题仍然存在)。
在峰值期间,传入负载没有增加。在事件发生之前、期间和之后,受影响的查询在 RDS 性能洞察中显示相同的“调用次数/秒”,并且我们的客户端应用程序显示稳定的 QPS,因此这不是由查询增加引起的。
我确实观察到,EXPLAIN ANALYZE
这些查询的结果确实表明在这些峰值期间堆提取有所增加,即使对于仅索引扫描也是如此。我读到在索引扫描期间堆仍然会受到影响,因为可见性图可能表明包含索引引用的元组的页面可能不可见。这似乎是一个强烈的信号:PENDING
任务(如上面的查询所示)通常是接收许多更新的“热”行,因此包含这些行的页面的可见性图需要提取是有道理的。
为了弥补这一缺陷,我们调整了自动清理设置,使清理频率大大提高。现在我们每隔几个小时就会看到一次自动清理,但问题并没有消失。我看到这些峰值出现在表的自动清理完成 20 分钟后。此外,EXPLAIN ANALYZE
峰值期间报告的堆提取次数可能比任务记录数高出 1-2 个数量级PENDING
:总数很少超过 200,我们可以看到在仅包含PENDING
记录的索引的索引扫描中存在数千次堆提取。
在调试过程中,我注意到查询规划器确实倾向于随着时间的推移调整查询的索引,即使负载保持不变。它有时会选择允许索引扫描但不允许仅索引扫描的索引。例如,它可能会选择 上的索引,而忽略上的条件status
索引,即使后者完全满足查询。我将和设置调整为 而不是 Postgres 默认值(分别为 1 和 4),这应该会指示 Postgres 优先使用索引扫描。不幸的是,问题仍然存在(尽管可能频率较低)。上面的屏幕截图是在设置调整之后的。status
status = 'PENDING' and not deleted
seq_page_cost
random_page_cost
1.0
一些附加信息:
- 该问题仅影响此一张表(或者我应该说,此一张表的查询)。
- 峰值期间表上的所有查询都会受到影响,而不是单个查询。
到目前为止,我几乎没有主意了。作为参考,我们在 Postgres 14.10 上运行db.m5.16xlarge
GP2 SSD 卷。我怀疑我们的 Postgres 版本只需要更新(计划中的工作),但这不是一个令人满意的解决方案。
我有点困惑的是,为什么造成延迟的资源使用是 CPU。您可以在上面的屏幕截图中看到绿色(CPU)的增加。基本上每个查询都应该扫描很少的记录。所有查询都是计数聚合,查询的条件都是简单的相等。这是没有峰值时受影响的查询输出之一:
Aggregate (cost=46.02..46.03 rows=1 width=8) (actual time=0.362..0.362 rows=1 loops=1)
" -> Index Only Scan using ""tasks_globalPending"" on tasks (cost=0.25..37.54 rows=3393 width=0) (actual time=0.239..0.353 rows=128 loops=1)"
Heap Fetches: 178
Planning Time: 0.161 ms
Execution Time: 0.377 ms
非常快!可以说是非常高效(尽管我更喜欢零堆提取)。对于COUNT()
~never 返回值大于 300 的程序来说,令人费解的是,它如何从每个查询 0.4 毫秒增加到 10-20 毫秒,并将这段时间浪费在 CPU 使用上。它在做什么?