排查 CPU 使用率过高问题

CPU 使用率高只是一种症状,背后可能有很多原因,而靠猜测来判断则是代价高昂的错误。 如果服务器的真正问题是缺少索引,那么给它升级配置也不过是多撑几周,同时账单还会更高。 这份 CPU 指南的作用,是在你花任何钱之前,先确定你实际遇到的是哪一种原因。

本指南介绍的症状

  • CPU 利用率持续接近 100%,或按计划出现峰值飙升
  • 工作负载看起来没有变化,而查询延迟却在恶化
  • 一台起初一段时间表现良好、随后突然变慢的突发型服务器
  • 即使应用程序流量下降,CPU 也会保持高

开始之前

启用 PostgreSQL 服务器日志、会话数据、查询存储 运行时 和 AllMetrics;将 pg_qs.query_capture_mode 设置为 TOP 或 ALL;并启用 metrics.collector_database_activity。

对于锁定和阻止选项卡,还要将log_lock_waits设置为on。

完整步骤位于 “使用故障排除指南”中。

本指南的组织方式

选项卡 它所回答的问题
CPU CPU 占用率是否确实升高了,具体是在什么时候?
工作量 工作量是否增加?
交易 事务吞吐量是否增加?
长期运行的事务 是否为单个会话占用资源?
查询 哪些语句占用的 CPU 时间最多?
用户连接 这是连接风暴而不是查询问题吗?
锁定和阻止 会话是否因争用锁而耗费时间?
等待 执行实际上被什么阻塞了?
日志 服务器是否在写入自身日志时消耗 CPU 资源?
见解 自动检测到哪些异常?

演练:CPU 达到 95%,但工作负载没有明显变化

通过选项卡的预定路径的一个工作示例。

  1. 确认该窗口。 CPU选项卡显示,利用率在 09:15 攀升至 95% 并保持在该水平。 将范围缩小到 09:00 到 10:00,因此以后的选项卡会根据事件而不是全天进行排名。

  2. 检查工作量是否增加。 工作负载选项卡显示在 09:15,读取和写入元组活动保持平稳。 服务器未执行更多工作。 它正在以更低的效率执行相同的工作。 这种观察排除了“我们只是更忙”,并将你重定向到效率原因。

  3. 检查吞吐量。事务 确认 TPS 不变,这强化了相同的结论。

  4. 查找唯一的元凶。长时间运行的事务显示有一个 PID 的事务自 09:14 起一直处于打开状态,也就是在峰值出现前一分钟。 这个时间点可疑得值得记下来,但单个长事务本身很少会把 CPU 占满,所以你继续查下去。

  5. 对查询进行排名。 按总执行持续时间排序的 “查询 ”选项卡显示一个查询 ID,该 ID 占窗口的大部分 CPU 时间。 其调用计数是正常的,但其平均持续时间与早期的存储桶相比大致跃升了十倍。 一个查询变慢了,但调用频率并没有增加,这说明问题出在数据或执行计划上,而不是流量。

  6. 确认该机制。 “ 等待 ”选项卡显示主导等待与 I/O 相关,而不是与锁定相关,这与从索引扫描切换到顺序扫描的计划一致,这会消耗 CPU 和 I/O。

  7. 操作。 您检索 SQL 文本,运行 EXPLAIN (ANALYZE, BUFFERS),并确认在一个已严重膨胀的表上发生了顺序扫描。 通过一次性执行 VACUUM (ANALYZE, VERBOSE) 可恢复该计划,CPU 也会恢复到基线水平。 由于膨胀是根本原因,于是你打开 Monitor autovacuum,以查明 autovacuum 为什么未能及时跟上。 否则,它会再次发生。

这种排查思路具有普遍性:先确认问题,排除工作负载增长的因素,将原因归结到某个查询或会话,识别其作用机制,解决根本原因而不是只处理表面症状。

选项卡参考

CPU

此图表显示正在使用的 CPU 的最大百分比,而不是平均值。 单个简短的峰值呈现在完全高度,因此锯齿线不一定意味着持续的压力。 解读整体形态,而不只看峰值。

高 CPU 本身不是问题。 一台负载达到 100% 且查询延迟仍可接受的服务器,才是让你的钱花得值的服务器。 当它还伴随着延迟、超时或连接排队时,就会成为问题。 在花点时间在这里之前,请确认你有一个真正的症状。

识别形状。 该模式指示接下来要打开哪个选项卡:

