--- author: 棠羽 date: 2023/03/06 minute: 20 --- # CPU 使用率高的排查方法 在 PolarDB for PostgreSQL 的使用过程中,可能会出现 CPU 使用率异常升高甚至达到满载的情况。本文将介绍造成这种情况的常见原因和排查方法,以及相应的解决方案。 [[toc]] ## 业务量上涨 当 CPU 使用率上升时,最有可能的情况是业务量的上涨导致数据库使用的计算资源增多。所以首先需要排查目前数据库的活跃连接数是否比平时高很多。如果数据库配备了监控系统,那么活跃连接数的变化情况可以通过图表的形式观察到;否则可以直接连接到数据库,执行如下 SQL 来获取当前活跃连接数: ```sql:no-line-numbers SELECT COUNT(*) FROM pg_stat_activity WHERE state NOT LIKE 'idle'; ``` `pg_stat_activity` 是 PostgreSQL 的内置系统视图,该视图返回的每一行都是一个正在运行中的 PostgreSQL 进程,`state` 列表示进程当前的状态。该列可能的取值为: - `active`:进程正在执行查询 - `idle`:进程空闲,正在等待新的客户端命令 - `idle in transaction`:进程处于事务中,但目前暂未执行查询 - `idle in transaction (aborted)`:进程处于事务中,且有一条语句发生过错误 - `fastpath function call`:进程正在执行一个 fast-path 函数 - `disabled`:进程的状态采集功能被关闭 上述 SQL 能够查询到所有非空闲状态的进程数,即可能占用 CPU 的活跃连接数。如果活跃连接数较平时更多,则 CPU 使用率的上升是符合预期的。 ## 慢查询 如果 CPU 使用率上升,而活跃连接数的变化范围处在正常范围内,那么有可能出现了较多性能较差的慢查询。这些慢查询可能在很长一段时间里占用了较多的 CPU,导致 CPU 使用率上升。PostgreSQL 提供了慢查询日志的功能,执行时间高于 `log_min_duration_statement` 的 SQL 将会被记录到慢查询日志中。然而当 CPU 占用率接近满载时,将会导致整个系统的停滞,所有 SQL 的执行可能都会慢下来,所以慢查询日志中记录的信息可能非常多,并不容易排查。 ### 定位执行时间较长的慢查询 [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html) 插件能够记录数据库服务器上所有 SQL 语句在优化和执行阶段的统计信息。由于该插件需要使用共享内存,因此插件名需要被配置在 `shared_preload_libraries` 参数中。 如果没有在当前数据库中创建过 `pg_stat_statements` 插件的话,首先需要创建这个插件。该过程将会注册好插件提供的函数及视图: ```sql:no-line-numbers CREATE EXTENSION IF NOT EXISTS pg_stat_statements; ``` 该插件和数据库系统本身都会不断累积统计信息。为了排查 CPU 异常升高后这段时间内的问题,需要把数据库和插件中留存的统计信息做一次清空,然后开始收集从当前时刻开始的统计信息: ```sql:no-line-numbers -- 清空当前数据库的统计信息 SELECT pg_stat_reset(); -- 清空 pg_stat_statements 插件截止目前收集的统计信息 SELECT pg_stat_statements_reset(); ``` 接下来需要等待一段时间(1-2 分钟),使数据库和插件充分采集这段时间内的统计信息。 统计信息收集完毕后,参考使用如下 SQL 查询执行时间最长的 5 条 SQL: ```sql:no-line-numbers -- < PostgreSQL 13 SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 5; -- >= PostgreSQL 13 SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5; ``` ### 定位读取 Buffer 数量较多的慢查询 当一张表缺少索引,而对该表的查询基本上都是点查时,数据库将不得不使用全表扫描,并在内存中进行过滤条件的判断,处理掉大量的无效记录,导致 CPU 使用率大幅提升。利用 `pg_stat_statements` 插件的统计信息,参考如下 SQL,可以列出截止目前读取 Buffer 数量最多的 5 条 SQL: ```sql:no-line-numbers SELECT * FROM pg_stat_statements ORDER BY shared_blks_hit + shared_blks_read DESC LIMIT 5; ``` 借助 PostgreSQL 内置系统视图 [`pg_stat_user_tables`](https://www.postgresql.org/docs/15/monitoring-stats.html#MONITORING-PG-STAT-ALL-TABLES-VIEW) 中的统计信息,也可以统计出使用全表扫描的次数最多的表。参考如下 SQL,可以获取具备一定规模数据量(元组约为 10 万个)且使用全表扫描获取到的元组数量最多的 5 张表: ```sql:no-line-numbers SELECT * FROM pg_stat_user_tables WHERE n_live_tup > 100000 AND seq_scan > 0 ORDER BY seq_tup_read DESC LIMIT 5; ``` ### 定位长时间执行不结束的慢查询 通过系统内置视图 `pg_stat_activity`,可以查询出长时间执行不结束的 SQL,这些 SQL 有极大可能造成 CPU 使用率过高。参考以下 SQL 获取查询执行时间最长,且目前还未退出的 5 条 SQL: ```sql:no-line-numbers SELECT *, extract(epoch FROM (NOW() - xact_start)) AS xact_stay, extract(epoch FROM (NOW() - query_start)) AS query_stay FROM pg_stat_activity WHERE state NOT LIKE 'idle%' ORDER BY query_stay DESC LIMIT 5; ``` 结合前一步中排查到的 **使用全表扫描最多的表**,参考如下 SQL 获取 **在该表上** 执行时间超过一定阈值(比如 10s)的慢查询: ```sql:no-line-numbers SELECT * FROM pg_stat_activity WHERE state NOT LIKE 'idle%' AND query ILIKE '%表名%' AND NOW() - query_start > interval '10s'; ``` ### 解决方法与优化思路 对于异常占用 CPU 较高的 SQL,如果仅有个别非预期 SQL,则可以通过给后端进程发送信号的方式,先让 SQL 执行中断,使 CPU 使用率恢复正常。参考如下 SQL,以慢查询执行所使用的进程 pid(`pg_stat_activity` 视图的 `pid` 列)作为参数,中止相应的进程的执行: ```sql:no-line-numbers SELECT pg_cancel_backend(pid); SELECT pg_terminate_backend(pid); ``` 如果执行较慢的 SQL 是业务上必要的 SQL,那么需要对它进行调优。 首先可以对 SQL 涉及到的表进行采样,更新其统计信息,使优化器能够产生更加准确的执行计划。采样需要占用一定的 CPU,最好在业务低谷期运行: ```sql:no-line-numbers ANALYZE 表名; ``` 对于全表扫描较多的表,可以在常用的过滤列上创建索引,以尽量使用索引扫描,减少全表扫描在内存中过滤不符合条件的记录所造成的 CPU 浪费。