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%,但工作负载没有明显变化
通过选项卡的预定路径的一个工作示例。
确认该窗口。 CPU选项卡显示,利用率在 09:15 攀升至 95% 并保持在该水平。 将范围缩小到 09:00 到 10:00,因此以后的选项卡会根据事件而不是全天进行排名。
检查工作量是否增加。 工作负载选项卡显示在 09:15,读取和写入元组活动保持平稳。 服务器未执行更多工作。 它正在以更低的效率执行相同的工作。 这种观察排除了“我们只是更忙”,并将你重定向到效率原因。
检查吞吐量。事务 确认 TPS 不变,这强化了相同的结论。
查找唯一的元凶。长时间运行的事务显示有一个 PID 的事务自 09:14 起一直处于打开状态,也就是在峰值出现前一分钟。 这个时间点可疑得值得记下来,但单个长事务本身很少会把 CPU 占满,所以你继续查下去。
对查询进行排名。 按总执行持续时间排序的 “查询 ”选项卡显示一个查询 ID,该 ID 占窗口的大部分 CPU 时间。 其调用计数是正常的,但其平均持续时间与早期的存储桶相比大致跃升了十倍。 一个查询变慢了,但调用频率并没有增加,这说明问题出在数据或执行计划上,而不是流量。
确认该机制。 “ 等待 ”选项卡显示主导等待与 I/O 相关,而不是与锁定相关,这与从索引扫描切换到顺序扫描的计划一致,这会消耗 CPU 和 I/O。
操作。 您检索 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。 比较各个桶中的平均持续时间可以判断某个查询是否变慢了——通常是由于执行计划或数据发生了变化——还是它原本就一直很慢,只是出现得更频繁了。
- 运行 索引优化 ,根据实际工作负荷获取索引建议。 此步骤是最有效的长期修复。
- 对该查询运行
EXPLAIN (ANALYZE, BUFFERS),查看时间和 I/O 实际消耗在何处。 在大型表上查找顺序扫描、针对大行计数的嵌套循环,以及对溢出到磁盘的排序。 - 减小表臃肿。 作为一次性步骤,运行
VACUUM (ANALYZE, VERBOSE) <table_name>;。 如果臃肿问题反复出现,根源在上游。 请参阅 监控 autovacuum。 - 添加计划所需的索引,并删除不缩小结果的联接。
- 对持续热的大型表进行分区。
- 重新审视表设计:为子表中的外键创建索引,删除未使用的索引(它们会给每次写入带来开销),并在批量加载期间禁用触发器。
还要谨慎地调整 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 只会把被浪费的资源上限抬得更高。