图案 通常意味着 跳转到
持续高原近 100% 服务器真正饱和 工作负载优先,以确定负载是否增加了
按固定计划出现的规律性峰值 批处理作业、cron 任务或计划报表 查询,范围缩小到一个峰值窗口
持续的阶跃变化 部署、计划回归或参数更改 查询,比较此步骤前后的存储桶
锯齿状,先上升后急剧下降 通常检查点或清空活动 等待,然后 排查高 IOPS 使用率问题
尖峰波动无明显规律,延迟正常 正常突发工作负荷 可能没什么。 验证延迟是否可接受并停止。
数值较高但持平,而工作负荷保持平稳 效率损失,而不是负载增长 查询 和 锁定和阻止

继续之前,先将窗口固定。 请注意升高开始和结束的时间点,然后将时间范围缩小到大致对应的那段时间。 后续的每个选项卡都会以所选范围作为基准进行比较,因此,在四小时的时间窗口内调查一起持续 15 分钟的事件时,真正的元凶会淹没在正常流量中。 请记住一个小时的最小值:如果事件更短,你仍将得到一个小时,因此在读取查询排名时考虑周围的基线。

可突发层

此层上会显示两个额外的指标,它们会更改解释其他所有指标的方式:

  • 消耗的 CPU 信用额度:服务器运行在其基线之上时花费的积分。 具有 20% 基准且运行在 80% 负载下的服务器会持续消耗积分。
  • 剩余 CPU 积分:为将来突增使用需求而储备的积分。 它们累积在基线以下,并消耗在基线之上。

如果您的工作负载高于基线水平,并且剩余积分降为零,服务器性能将被限制到基线水平。 这种情况是 Burstable 层级中最常被误诊的性能问题:你的查询本身并没有问题,你只是用尽了该层级的预算。 在排查其他任何问题之前,先检查剩余额度,因为被限流的服务器会导致查询变慢、锁等待以及连接堆积,而这些现象看起来都像是彼此独立的问题。

高消耗且剩余额度充足,说明该层级正按预期运行。

突发性能型适用于低负载和开发/测试工作负载。 如果信用额度定期耗尽,请移动到“常规用途”。 无论进行多少查询调优,都无法改变基线。

工作量

拆分为 读取工作负荷 和 写入工作负荷 视图。 此选项卡将“更多工作”与“效率较低的工作”分开。了解两个读取指标计数,因为它们不可互换:

Metric 计数
tup_returned 由顺序扫描获取的活动行,加上由索引扫描返回的索引条目
tup_fetched 仅索引扫描提取的实时行

它们之间的比率是有用的信号。 当 tup_fetched 远高于 tup_returned 时,服务器会读取许多行数据,却只产生相对较少的有用结果。 此模式指示对大型表的顺序扫描。 这种模式会消耗与表大小成正比、而不是与结果集大小成正比的 CPU,这正是表膨胀或缺少索引所导致的现象。 当两者走势非常接近时,读取操作主要由索引驱动。

写入负载统计插入、更新和删除的元组数量。 更新会带来双重代价:它们现在会消耗 CPU 资源,并产生日后导致膨胀的死元组。

这两个视图都排除了系统数据库azure_sysazure_maintenance,因此数字反映了工作负荷而不是平台开销。

如何结合 CPU 曲线进行解读:

  • 工作负荷随 CPU 增加。 CPU 正在执行实际工作。 转到 查询 和 用户连接。
  • 工作负载持平,而 CPU 持续攀升。 效率损失。 怀疑是膨胀、执行计划退化、锁争用或日志记录导致的问题。
  • 读取工作负荷保持平稳,但 tup_returned 向上偏离。 尽管请求量没有变化,扫描的选择性却越来越低。 严重臃肿或统计信息过时的信号。

写入工作负载详细信息在只读副本上不可用。

Transactions

两个视图: 事务趋势 和 每秒事务数。 两者都需要 metrics.collector_database_activity。

事务趋势显示已提交事务与已回滚事务的对比趋势。 不要跳过回滚系列内容。 回滚率随着 CPU 使用率上升而攀升,通常意味着应用程序在失败后反复重试,因此服务器不得不为最终被丢弃的工作承担完整的执行成本。 单看 TPS 图表,这看起来和工作负载增加一模一样,但解决方法却完全不同:你要找的是错误,而不是去优化查询。

每秒事务数按数据库细分吞吐量,从而帮助你判断峰值是发生在整个服务器范围内,还是仅限于某个租户或应用程序。

