
做PostgreSQL运维这几年我最怕半夜收到一类告警——连接数打满。短信一响基本就是一片应用在报错“too many clients already”业务侧立刻炸锅。排查这类问题绕不开三个监控指标连接数、负载、IO。连接数告诉你到底有多少客户端挤在门口负载告诉你数据库这个引擎正在被压榨到什么程度IO则暴露了最后一道物理瓶颈。这三个指标在逻辑上是一条链路应用发起连接PostgreSQL进程分配到CPU资源查询命中磁盘时需要IO支撑。只要其中一环堵住整个数据库看起来就会像“卡死”了一样。这篇文章的思路是我这些年PostgreSQL监控实践里整理出来的一个通用框架。对于正在维护PostgreSQL的DBA、后端工程师以及准备自建监控体系的团队来说可以直接拿来当参考。接下来我会按“监控前准备—连接数—负载—IO—落地实施”的顺序展开把每个维度的关键视图、排查SQL、阈值经验和优化方向都说清楚。1. 开始监控之前先想清楚这三件事1.1 为什么框定连接数、负载、IO这三个维度很多人一上来就装Prometheus、配一堆exporter看板做得花团锦簇实际上连pg_stat_activity都没跑过。我的观点是与其贪多嚼不烂不如先把连接数、负载、IO三件事吃透。这三个指标的关系我常用一个类比来想连接数是进停车场的车辆数量负载是收费口的处理速度IO是停车场里的停车位。车太多了连接数高收费口忙不过来CPU负载高车位又不够IO瓶颈车辆就会排队反映到业务端就是查询越来越慢。在实际故障里这三个指标往往连环爆发。比如某个应用突然起了大量线程连接数暴涨直接打满max_connections新连接进不来已有的连接还在跑慢查询CPU负载跟着上升慢查询带来大量物理读IO压力也被点燃。如果你只盯一个指标很容易被表象误导——明明看到的是负载高根因却是连接数被某个异常客户端占满。所以监控的第一步不是装工具而是确认“我要监控哪些指标以及它们之间的因果链路”。PostgreSQL是进程模型每个客户端连接对应一个后端进程而不是线程。进程切换的开销比线程大连接数一旦上去会显著放大CPU和内存的消耗。这也是为什么连接数往往是PostgreSQL故障的第一层症状。1.2 监控工具选型的现实考量工具选型要分阶段来看。如果只是维护一两套库没必要一上来就上PrometheusGrafana全家桶。用系统自带的psql、pg_stat_activity、iostat、uptime配合crontab和日志文件就能解决90%的问题。这套方式胜在轻量、无侵入、排障直接缺点是没有历史趋势需要自己在脚本里落数据否则事后回看时拿不出曲线。如果维护几十套实例或者有业务方经常来问“昨晚为什么慢了”那就有必要上一个集中的采集和展示平台。我常用的组合是Prometheus postgres_exporter node_exporter Grafana。postgres_exporter会把pg_stat_database、pg_stat_bgwriter、pg_stat_replication、锁信息等大量数据库指标暴露成Prometheus格式node_exporter负责操作系统层的CPU、内存、磁盘IO和网络指标。这套组合最大的好处是数据库指标和系统指标能对到同一条时间线上排查问题时非常关键。选型上有两个提醒。第一别迷信工具本身监控体系的灵魂永远是“指标解读能力”看板只是辅助。第二postgres_exporter默认采集的指标非常多建议按需关闭一部分否则采集本身也会给数据库带来额外压力。1.3 采集周期不是越短越好采集周期的设定是我看到很多人踩坑的地方。连接数是快变量可能在几秒内从200冲到2000建议采集间隔控制在15秒到1分钟之间至少能捕捉到突增的轮廓。负载指标里的load average本身是1分钟、5分钟、15分钟的平均值采集间隔60秒就够太密反而看不出趋势。IO指标比较特殊。iostat如果持续跑能看到瞬时峰值但一次性采样看%util容易出现误判建议看持续几轮的平均趋势。数据库侧的pg_stat_bgwriter是累计值两次采样的差值才有意义所以IO监控本质上依赖历史数据做对比中间断档一天会直接导致当天的趋势分析失真。我在实际生产环境里的设定是核心库的连接数采集频率15秒其余指标统一60秒。这样监控数据量可控排障时也有足够的分辨率。2. 连接数监控从pg_stat_activity到连接池治理2.1 连接数最常用的排查SQLpg_stat_activity是整个连接数监控的核心视图每一行代表一个后端进程也就是一个连接。字段很多但排查时最常用的是这几个state连接当前状态。active正在执行SQL、idle空闲、idle in transaction事务内空闲是三类最常见的状态。wait_event_type和wait_event连接当前正在等待什么这个字段在负载和IO排查里同样非常重要。application_name来自哪个应用很多连接池框架会配置这个字段。client_addr客户端来源IP定位异常连接来源时直接看它。xact_start、query_start、state_change分别记录事务开始时间、当前SQL开始时间、状态变更时间算时长全靠这几个字段。下面是我排查连接问题时必跑的几条SQL。第一看当前连接总量和剩余量SELECT (SELECT setting::int FROM pg_settings WHERE name max_connections) AS max_conn, (SELECT count(*) FROM pg_stat_activity) AS used_conn, (SELECT setting::int FROM pg_settings WHERE name max_connections) - (SELECT count(*) FROM pg_stat_activity) AS remain_conn;第二按状态和等待类型分组快速定位异常聚集的方向SELECT state, wait_event_type, count(*) FROM pg_stat_activity GROUP BY state, wait_event_type ORDER BY count(*) DESC;第三找出“空占着茅坑不拉屎”的连接——事务内空闲时间最长的那些SELECT pid, usename, application_name, client_addr, now() - xact_start AS xact_duration, now() - state_change AS state_duration, query FROM pg_stat_activity WHERE state idle in transaction ORDER BY xact_duration DESC LIMIT 20;我处理过的连接数打满事故里相当一部分不是active连接太多而是idle in transaction把连接全部占满。应用开了事务查询完之后既不提交也不回滚事务就这么一直挂着。从业务侧看这条连接是“空闲”的实际上事务的锁、未提交数据、已分配的内存资源全都被它占着。2.2 max_connections的合理设置与踩坑max_connections是PostgreSQL允许的最大并发连接数默认值只有100。很多团队遇到连接数告警的第一反应就是把这个参数调大300、500甚至直接调到2000。这种做法短期能压下告警但给系统埋下了更大的隐患。PostgreSQL的进程模型决定了每个连接都要独占一块进程内存还需要分配work_mem、temp_buffers等临时空间。并发连接越大系统内存消耗越高CPU上下文切换越频繁。尤其当连接数接近上限时整个实例会因为进程调度开销暴增而明显变慢。换句话说max_connections设置过大等于用更多资源去“养”低效的空闲连接反而挤压了正常查询的资源。要估算连接数和内存的关系粗略可以用这个公式(max_connections × 预估单连接内存消耗) shared_buffers 系统开销但这里的单连接内存消耗不是一个固定值work_mem会在排序、哈希连接时按需分配不能直接用work_mem乘连接数去算死。更严谨的做法是观察连接数从100涨到300时实例RSS内存涨了多少用实测数据反推单连接开销。这个数字在你的业务场景里才是可信的。提示先处理异常连接再讨论调大max_connections。大量idle in transaction连接打满上限时调大参数只是治标不治本。我给出的通用建议小型应用、几十个并发用户默认100都不用动。常规业务系统配合应用侧连接池150到300足够。超过300还经常打满优先考虑的不是继续调大而是处理应用侧的连接池配置和空闲事务。如果你确实需要大连接数请先给系统加内存并同步注意shared_buffers与连接内存的配比别让shared_buffers把连接所需内存挤占掉。2.3 连接池连接数监控之外更要命的一环监控连接数时很多人忽略了连接池这个上游变量。连接池位于应用和数据库之间作用就是把数据库侧的真实连接数降下来。一个应用服务有50个线程如果每条线程都直连数据库那就是50条连接如果中间加了连接池数据库侧可能只需要10条连接。PostgreSQL建立连接的代价比MySQL更高除了TCP握手还要经过身份验证、系统表读取、进程分配等流程。压测数据里单次建连在毫秒级看起来不吓人但连接数波动大时建连风暴足以拖垮数据库CPU。连接池的选择主要有两种。应用内嵌连接池比如Java的HikariCP、Go的pgxpool适合单应用直连数据库的场景配置简单但每个应用实例仍然会各持有一批连接。后端代理类型的连接池比如PgBouncer则统一收口所有应用的连接再由PgBouncer维持与数据库的固定连接在应用实例多、连接波动大的场景里效果更明显。一个务实的监控口径连接数告警时先看client_addr和application_name搞清楚连接到底是哪个应用、哪台机器建的。如果是多个实例各自直连大概率是连接池配置太小导致连接数被撑爆如果是统一走PgBouncer就要重点看PgBouncer侧的连接数因为它才是数据库连接数的真实来源。2.4 两种高发连接异常泄漏与空闲事务连接泄漏和空闲事务是我在连接数故障里见到频率最高的两种类型。连接泄漏的表现是连接数曲线呈现一条稳步上升的阶梯线过了峰值后不回落只增不减。原因通常是应用代码里某条异常路径没有正确close连接连接池回收不到连接。这类问题的根因在应用侧数据库侧能做的是通过pg_stat_activity查到长期保持idle状态的连接按pid查锁、查query再顺藤摸瓜找到应用日志里对应的线程。空闲事务在数据库侧是能直接治理的。PostgreSQL从上到下有一系列超时机制statement_timeout单条SQL执行超时。idle_in_transaction_session_timeout事务内空闲超时也就是事务开了但一直不提交也不回滚。我建议核心业务库把idle_in_transaction_session_timeout设置为60到300秒之间。短事务业务可以设60秒长事务业务适当放宽但不要超过20分钟。这个参数配合告警能有效避免“事务饿死连接”的极端状况。补充一个实用的排查技巧判断是不是空闲事务占满连接可以看state idle in transaction的数量占比。如果这类连接占了总连接数的一半以上基本可以断定业务代码里有大批“开事务不结束”的问题这时候去修代码比调大max_connections有效得多。3. 负载监控不能只盯着load average3.1 数据库内部的负载信号等待事件很多人看负载只看load average和CPU us百分比这本属于只看“引擎盖”。数据库内部其实有自己的负载温度计就反映在pg_stat_activity的wait_event字段上。等待事件是PostgreSQL 9.6之后才系统化的机制专门帮助DBA定位后端进程正在等待什么。按wait_event_type划分常见的类型有Client等待客户端读取或处理数据常见的是ClientRead。IO等待磁盘IO返回比如DataFileRead、DataFileWrite、WALWrite。Lock等待获取重量级锁。LWLock内部轻量级锁比如buffer content锁。Activity等待内部活动比如并行查询时的等待。排障时我一般先聚合当前活跃连接的等待事件SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state active GROUP BY wait_event_type, wait_event ORDER BY count(*) DESC;如果看到大量进程挂在ClientRead上说明数据库把结果发给客户端后客户端迟迟不取走数据库被“慢消费者”拖住了这不是真正的数据库负载。如果大量进程挂在DataFileRead上说明shared_buffers命中率偏低物理读压力大。如果挂在Lock上那就是锁冲突来了需要去看pg_locks和锁等待链。等待事件的价值在于它把“数据库CPU高”这个笼统的现象拆解成“CPU到底在处理什么”的真相。同样是回答“为什么负载高”等待事件是数据库内部最直接的证据。3.2 系统层面的负载关联分析系统层面最经典的是uptime里的load average三个值分别代表1分钟、5分钟、15分钟的平均运行队列长度。判断负载是否越界不能光看数字要和CPU核数对比。4核机器load average到4说明队列刚好排满长期超过核数比如16核的机器跑到20意味着有大量进程在等待CPU调度系统已经过载。我看load average有个习惯先看15分钟值还是1分钟值高。15分钟高、1分钟低说明系统长期高负载、正在缓解1分钟高、15分钟低说明刚刚发生了一段突发压力可能是定时任务、批量查询、甚至某条大SQL顶上来的。CPU使用率要结合us、sy、wa三部分拆解us高用户态进程占CPU多通常是SQL计算、排序、hash join这类消耗。sy高内核态消耗大可能是系统调用频繁、进程切换多、网络中断多。连接数过大时sy会急剧上升因为进程上下文切换全耗在内核里了。wa高CPU在等IO这个直接指向磁盘瓶颈需要立刻去看iostat。我遇到过一个典型场景连接数打满时CPU的sy占比飙到40%以上而真正跑SQL的us反而很低。这时候加CPU解决不了问题根因在连接数过多、进程切换过于频繁。系统层和数据库层的关联分析最常见的操作是“时间对齐”。你在数据库监控曲线看到某个时间点active连接暴涨又看到系统监控曲线同一时间CPUsy飙高两条线对上问题就定位了一大半。这也是我强调要选一套能把系统指标和数据库指标放在同一条时间线上的监控平台的原因。3.3 慢查询与负载的双向影响负载高和慢查询经常互为因果。一条SQL写得烂每次执行好几秒一旦并发上来CPU、内存全被它吃掉其他正常SQL全被拖下水。要定位慢查询最有力的工具是pg_stat_statements。它需要作为扩展预先安装CREATE EXTENSION IF NOT EXISTS pg_stat_statements;注意这个扩展要写进postgresql.conf的shared_preload_libraries配置后需要重启实例且重启后pg_stat_statements里的累计数据会归零。pg_stat_statements会把SQL按规范化后的文本聚合记录调用次数、总耗时、平均耗时、最大耗时、返回行数等。排查慢查询最常用的是按总耗时倒序SELECT queryid, calls, total_exec_time, mean_exec_time, max_exec_time, rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;PostgreSQL 13及以上版本用total_exec_time字样更早版本是total_time。跑这条SQL时注意一个点它是实例级别的累计统计如果想排查“某个时间窗口内的慢查询”需要对比两个时间点的差值或者直接去看pg_stat_activity里query_start较早且仍在执行的SQL。锁等待是另一种“伪负载”。两个事务同时更新同一行后者会一直挂在Lock等待里进程状态始终是active但CPU并没有被大量消耗。从监控曲线看连接数和active进程数量都上去了load average却不一定高。这种场景需要去pg_locks查等待链SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid, blocked_activity.query AS blocked_query, blocking_activity.query AS blocking_query FROM pg_locks blocked_locks JOIN pg_locks blocking_locks ON blocking_locks.locktype blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple JOIN pg_stat_activity blocked_activity ON blocked_activity.pid blocked_locks.pid JOIN pg_stat_activity blocking_activity ON blocking_activity.pid blocking_locks.pid WHERE NOT blocked_locks.granted AND blocking_locks.granted;这条SQL能把谁在等谁看得明明白白。实际使用中我会再加个条件过滤掉阻塞者是自身进程的情况。4. IO监控数据库指标与系统指标对照看4.1 数据库侧的IO关键指标数据库侧的IO信息核心集中在pg_stat_bgwriter和pg_stat_database两个视图。注意这些指标大多是累计值必须做差值计算才有意义。pg_stat_bgwriter关注后台写统计重点看这几列checkpoints_timed按checkpoint_timeout定时触发的checkpoint次数。checkpoints_req因为WAL达到max_wal_size而被强制触发的checkpoint次数。buffers_checkpointcheckpoint期间刷出的缓冲区页数。buffers_clean后台写进程按bgwriter策略刷出的页数。buffers_backend后端进程自己直接刷出的页数。maxwritten_clean后台写进程因为一次刷盘达到bgwriter_lru_maxpages而提前停止的次数。这里有个关键比值checkpoints_req / (checkpoints_timed checkpoints_req)。如果这个比值偏高比如超过20%说明WAL增长太快、频繁触达max_wal_sizecheckpoint被迫提前发生。频繁checkpoint会带来大量脏页刷盘直接影响IO延迟。buffers_checkpoint占刷盘总量的比例也很关键。如果checkpoint刷盘占了大头说明大部分IO压力是checkpoint周期性制造出来的不是业务SQL造成的。这种情况可以适当调大checkpoint_timeout和max_wal_size把刷盘分散到更长周期。pg_stat_database里与IO相关的字段是blks_read、blks_hit以及blk_read_time、blk_write_time需要打开track_io_timing才会记录SELECT datname, blks_read, blks_hit, blk_read_time, blk_write_time FROM pg_stat_database;blks_hit / (blks_hit blks_read)就是共享缓冲区命中率。命中率不是越高越好但明显偏低比如低于90%说明shared_buffers可能配小了或者存在大量全表扫描物理读压力全部压给了磁盘。4.2 系统侧IO工具与指标解读数据库指标负责给出“谁在触发IO”的方向系统侧工具负责揭示“磁盘到底有多忙”。排查IO瓶颈我一般直接用iostat和vmstat。iostat的指标需要横向着理解%util设备繁忙程度。对传统机械硬盘%util接近100%基本就是打满了但SSD并发能力强%util到100%不一定代表性能耗尽还要结合IOPS和带宽看是否真的达到硬件上限。await平均IO响应时间包含排队和服务时间。机械盘await在几十毫秒是常态SSD通常应在个位数毫秒。如果await明显偏高说明磁盘有排队。r/s、w/s每秒读写请求数。结合块大小可以算出实际IOPS和吞吐。svctm单次IO服务时间仅对机械盘有参考意义SSD环境下基本可以忽略。vmstat里的wa列是CPU等待IO时间的比例也是快速判断磁盘是否拖后腿的口径。当wa持续高于30%~40%且load average同时偏高时IO瓶颈基本实锤。还要区分“写放大”和“读放大”。数据库里最常见的写放大场景是大量update/delete导致页被反复标记为脏页checkpoint时统一刷盘最典型的读放大场景是索引设计不良导致大量无效读取和回表。出现IO问题后先问自己是读的IO压力大还是写的IO压力大用iostat的读写IOPS和吞吐就能区分这决定了优化方向完全不同。4.3 最常见的IO瓶颈场景checkpoint和WALPostgreSQL的主写路径是事务提交时WAL预写日志必须写到磁盘数据页的修改允许延后落实。因此WAL写入是每一次commit都要发生的同步IO直接决定事务提交延迟。如果所在磁盘fsync性能差commit延迟就会直接变高并表现为应用侧大量“等待commit”。和WAL相关的调优点有几个。wal_buffers是WAL日志的内存缓冲默认值一般按shared_buffers的比例计算通常不需要手动调得过。更关键的是WAL的可靠性配置Linux下常见默认是fdatasync如果你的业务能接受一定的安全性折中可以对配置做评估但不要盲目改动。这个决策涉及数据安全边界必须结合业务对数据丢失的容忍度来定。更常见的场景是checkpoint频繁触发。假设你的库每分钟生成的WAL量比较大但max_wal_size只有1GB那么过一会儿checkpoint就会因为WAL满而被迫触发而不是按checkpoint_timeout的5分钟定时触发。从监控曲线看每分钟都会出现一个刷盘峰值IO的await和util同步抬升。把max_wal_size从1GB调到4GB、8GB之后checkpoint被强制触发的频率显著下降IO尖峰也能被抹平不少。还有一个容易被忽略的点临时文件溢出。work_mem设置过小排序、哈希连接放不进内存就会创建临时文件读写全部落到磁盘。这类IO问题从数据库侧看不到明显的checkpoint异常但iostat里的写IO或系统临时目录的读写在持续上涨。定位办法是用pg_stat_database里的temp_files和temp_bytes字段去对比。理解了这一层你就会明白IO问题的排查不能只看IO——它往往是内存、SQL、配置三者交叉作用的结果。5. 监控落地的最后一公里告警阈值与优化闭环5.1 一套够用的监控采集脚本聊了这么多指标核心还是看怎么落地。在没有完整监控平台的情况下我习惯用一套极简的shell脚本加cron先把监控跑起来。下面是个简化的采集脚本按60秒一轮输出连接数、活跃连接、负载均值、IO状态和数据库后台写指标的关键值#!/bin/bash PGUSERmonitor PGDATABASEpostgres export PGPASSWORDyour_password ts$(date %Y-%m-%d %H:%M:%S) conn_total$(psql -U $PGUSER -d $PGDATABASE -tA -c SELECT count(*) FROM pg_stat_activity;) conn_active$(psql -U $PGUSER -d $PGDATABASE -tA -c SELECT count(*) FROM pg_stat_activity WHERE stateactive;) load$(uptime | grep -oP load average: \K.*) checkpoints_timed$(psql -U $PGUSER -d $PGDATABASE -tA -c SELECT checkpoints_timed FROM pg_stat_bgwriter;) checkpoints_req$(psql -U $PGUSER -d $PGDATABASE -tA -c SELECT checkpoints_req FROM pg_stat_bgwriter;) buffers_checkpoint$(psql -U $PGUSER -d $PGDATABASE -tA -c SELECT buffers_checkpoint FROM pg_stat_bgwriter;) echo $ts conn_total$conn_total conn_active$conn_active load$load ckpt_timed$checkpoints_timed ckpt_req$checkpoints_req ckpt_buffers$buffers_checkpoint /var/log/pg_monitor.log采集时要创建一个专门的监控账号。不要直接用postgres超级用户跑采集脚本一方面安全另一方面避免监控查询本身抢占连接资源。监控账号权限可以用pg_read_all_stats角色来赋予这是PostgreSQL 10之后提供的内置角色读取统计视图足够用。注意脚本落盘后配合cron定时执行同时要保证采集数据期间不叠加额外的psql进程数量。如果你有几十套实例建议还是尽快迁移到Prometheus体系文本日志只适合起步阶段。5.2 告警阈值怎么定告警阈值没有一个放之四海的标准但有些经验值可以当起点。连接数建议双阈值。使用率达到70%发提示属于关注级别达到85%发告警需要人工介入排查90%以上必须立刻响应。为什么不是100%才告警因为PostgreSQL连接达到max_connections后新连接会直接收到“too many clients already”错误业务可能瞬间大面积不可用。等到100%再处理损失已经造成了。负载告警load average超过CPU核数且连续15分钟不回落属于高负载危险信号。这里要把1分钟值和15分钟值拆开看如果在持续爬升优先查慢查询和等待事件如果一直是高位优先查连接数和锁。IO告警iostat的%util超过80%机械盘重点关注SSD/NVMe可以放宽到90%。同时要盯await和数据库侧的checkpoint次数。checkpoints_req占checkpoint总数比例超过20%~30%就值得去看max_wal_size和WAL增长情况。还有一个容易被忽略的告警项空闲事务占比。当idle in transaction连接数占总连接数的比例超过10%就要查一查业务代码里的事务管理是否规范。这个指标比连接数本身更早暴露应用侧的坏味道。5.3 从监控指标反推优化方向监控的目的是定位问题定位到问题就要推动优化。我把监控优化闭环拆成三个方向来做。连接数方向连接数高、空闲事务多先看连接池和应用事务管理。该加PgBouncer的加PgBouncer该加事务超时的加超时该修代码的修代码核心是减少连接占用和不必要的事务悬挂。负载方向等待事件集中在某个type就按type对症下药。DataFileRead对应的物理读压力大优先检查shared_buffers和索引Lock对应的锁等待优先处理被阻塞的会话和业务事务串行化问题ClientRead占比高说明客户端消费速度已经跟不上数据库了优先排查应用侧。IO方向checkpoint刷盘占比高调大checkpoint_timeout和max_wal_size配合观察IO峰值是否被摊平WAL写入慢关注磁盘本身的fsync性能和WAL所在目录的IO隔离临时文件频繁落盘把work_mem调大或优化SQL的排序和哈希连接。完整走完这三步你会慢慢形成一种条件反射看到连接数告警不再急着加连接数看到负载高不再急着加CPU看到IO高不再急着换磁盘。而是先回答“这条链路堵在哪一环”再决定动哪个配置或者改哪段代码。最后说一句个人这几年比较深的体会PostgreSQL的监控本质上是把系统性能问题翻译成数据指标、再翻译回决策的循环。指标本身不会直接给你答案但它能让你在凌晨突发故障时少走弯路、少靠猜。连接数、负载、IO这三个基础维度是我认为性价比最高的起点。先把它们吃透比装再多花哨的监控工具都实在。