ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PostgreSQL维护实践指南:从MVCC原理到十大体检清单

PostgreSQL维护实践指南:从MVCC原理到十大体检清单 1. 为什么PostgreSQL也需要健身先搞清楚维护的本质干了这么多年数据库我遇到过太多类似的求助数据库刚上线的时候跑得飞快业务量也不大运维的同学基本处在装完就忘的状态。可一旦跑上几个月查询开始变慢磁盘空间悄悄被吃掉某些原本几十毫秒的接口硬生生拖到几秒。拿过执行计划一看SQL写得没问题索引也在问题就出在数据库本身亚健康了。PostgreSQL 和很多关系型数据库不一样的地方在于它靠 MVCC多版本并发控制来保证事务隔离和读写并发每一次 UPDATE 在物理层面上都是一次 INSERT 加一次标记删除。这个机制让读操作不会被写操作阻塞是 PostgreSQL 并发能力的根基但它也给数据库留下了持续的代谢废物。如果不做维护这些废物会让表膨胀、索引失效、统计信息失真直到把数据库拖垮。所谓PostgreSQL Fitness说白了就是一套针对这类代谢机制的、可持续执行的维护习惯。这十个实践每一件都不是新鲜事难的是把它们串成一套可以在日常运维中稳定落地的动作。接下来我会按自己的实操习惯逐条展开讲包括为什么要这样做、用什么命令、以及我踩过的坑。1.1 MVCC 机制带来的先天代谢废物很多人第一次接触dead tuple这个词是在 VACUUM 的日志里但没太当回事。我换个说法你就有体感了假设你有一张一亿行的订单表每天有 8% 的行会被更新那每天会产生大约八百万个旧版本行。PostgreSQL 不会立即清理它们因为可能有正在运行的长事务还需要读取旧快照。几天不清理表里就可能堆积了几千万条已失效但仍然占着物理空间的旧数据。查询要扫描的数据块自然就多了。本来走索引可以只读 10 个页面现在因为页面里塞满了死数据同样的行要读 15 个甚至 20 个页面索引本身也可能因为同样原因膨胀。更隐蔽的是统计信息比如行数估算也会因为这些死行变得不准确优化器可能因此选错执行计划。这就是为什么看起来没毛病却越来越慢。所以任何一个 PostgreSQL 维护体系第一个要解决的核心问题就是如何及时清理这些代谢废物并让统计信息保持新鲜。1.2 默认参数只保证能跑不保证健康每次我在新环境里看配置文件都会提醒自己一句话PostgreSQL 的默认参数目标是开箱即用、不惹麻烦不是为你的业务负载定制的最优配置。这就像我刚提车时的原厂机油能保证发动机运转但长期激烈驾驶就得换更合适的机油。举个最常见的例子autovacuum_vacuum_scale_factor 默认值是 0.2意味着表中发生变化的行数超过总行数的 20% 时autovacuum 才会对这张表做一次 VACUUM。对一亿行的表来说这个触发阈值是多少是两千万行。等它积累到两千万行死数据再动手清理膨胀早就发生了。类似这种默认值看着合理一放大就失控的参数后面我会逐个处理。理解这两点你再看后面十条维护实践就会明白每个动作都是在回答同一个问题如何让 PostgreSQL 在长期运行中保持干净的存储、准确的统计信息和稳定的执行性能。2. 维护第一课让 Autovacuum 成为你可靠的自动化防线Autovacuum 是 PostgreSQL 内置的环卫工人它由后台进程持续监控所有数据库的活动按触发的阈值自动执行 VACUUM 和 ANALYZE。我对团队的要求很明确绝大多数情况下我们要让 autovacuum 能干完所有的常规清理活人就别轻易手动掺和。因为它一旦配置正确是持续、自动、分批次运行的比人想起来才去执行一次要稳定得多。但配置正确这三个字恰恰是很多人没做到的部分。2.1 Autovacuum 核心参数把触发阈值调到和表规模匹配这是我在生产环境里比较常用的一套调整逻辑先看参数对照表参数默认值建议值作用autovacuum_vacuum_scale_factor0.20.05VACUUM 的按表行数触发比例autovacuum_vacuum_threshold5050VACUUM 的最小触发行数autovacuum_analyze_scale_factor0.10.05ANALYZE 的按表行数触发比例autovacuum_analyze_threshold5050ANALYZE 的最小触发行数autovacuum_naptime60s30s每轮巡检间隔autovacuum_max_workers3取决于并发最多同时几个 worker 跑 VACUUMautovacuum_vacuum_cost_limit200按 IO 能力autovacuum 的资源消耗上限触发阈值有一个计算公式实际触发行数 autovacuum_vacuum_threshold autovacuum_vacuum_scale_factor * pg_class.reltuples。也就是说一亿行的表默认配置要累计两千万行变化才触发我之前就是这么算出来的这也是我坚信必须调参的原因。我一般的做法是先全局把所有库都调到 scale_factor0.05 这种水平再对特别大的核心表单独做局部配置。比如订单表我可能会在表级别设置ALTER TABLE orders SET (autovacuum_vacuum_scale_factor 0.01); ALTER TABLE orders SET (autovacuum_vacuum_threshold 10000); ALTER TABLE orders SET (autovacuum_analyze_scale_factor 0.01); ALTER TABLE orders SET (autovacuum_analyze_threshold 10000);表级别的参数会覆盖全局配置这样你可以让大表更勤快地清理而不至于让小表也频繁触发 VACUUM 浪费资源。还有一个容易被忽略的参数是 autovacuum_vacuum_cost_limit。它是有代价模型的limit 太小会导致 worker 干一点活就休息cost_delay大表的 VACUUM 拖很久都跑不完。我见过 SSD 环境上默认 cost_limit200 让一个大表的 VACUUM 跑了快 40 分钟的案例。如果你的存储是 SSD可以适当提高到 1000 甚至 2000配合观察系统 IO别让清理本身成为瓶颈。2.2 手动 VACUUM/ANALYZE 的正确姿势哪些场景下别指望自动autovacuum 配置得再好也有它反应慢半拍的时候。下面这几类场景我会手动介入大批量导入或批量 UPDATE 之后比如 ETL 任务跑完表在短时间内新增了大量行autovacuum 下次巡检发现阈值达到可能已经是几十秒甚至几分钟后了手动执行一次能立刻恢复统计信息的准确性。从恢复或备份环境拉起来的新库统计信息没有经过充分分析。在 autovacuum 因为长时间执行而被取消某些版本的 ldconfig 限制之后。手动操作时我的习惯是加上 VERBOSE 和 ANALYZEVACUUM (VERBOSE, ANALYZE) orders;VERBOSE 会在服务端日志里输出这次清理的详细统计包括移除了多少死元组、释放了多少空间。ANALYZE 会顺带更新优化器需要的统计信息一次做完比分开两次跑更省事。这里必须给新手提个醒VACUUM FULL 不是日常维护命令。它会把表重写一遍期间要拿到 ACCESS EXCLUSIVE 锁业务会被卡住。日常清理膨胀应该靠普通的 VACUUM 和后续的自动回收页真正需要 VACUUM FULL 的是表膨胀已经非常严重、普通 VACUUM 无法把空间还给操作系统的情况。这种情况你还要评估时间窗口或者考虑用 pg_repack 这类在线重写工具。3. 索引与统计信息查询变慢的两个隐形推手第 3 和第 4 个实践我想放在一起讲因为它们经常同时发威索引膨胀导致本应走索引的查询退化成了全表扫描统计信息失真导致优化器选了一个烂计划。两个问题单独拿出任何一个都好处理混在一起时排查却很绕。3.1 索引膨胀它不是坏了而是胖了PostgreSQL 的索引也会产生膨胀。索引页面里被更新的条目同样会留下旧版本空间加上一些特殊场景比如频繁的并发更新热点字段索引的实际大小可能远大于它应该有的样子。索引膨胀的代价不像表膨胀那样直观但它会让扫描的叶节点变多内存缓存命中率下降最终拖慢查询。我用比较笨但可靠的方法做索引体检先用 pgstattuple 扩展看每一层索引页面的 dead tuple 情况或者直接对比索引的逻辑大小和它重建之后的大小。重建索引用REINDEX INDEX idx_orders_order_time; REINDEX TABLE orders;在 PostgreSQL 12 之后REINDEX 支持 CONCURRENTLY 模式不会长时间锁表是生产环境的首选REINDEX INDEX CONCURRENTLY idx_orders_order_time;但注意CONCURRENTLY 不能在一个事务块里执行而且它需要更多的临时工作内存。重建后索引文件会缩小查询性能通常能立刻感觉到改善。我见过 8GB 的索引重建后变成 1.8GB 的当时那个系统索引扫描慢得离谱重建完直接恢复正常全程业务无感。什么频率重建合适别搞定时任务每周无脑重建。我的经验是先通过监控发现膨胀率超过 20%-30%或者通过 pg_stat_user_indexes.idx_scan 观察到索引有效性明显下降时再触发重建。为了自动化也有人写脚本定期收集 pgstatindex 数据超过阈值自动提交 REINDEX CONCURRENTLY但这要注意错开业务高峰。3.2 统计信息过期优化器做了错误的选择数据库不背锅PostgreSQL 的查询计划器依赖 pg_statistic 里的统计数据这些数据需要靠 ANALYZE 来更新。autovacuum 在触发 VACUUM 的时候如果发现分析阈值也到了会顺带做 ANALYZE。大表更新频繁统计信息就容易过期结果就是优化器以为目标表只有 1000 万行实际已经 3000 万行于是选了一个当时最优、现在很烂的执行计划。排查时我喜欢直接看这张表SELECT relname, seq_scan, idx_scan, last_analyze, last_autoanalyze FROM pg_stat_user_tables WHERE relname IN (orders, users, order_items);如果 last_autoanalyze 距今已经过了一个大业务的峰值窗口或者 seq_scan 对一张大表的比例异常升高我基本会先手动 ANALYZE 一次再重新看执行计划ANALYZE orders;多说一句default_statistics_target 这个参数默认 100表示每个字段最多采样 100 个值。对于超高基数的字段比如订单金额、时间戳100 个桶不够精细容易导致等值估算偏差。从 PostgreSQL 13 开始可以把 default_statistics_target 调成 200 或 300代价是 ANALYZE 的时间和内存稍微增加但能显著降低估算错误。也可以对特定大表的核心字段单独修改ALTER TABLE orders ALTER COLUMN user_id SET STATISTICS 1000;在带关联条件的多字段统计上扩展统计信息CREATE STATISTICS可以捕捉字段间的相关性这个进阶做法很适合在数据仓库类负载上投入。4. WAL 与控制点维护数据库的心跳节奏前面讲的是表和索引这一节进入更底层的机制预写日志WAL和检查点。我遇到过的最经典的数据库磁盘被写满导致只读事故根子就在 WAL 积累上。这部分的维护实践核心是两件事确保 WAL 被及时清理或归档以及让检查点尽量平滑地执行。4.1 WAL 文件积累磁盘到底是怎么被写爆的PostgreSQL 把事务日志写在 pg_wal 目录里。这些日志文件是循环复用的正常情况下检查点之后的旧日志只要不再需要已刷新到数据文件就可以被回收重用。一旦有环节卡住文件就会不断堆积把磁盘写满。我在排查这类事故时按下面这个顺序一个环节一个环节地排除-- 1. 当前 WAL 位置和实际保留的 WAL 文件大小 SELECT pg_current_wal_lsn(), pg_wal_lsn_diff(pg_current_wal_lsn(), 0/0) AS wal_bytes; -- 2. 看有没有备库明显落后 SELECT client_addr, state, sent_lsn, replay_lsn, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag FROM pg_stat_replication; -- 3. 查归档状态看 archive_command 是不是一直失败 SELECT * FROM pg_stat_archiver;如果 pg_stat_archiver 里 failed_count 一直涨而 pg_stat_replication 没有明显延迟那问题基本就是归档目标不可用。常见的坑是归档脚本往 NFS 挂载点拷贝NFS 假死或者权限炸了archive_command 一直返回失败主库就认为日志还没归档成功不能回收于是一边重试一边堆积。这种场景下即使归档 Broker 恢复WAL 积压也需要时间消化应急时可以先调整 archive_command 到一个安全目录再逐步回收旧文件。除了归档失败还有一个常见源头是 max_wal_size 配得太大。它的作用是控制 WAL 增长到多少时触发一次由 WAL 增长驱动的检查点。默认 1GB 通常没问题但如果你把 max_wal_size 调到 16GB 又没有配套增加 checkpoint_timeout数据库要等 WAL 积累 16GB 才做检查点期间崩溃恢复时间会非常长而且一旦备库追不上保留的 WAL 就会迅速膨胀。4.2 检查点优化避免周期性 IO 尖峰检查点是 PostgreSQL 的记账动作它把脏页从共享缓冲区刷新到磁盘保证崩溃恢复时从检查点位置开始回放就可以了。但检查点触发时如果积压的脏页太多会瞬间产生大量写 IO这就是很多人眼熟的数据库每半小时卡一次的典型症状。控制检查点平滑度的关键参数有三个checkpoint_timeout默认 5 分钟检查点触发的最大时间间隔。max_wal_sizeWAL 达到多少触发基于 WAL 积累的检查点。checkpoint_completion_target默认 0.5表示检查点要在下一个检查点到来前的时间段的 50% 内完成。其中 checkpoint_completion_target 是最值得调的。把它从 0.5 调到 0.9把检查点的写压力从一次性爆发分摊到后续时间窗口里很多周期性 IO 尖峰的问题就能缓解checkpoint_timeout 15min max_wal_size 4GB checkpoint_completion_target 0.9调完之后我在生产环境里观察 pg_stat_bgwriter 的 checkpoints_timed 和 checkpoints_req 比例前者是按照 timeout 触发的后者是外部请求触发的。如果 checkpoints_req 占比偏高说明靠 WAL 增长触发得太频繁需要加大 max_wal_size 或调整 workload。检查点本身是好事越规律越好但频率和刷盘量的平衡才是调优重点。5. 备份与监控平时多下功夫战时才能少流血我经常跟人讲维护工作中备份和监控是最反人性的它们平时看不到收益只在出大事故时才体现价值。可如果真等事故来了再验证就晚了。所以这两个实践我排第 5 和第 6但它们的重要性在我心里排前三。5.1 分层备份策略与恢复演练关键是能恢复不是有备份PostgreSQL 的备份体系至少要包含三层层级工具/方式恢复粒度适用场景物理全量备份pg_basebackup 或存储快照整个集群灾难恢复快速拉起WAL 连续归档archive_command 归档任意时间点PITR误删除、误更新等 SQL 级事故逻辑备份pg_dump / pg_dumpall表级/库级跨版本迁移、单表恢复pg_basebackup 的常规写法pg_basebackup -h pg-primary -D /backup/base/$(date %F) -U backup_user -P --wal-methodstreamWAL 归档的配置核心就是 archive_commandarchive_mode on archive_command test ! -f /backup/wal/%f cp %p /backup/wal/%f在处理误删一张小表的恢复时逻辑备份非常方便。但真实生产事故里最高频的还是误 UPDATE/误 DROP 大量数据这时候靠 WAL 归档做 PITR 最有效。恢复时指定时间点recovery_target_time 2025-01-20 03:14:00 restore_command cp /backup/wal/%f %p关于备份我想多啰嗦几句踩过的坑。第一备份不是跑完脚本就结束了必须把恢复演练纳入定期计划。至少每个季度要挑一个晚上把备份拉到隔离环境里做一次完整恢复验证归档链是否连续、时间点是否可达。第二WAL 归档的保留策略要提前想好否则 30 天的归档会吃掉比你数据文件本身还大的空间。第三如果备份主机和主库在同一个机房断电或机房故障时备份也救不了你——地理冗余这件事虽然花钱但事故面前你会庆幸做过。5.2 监控体系从系统表体检到外部观测即使不引入 Prometheus 和 Grafana 这类外部工具PostgreSQL 系统视图本身就是一套相当强大的体检工具。我平时在终端上最常查这几张表-- 当前所有连接抓长时间 idle / 恶意连接 SELECT pid, usename, application_name, client_addr, state, wait_event_type, wait_event, backend_xid, age(now(), state_change) FROM pg_stat_activity WHERE state idle ORDER BY age(now(), state_change) DESC; -- 缓存命中率这个低了查询一定慢 SELECT relname, heap_blks_read, heap_blks_hit, round(heap_blks_hit::numeric / (heap_blks_hit heap_blks_read) * 100, 2) AS hit_ratio FROM pg_statio_user_tables WHERE (heap_blks_hit heap_blks_read) 0 ORDER BY (heap_blks_hit heap_blks_read) DESC LIMIT 20;在生产库上我强烈建议把 pg_stat_statements 扩展打开它是定位慢查询和锁问题的最佳起点CREATE EXTENSION pg_stat_statements;然后配置共享 preload 库等待实例重启后生效。之后你可以按总执行时间、平均执行时间、调用次数这几个维度的排序快速找到最需要优化的 SQL。再结合解释计划绝大多数性能问题都能定位到根因。外部监控方面Prometheus 加 postgres_exporter 是社区里常用的组合它导出的指标覆盖系统视图里的所有关键项包括复制延迟、事务 ID 回卷进度、autovacuum 活动等。对于数据库规模小、不想搭整套监控体系的团队至少也要有一个定时脚本把 pg_stat_activity 中的异常状态、pg_wal 目录大小、磁盘使用率这几个关键指标丢到告警平台里。6. 连接与会话、磁盘与膨胀容易被忽视的慢性病最后这几个实践平时不显山不露水但它们在业务高峰期最容易变成压垮数据库的最后一根稻草。我在第 7 到第 10 条实践里处理的都属于这种慢性病。6.1 连接管理塞车不是因为你路修得不够宽PostgreSQL 是进程模型每个连接对应一个后端进程内存占用和上下文切换开销都随连接数线性增长。max_connections 配得过大操作系统要管理的进程就多CPU 上下文切换和缓冲区争用都会恶化配得过小业务一冲量就报too many connections。正确的方向是用连接池把前端连接数压到百以内。像 PgBouncer 这种轻量级方案把数据库端的真实连接控制在 50 到 100前端应用随便开几百个连接都不会把库压垮。应用侧的连接池比如 Java 的 HikariCP也要控制上限别默认开 200。除了池化之外还有一类连接幽灵特别容易出问题应用层配置了错误的连接超时导致连接一直挂在 idle in transaction 状态占着后端进程还攥着锁不放。我在 pg_stat_activity 里见到过 600 多个 idle in transaction 的连接某张表的行锁被它们卡得死死的。这时候一个关键参数能救命idle_in_transaction_session_timeout 30000把它设成 30 秒或 60 秒事务超过这个时间还开着不提交后端进程就自动断开了。我在业务上线前就会设置好这比事后挨个 kill 连接有效得多。真要清理异常连接用这个脚本可以按指定条件终止SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state idle in transaction AND age(now(), state_change) interval 5 minutes;6.2 磁盘与膨胀专项体检搞清楚空间到底去哪了数据库磁盘被写满时报错五花八门有报could not write to file的有报No space left on device的还有干脆在恢复时出奇葩错误。所以我坚持把磁盘使用率的检查写进所有巡检脚本里同时对空间去了哪里做专项审计-- 看库级大小 SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY pg_database_size(datname) DESC; -- 看表级大小和膨胀 SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS total_size, pg_size_pretty(pg_relation_size(relid)) AS heap_size, pg_size_pretty(pg_relation_size(relid, main)) AS main_size FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 30;判断一张表是否真的膨胀除了看 total_size 和实际行数是否匹配另一个更精确的办法是用 pgstattupleSELECT * FROM pgstattuple(orders);这里的 dead_tuple_percent 如果超过 20%而且普通 VACUUM 后快照仍然很大那就得考虑重写表了。重写之前要确认磁盘剩余空间足够放下一份拷贝否则会真在写一半的时候把自己卡死。我经常提醒自己表膨胀不是一天形成的通常是因为长时间 autovacuum 没跟上所以把它做成趋势监控远比爆发后处理有意义。还有一个容易忽略的地方是系统目录膨胀。PostgreSQL 自身的系统表pg_attribute、pg_depend 等在你大量创建和删除表、索引之后也可能膨胀影响元数据读取性能。定期对 pg_catalog 做一次 VACUUM很多隐性慢查询能立刻改善。7. 把健身变成习惯我的月度与季度体检清单到这里十条实践讲完了。但比单次操作更重要的是形成一个可持续执行的节奏。我自己的习惯是分两个周期做月度巡检检查 autovacuum 相关日志看是否有长时间运行的 VACUUM 或 ANALYZE确认没有取消日志。通过 pg_stat_user_tables 找出 dead tuple 比例异常的表手动 VACUUM (VERBOSE, ANALYZE)。检查 pg_stat_replication 的复制延迟确定备库健康。检查 pg_wal 目录大小和归档成功率。扫描 pg_stat_activity清理 idle in transaction 和长期 saturate 状态的会话。季度巡检对所有膨胀率超过 30% 的大表执行 REINDEX CONCURRENTLY。检查统计信息新鲜度对核心表做一次 ANALYZE。做一次完整的备份恢复演练。复查共享缓冲区、work_mem、max_connections 等核心参数是否需要随业务变化调整。每次巡检不要怕暴露问题怕的是发现了问题没人处理。数据库维护的最大成本不是执行这些命令而是一直没人看、一直没人管的沉默期。我见过很多数据库事故根因拆开看都特别简单要么是 autovacuum 配置没跟上要么是索引膨胀没人发现要么是备份只备不练。这十条实践没有任何一条需要很高深的理论但把它们当成固定习惯坚持下来PostgreSQL 就能在业务眼里一直保持稳定、安静、不用管的状态。在数据库的世界里最好的运维成果往往就是没有事故。
返回列表