与 CPU 上升相关的 TPS 峰值指向真正的工作负荷增长。 接下来:

  • 使用 查询 查看特定语句是否占主导地位。 在高 TPS 下,低效查询的影响会迅速放大。
  • 使用 用户连接 将实际流量增长与重试风暴区分开来。
  • 如果连接模式表现为大量短生命周期连接,请考虑使用连接池。
  • 如果增长合理且持续,则进行扩容。

长期事务

任何运行时间超过指南规定阈值的事务。 这两个持续时间是单独绘制的,而且这种区分很重要:

  • 连接持续时间 (collection_time - backend_start):客户端已连接多长时间。 在池化情况下,较高的值是正常的,其本身并不是问题。
  • 事务持续时间 (collection_time - xact_start):当前事务已打开的时间。 这是表示出现问题的数字。

当该指南检测到处于 idle in transaction 状态的会话时,会显示横幅提示,并列出具体的 PID。 将该横幅视为发现项,而不是警告。 处于此状态的会话在按住锁、占用连接槽并固定 xmin 地平线时不起作用,因此无法清理死元组。 这往往是许多问题的根本原因,而这些问题实际上会在完全别的地方暴露出来,其中就包括把你带到本指南前来的臃肿问题。

立即缓解。 请先取消当前正在运行的语句。 这是两个选项中干扰较小的一个,因为会话及其事务都得以保留:

SELECT pg_cancel_backend(<pid>);

如果会话处于事务空闲状态,或者取消操作也无法释放资源,请终止整个后端进程:

SELECT pg_terminate_backend(<pid>);

Caution

终止后端会回滚其未完成的事务。 在结束会话之前,请先确认该会话当前正在执行什么操作。 PID 可能属于长时间运行的迁移任务,或属于会从头开始重新启动的批处理作业。

设置防护措施,防止再次发生。 设置会话和事务可以运行多长时间的限制:

参数 它所限定的范围 备注
statement_timeout 单个语句 最广泛的安全网。 如果某些作业确实会运行较长时间,应按角色或按会话进行设置,而不是在整个服务器范围内统一设置。
idle_in_transaction_session_timeout 处于空闲状态且存在未提交事务的会话 默认值为 0 (从不)。 此设置通常可防止清理被阻止。 300000 (5 分钟)是一个常见的起点。
idle_session_timeout 无事务的空闲会话 PostgreSQL 14 及更高版本。 回收已废弃客户端占用的连接槽位。
transaction_timeout 事务总持续时间 PostgreSQL 17 及更高版本。

Queries

此选项卡由 查询存储 提供支持。需要将 pg_qs.query_capture_mode 设置为 TOP 或 ALL。

选项卡按三种方式对查询进行排名,因为“昂贵”具有多种含义:

  • 按平均持续时间:单个慢查询。
  • 按总时长:时间窗口内真正消耗 CPU 的对象。 一个耗时 50 毫秒且被调用 100,000 次的查询,其优先级高于一个耗时 10 秒但只被调用两次的查询。
  • 按调用次数:适合进行缓存或批处理的高频语句。

始终检查总时长,而不只是均值。 一千次削减的死亡比一个灾难性的查询更常见,只有总排名暴露了它。

选择用于查看各存储桶历史记录的查询 ID。 比较各个桶中的平均持续时间可以判断某个查询是否变慢了——通常是由于执行计划或数据发生了变化——还是它原本就一直很慢,只是出现得更频繁了。

  1. 运行 索引优化 ,根据实际工作负荷获取索引建议。 此步骤是最有效的长期修复。
  2. 对该查询运行 EXPLAIN (ANALYZE, BUFFERS),查看时间和 I/O 实际消耗在何处。 在大型表上查找顺序扫描、针对大行计数的嵌套循环,以及对溢出到磁盘的排序。
  3. 减小表臃肿。 作为一次性步骤,运行 VACUUM (ANALYZE, VERBOSE) <table_name>;。 如果臃肿问题反复出现,根源在上游。 请参阅 监控 autovacuum。
  4. 添加计划所需的索引,并删除不缩小结果的联接。
  5. 对持续热的大型表进行分区。
  6. 重新审视表设计:为子表中的外键创建索引,删除未使用的索引(它们会给每次写入带来开销),并在批量加载期间禁用触发器。

还要谨慎地调整 max_connections 和 max_parallel_workers_per_gather。 两者看起来都类似于吞吐量设置,在设置过高时都消耗 CPU。 并行工作进程会增加单个查询的 CPU 使用量。

在只读副本上

读副本支持 查询存储,并且仍然是此选项卡的主要数据源。像在主副本上一样启用它,并将 pg_qs.query_capture_mode 设置为 TOP 或 ALL。

本机日志记录是同一查询的替代视图。 你可以使用它来替代 查询存储,或与 查询存储 一起使用。 它需要对副本设置两个参数:

  • log_line_prefix 精确设置为 time=%t, session=%c, pid=%p, user=%u, db=%d, client=%h, app=%a ,包括尾随空格。 本指南会从日志行中解析出这些具名字段,因此如果使用不同的前缀,即使其包含的信息相同,也会导致基于日志的图表为空。
  • log_min_duration_statement 设置为阈值(以毫秒为单位)。

Caution

不要将 log_min_duration_statement 设置为 0. 记录每个查询可能会产生大量日志,足以导致查询失败。 如果遇到这种情况,请提高指南中的执行时长筛选条件,或调高该参数。 请查看日志选项卡,其中日志量本身就会显示为一种 CPU 成本。

对于副本,调优也有所不同。 无法运行 VACUUM,因此请确保 autovacuum 在 主服务器上保持运行。 在主副本上创建的索引会自动创建。 如果看到 canceling statement due to conflict with recovery,请调高 max_standby_streaming_delay,并使用 hot_standby_feedback,但要知道这会以主库膨胀增加为代价来减少副本端取消。

用户连接

需要 PostgreSQL 会话数据 日志类别。 三个视图:

  • 按状态划分的连接:在活动、空闲、idle in transaction等状态之间的分布。 此处的大量 idle in transaction 人口支持 “长时间运行的事务 ”选项卡显示的内容。
  • 按持续时间进行连接: 短 (低于 1 秒)、 正常 (1 秒到 20 分钟)和 长 (超过 20 分钟),派生自日志中的断开连接消息。
  • PgBouncer 连接统计:启用 PgBouncer 时,包括连接池中的连接总数和连接池数量。

大量短连接表明某个应用程序会为每个请求打开一个连接。 每一次都需要在执行任何查询之前完成一次进程 fork 和后端初始化,而在高频率下,仅这种初始化开销就可能占用大部分 CPU。

PgBouncer 视图可将怀疑转化为明确诊断。 将池化连接数与连接池的数量进行比较,可以判断连接池的大小设置是否合适。 将它与按状态划分的连接数结合查看,可以看出连接池是否确实减少了后端连接数,还是只是将每个客户端连接直接透传到后端。

每个 PostgreSQL 连接都是一个单独的操作系统进程,具有自己的内存。 即使在执行任何一条查询之前,数以百计的多数处于空闲状态的连接也会实实在在地占用 CPU 和内存资源,因此使用连接池通常比纵向扩展更划算。 Azure Database for PostgreSQL灵活服务器中的 PgBouncer 内置于Azure Database for PostgreSQL灵活服务器中。 使用 pgbouncer.enabled 启用它,并启用 metrics.pgbouncer_diagnostics 以获取其指标。 两者都是动态的,无需重启。 PgBouncer 不受 可突发 层级支持。

调整池的大小:

设置 Guidance
pool_mode 对于由大量短事务组成的高吞吐量工作负载,请使用 transaction,因为它能实现高效得多的连接复用。 如果需要使用依赖连接的功能,例如预处理语句、建议锁或/LISTENNOTIFY,请使用 session。
default_pool_size 适用于每个用户/数据库对,而不是每个服务器。 把你预计的每个池都加总起来,并将总量控制在 max_connections 以下,留出足够余量。
server_idle_timeout 保留未使用的服务器连接的时间。 将其调低,以便更早释放空闲后端。
query_timeout 将单个查询绑定到连接池管理器。

两种值得识别的故障模式:

  • 资源池耗尽。 活动服务器连接数保持在 default_pool_size,而等待中的客户端数量则在上升。 提高 default_pool_size、优化占用连接的慢查询,或将模式从 session 切换为 transaction。
  • PgBouncer 饱和度。 PgBouncer 是单线程的,因此无论服务器有多大,它都使用一个核心。 其典型特征是:6432 端口上的延迟比直接连接到 5432 更高,同时等待的客户端不断增加,而活跃的服务器连接数则保持在较低水平default_pool_size。 在单独的 VM 上跨多个 PgBouncer 实例横向扩展,或评估多线程池程序,例如 PgCat。

锁定和阻止

需要 log_lock_waits 和 metrics.collector_database_activity。 PostgreSQL 在会话等待的时间超过 deadlock_timeout1 秒时记录消息,以获取锁。

四个部分:

  • 按锁等待事件划分的会话数:等待 Lock 等待事件的会话数。
  • 按锁类型获取锁的持续时间:获取锁后持有的时间。 在获取锁之前已断开连接的会话不会显示。
  • 等待锁和已获取锁概览:同时显示两者及其持续时间。
  • 阻塞信息:在时间窗口结束时仍在等待的会话。

锁争用会间接推高 CPU 使用率:等待中的会话仍会占用连接并频繁重新检查,而它们正在进行的工作会被重试或串行化处理。

等待

需要 pgms_wait_sampling 以及“等待统计”诊断类别。 CPU 使用率高几乎总是伴随着某种潜在的等待模式。

  • 按 WaitEventType 进行筛选,然后钻取到特定的 WaitEvent。
  • 启用 “隐藏不可操作的等待 ”以删除 Client 和 Activity 事件。 这些事件表明服务器正在等待其他进程,通常并不表示存在服务器端争用。
  • 在 Summary 和 之间切换Top queries per wait。
  • 选择任意行可读取事件的含义、其典型原因和建议的操作。

Logs

日志易于忽略,但可以提供有价值的信息。 PostgreSQL 在同步运行查询的同一后端进程中写入日志行。 详细日志记录会直接与查询执行争夺 CPU 和 I/O 资源。 此选项卡显示随时间变化的日志量,并按最可能负责的参数进行细分,同时叠加显示总量。 将此数据与 CPU 选项卡进行比较:持续日志突发与 CPU 峰值保持一致,有助于识别问题以及要更改的参数。

设置 Recommended 它为何重要
log_min_duration_statement -1或 >= 1000 ms 介于 999 和 0 毫秒之间的值会在 OLTP 工作负载中产生极大的负载。
log_statement none 或 ddl mod 和 all 会记录每次修改或每条语句。
log_statement_stats off 每条语句的资源统计信息。仅用于简短且有针对性的调优会话。
pgaudit.log none或 ddl,role 审核 READ//WRITEALL 时,会同步记录每条匹配的语句。
pgaudit.role 除非需要对象审计,否则保持未设置 对象级审计会记录该角色的对象上的所有读取和写入操作。

为持续了解查询情况,优先使用Azure Database for PostgreSQL 灵活服务器中的查询存储,而不是log_min_duration_statement。 它具有较低的开销,并保留可以分析的历史记录。 如果必须记录慢语句,请将 log_min_duration_sample 与 log_statement_sample_rate 搭配使用,以仅用一小部分日志量保留有代表性的样本。

Insights

此选项卡显示此窗口中自动检测到的异常:连接激增、显著的等待事件,以及按综合数量和突增得分排名的日志消息异常。 空白的 洞察 选项卡表明服务器在该时间段内状态正常。 打开后请稍等片刻,以便完成加载。

优秀的标准是什么样的

信号 正常 调查
CPU 使用率 峰值时的余量 维持在 90% 以上
剩余可突发信用额度 稳定或恢复 趋近于零
工作负荷与 CPU 一起移动 工作负载持平时,CPU 使用率上升
排名靠前的查询占总持续时间的份额 分散在多个查询中 一两个查询占据主导地位
连接组合 多为正常时长 大规模短连接群体
锁定等待 罕见 重复,或在窗口结束时仍被阻止的会话
日志量 稳定且低 与 CPU 峰值对齐的突发活动

本指南无法告诉你的内容

  • 客户端成本。 应用程序、ORM 或网络层使用的 CPU 在此处不可见。
  • 子采样事件。 对会话和等待进行采样。 两次采样之间发生的200毫秒锁风暴不会留下任何痕迹。
  • 为什么计划发生了变化。 本指南显示查询速度较慢;它不显示计划差异。 使用 EXPLAIN (ANALYZE, BUFFERS) 并检查统计信息是否过时。
  • 按角色和每会话替代。 显示的参数值是服务器级。 拥有自己的 work_mem 的角色将不会显示出来。
  • 扩展是否有帮助。 如果原因在于某个未建立索引的查询,那么更大的 SKU 只会把被浪费的资源上限抬得更高。