PostgreSQL 运维要点:安装配置、备份与连接排障
本文目录
应用侧连接池容量与进程连接预算,另见Node.js 服务优雅关停与数据库连接池。这篇换到桌子另一边,讲数据库服务器本身的运维:装好后哪些参数必须核对、账号权限怎么收口、备份怎么做、VACUUM 为什么不能指望默认值、连接出问题怎么定位。视角是拿到一台新数据库主机到把它稳定跑起来,每一步给出命令和判断标准。
安装与初始化:先过 pg_hba.conf 这一关
安装建议直接用发行版仓库(以 Debian/Ubuntu 生态为例,其他发行版思路一致),装完第一条要改的配置就是认证规则文件 pg_hba.conf——它决定"谁能连、怎么验证",默认配置里有几条对生产不安全的规则:
| 认证方式 | 验证机制 | 位置 | 建议 |
|---|---|---|---|
| peer | 操作系统用户同名即放行 | 仅本地 | 本机管理用,别对网络监听开放 |
| trust | 不验证,直接放行 | — | 生产绝对禁止,测试机也要删 |
| md5 | 口令的 md5 挑战应答 | 网络 | 有中间人重放风险,被 scram 取代 |
| scram-sha-256 | 口令 + 随机盐挑战 | 网络 | 当前推荐的默认 |
| cert | 客户端 TLS 证书 | 网络 | 高安全场景,需要证书发放体系 |
sudo apt install postgresql-16 # 版本号跟随发行版仓库
sudo -u postgres psql -c "ALTER USER postgres PASSWORD :'pw';" # 管理员先设口令
systemctl status postgresql@16-main # 确认实例服务状态
pg_hba.conf 的每行规则是 类型 数据库 用户 地址 认证方式,从上到下首条匹配生效。一份收敛后的生产骨架:
# /etc/postgresql/16/main/pg_hba.conf(地址段为文档示例,按实际网段替换)
# 本机管理:peer
local all postgres peer
# 应用网段:强制 scram + 要求 TLS
hostssl all blog_app 10.0.0.0/8 scram-sha-256
# 运维跳板网段:scram
hostssl all ops_rw 10.0.0.0/8 scram-sha-256
# 拒绝其余一切(显式兜底,防止上方规则被误删后裸奔)
host all all 0.0.0.0/0 reject
配套动作:ssl = on 且证书就位(hostssl 才会生效,host 行是允许明文的);改完 pg_hba.conf 用 pg_ctl reload 生效,规则错误是"应用突然全连不上"的头号自伤来源,改前先 pg_hba_file_rules 视图自查语法。
postgresql.conf 关键参数:内存不是越大越好
PG 的内存结构决定了参数之间是联动的,不是各自独立的旋钮:
| 参数 | 默认值 | 作用 | 调整依据 |
|---|---|---|---|
| max_connections | 100 | 实例总连接上限 | 按应用侧预算反推,见前文连接池文章 |
| shared_buffers | 128MB | 共享缓冲池(数据页缓存) | 常见起点是机器内存的 25%,不是越大越好 |
| work_mem | 4MB | 每个排序/哈希节点的工作内存 | 见下文乘法警告 |
| effective_cache_size | 4GB | 给优化器的"内存可用量"估计 | 设为可用内存的一半左右,它不分配内存 |
| wal_level | replica | WAL 记录详细程度 | 要做 PITR/复制至少 replica |
| idle_in_transaction_session_timeout | 0(不限制) | 空闲事务超时 | 生产建议设,见连接排障一节 |
work_mem 的乘法警告是排障高频考点:它不是每连接的限制,而是每个查询里每个排序、哈希、聚合节点的单块额度。一条查询三个排序节点就是 3 份 work_mem,100 个连接同时跑复杂查询,理论峰值是两者相乘。盲目把 work_mem 调到 1GB,一次并发高峰就能把 32GB 内存的机器推进 swap。
-- 观察排序/哈希有没有溢出到磁盘:溢出说明 work_mem 不够(但先改查询再改内存)
SELECT query, calls, temp_blks_written
FROM pg_stat_statements
ORDER BY temp_blks_written DESC LIMIT 5;
调参次序:先 ALTER SYSTEM SET 单参数 + 重载(可回退、有记录),重启级参数(如 max_connections)安排窗口改。改完后 SHOW 确认生效值,监控对比前后内存与延迟,不要一把梭全量改完再观察——出问题都不知道是哪个参数。
角色与权限:只读账号和最小权限
应用的账号不该是超级用户,这是把"一条注入"的影响面从"整台数据库"缩到"几张表"的机制。标准动作:每库一个专用角色,只授该库所需对象:
-- 只读角色:报表、运维查询、误操作护栏
CREATE ROLE blog_ro WITH LOGIN PASSWORD :'pw';
GRANT CONNECT ON DATABASE blog TO blog_ro;
GRANT USAGE ON SCHEMA public TO blog_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO blog_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO blog_ro; -- 未来新建的表自动可读
-- 应用角色:仅业务 schema,收回危险默认权限
CREATE ROLE blog_app WITH LOGIN PASSWORD :'pw';
REVOKE CREATE ON SCHEMA public FROM PUBLIC; -- 阻止任意角色建对象污染 schema
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO blog_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO blog_app; -- 序列要单独授
权限的验收不是"应用能跑",而是反向用 SQL 审计:
-- 列出角色在某库能做的敏感操作面
SELECT grantee, privilege_type, table_schema, table_name
FROM information_schema.role_table_grants
WHERE grantee IN ('blog_app', 'blog_ro')
ORDER BY grantee, table_name;
另外三条纪律:口令用 ALTER ROLE ... PASSWORD 配合变量注入而不是历史记录里留明文;离岗即 ALTER ROLE ex_employee NOLOGIN(吊销而非删除,保留审计线索);迁移工具账号临时提权、用完即收,"图方便一直用超级用户连迁移工具"是数据误删事故的固定开场。
逻辑备份:pg_dump 的格式选择
逻辑备份的四个格式(-F 参数)决定后续怎么用,选择有明确依据:
| 格式 | 参数 | 特点 | 适用 |
|---|---|---|---|
| 纯文本 | -Fp | SQL 文本,psql 直接吃,可用 git/diff 管理 | 小库、需要人读 |
| 自定义 | -Fc | 压缩 + 目录索引,支持 -j 并行恢复、选择性恢复 | 日常单库备份 |
| 目录 | -Fd | 多文件目录,支持备份并行 -j | 大库,备份也要并行 |
| tar | -Ft | tar 打包(无压缩) | 特殊交付需要 |
# 单库备份:自定义格式 + 并行恢复的基础
pg_dump -Fc -d blog -f /backup/blog_$(date +%F).dump
# 大库用目录格式:备份阶段就并行(前提是并行会话数,-j 数值)
pg_dump -Fd -j 4 -d blog -f /backup/blog_$(date +%F).dir
# 表级恢复:从自定义格式里只捞一张表(这就是选 -Fc 的理由)
pg_restore -d blog_restore --table=articles blog_2026-10-22.dump
# 库级全量恢复:-C 顺带建库,-j 并行导入
pg_restore -d postgres -C -j 4 /backup/blog_2026-10-22.dump
库级与表级的取用形态不同:库级备份默认不包含全局对象(角色、表空间),要 pg_dumpall --globals-only 单独备,恢复演练时这条忘掉就是"角色不存在、应用起不来"。备份的验证(pg_restore --list、定期恢复到隔离环境)在备份与恢复指南里单独展开,这里不重复。
连续归档与 PITR:把 RPO 从"天"压到"分钟"
pg_dump 只能回到备份时刻。要做到"恢复到 10:23:45 那一刻"(PITR,point-in-time recovery),需要"基础备份 + WAL 连续归档"的组合,思路一句话:定期做物理基础备份,期间及之后的每次变更(WAL 日志)持续归档;恢复时先摆数据文件,再把 WAL 重放到指定时刻。
-- 前置参数(改后重启):至少 replica 级别才记录完整 WAL
ALTER SYSTEM SET wal_level = 'replica';
ALTER SYSTEM SET archive_mode = 'on';
ALTER SYSTEM SET archive_command =
'test ! -f /archive/%f && cp %p /archive/%f'; -- 归档命令,失败必须返回非零
# 基础备份:物理拷贝数据目录 + 记录 WAL 位点
pg_basebackup -D /backup/base/$(date +%F) -X stream -P \
--max-rate=32768 -h db.internal -U backup_user
# 恢复到目标时刻:数据目录摆好后,配置恢复目标再启动
# (示意,参数在 recovery 配置 / restore_command 中给出)
# restore_command = 'cp /archive/%f %p'
# recovery_target_time = '2026-10-22 10:23:45+08'
# recovery_target_action = 'promote'
三个必须盯的点:archive_command 失败时 PG 会重试并不推进 WAL,归档目录磁盘满是隐性的库停写事故,必须监控归档延迟与目录水位;恢复后实例是新的时间线(timeline),旧时间线的 standby 与备份策略要同步处理;PITR 恢复要停在"事故动作之前",目标时刻靠应用日志反查,这就是为什么日志与数据库要有时钟一致性(NTP)。
VACUUM:MVCC 的代价要有人还
PostgreSQL 的 MVCC 实现是"更新=插入新行 + 旧行标记删除",旧行(死元组)不立刻清磁盘,由 VACUUM 回收。后果是两条生产铁律:
- 删除不还磁盘:
DELETE FROM t后表文件大小不变,只是死元组可复用。以为删了数据就该释放空间,是 PG 新手的经典预期偏差。 - 长事务会卡死回收:VACUUM 不能清掉"比最老事务还新"的死元组。一条挂着的
idle in transaction事务几小时,全表膨胀(bloat)就来了。
-- 死元组与表膨胀情况的快速体检
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup*100.0/NULLIF(n_live_tup+n_dead_tup,0)) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;
-- 最老的事务:膨胀排查第一眼
SELECT pid, state, now() - xact_start AS xact_age,
now() - state_change AS state_age, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start LIMIT 5;
autovacuum 的默认参数在大表上偏保守(按百分比触发,大表要积压 20% 死元组才触发),高频更新表要单独调:
| 参数 | 默认 | 作用 | 实践 |
|---|---|---|---|
| autovacuum_vacuum_scale_factor | 0.2 | 死元组占表比例阈值 | 大表按表级设小(如 0.02) |
| autovacuum_vacuum_threshold | 50 | 死元组绝对数阈值 | 与上条相加生效 |
| autovacuum_analyze_scale_factor | 0.1 | 统计信息更新阈值 | 写入大的表同样按表级调 |
-- 表级覆盖示例:高频更新的表更积极地回收(按表决策,不要全局调小)
ALTER TABLE sessions SET (autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 1000);
VACUUM 与 ANALYZE 是两件事:前者回收死元组,后者刷新统计信息(优化器靠它选执行计划)。统计信息过期导致的"计划突然变烂",用 ANALYZE 解决;普通 VACUUM 不锁表不还磁盘,VACUUM FULL 会还磁盘但持排他锁、基本只能停机窗口做。
连接排障:从报错到根因的三种典型
症状一:FATAL: too many connections。先看是谁占的,而不是先调大 max_connections:
SELECT usename, state, count(*) AS conns
FROM pg_stat_activity
GROUP BY 1, 2 ORDER BY conns DESC;
典型结论是某个应用实例没复用连接(泄漏)或某个会话数异常的进程在疯狂建连。max_connections 上调是最后手段,它会放大每个连接的内存摊销,正确方向是核对应用侧池化(预算方法见连接池那篇)。
症状二:孤儿/挂死会话。idle in transaction 的会话持有快照卡 VACUUM、还可能持有锁:
-- 找出空闲事务并处理(kill 的是数据库侧会话,进程还活着只是断连)
SELECT pid, usename, now()-state_change AS idle_for, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < now() - interval '15 minutes';
-- 给未来兜底:空闲事务 15 分钟自动断开(对应用是连接错误,配合应用重试与事务纪律)
ALTER SYSTEM SET idle_in_transaction_session_timeout = '15min';
SELECT pg_reload_conf();
症状三:连接泄漏定位。连接数只增不减,重启应用立刻好转——用 application_name 和 backend_start 圈出嫌疑进程:
-- 按 application_name 分组看连接年龄分布:正常的池化连接应该年轻且稳定
SELECT application_name, client_addr, count(*) AS conns,
min(backend_start) AS oldest_conn
FROM pg_stat_activity WHERE pid <> pg_backend_pid()
GROUP BY 1, 2 ORDER BY conns DESC;
同一 client_addr + application_name 的连接数远超该进程池上限、且 oldest_conn 越来越老,基本可以定罪:应用有代码路径拿了连接没还(典型是异常分支没进 finally)。杀会话只是止血,修复在应用层。
监控查询:pg_stat_activity 与 pg_stat_statements
日常巡检两个视图覆盖大部分需求。pg_stat_activity 是"现在谁在干什么",上文排障已多次使用;pg_stat_statements 是"历史上哪条 SQL 最费资源",需要先装:
-- 一次性开启:预载入库 + 建扩展(需要重启一次才生效 preload)
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
-- 重启后:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 巡检三连:最耗时 / 最耗 IO / 临时溢出
SELECT round(total_exec_time::numeric,1) AS ms, calls, left(query, 80) AS q
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
SELECT calls, round((shared_blks_read*8)/1024.0) AS read_mb, left(query, 80) AS q
FROM pg_stat_statements ORDER BY shared_blks_read DESC LIMIT 5;
SELECT round(temp_blks_written*8/1024.0) AS spill_mb, left(query, 80) AS q
FROM pg_stat_statements WHERE temp_blks_written > 0
ORDER BY temp_blks_written DESC LIMIT 5;
使用注意:pg_stat_statements 按"归一化后的语句文本"聚合(常量替换成参数记号),查询结果里出现的 top 语句要结合 calls 看——平均单次耗时的粗算(total_exec_time / calls)比总量更能定位坏查询;视图里的统计是累计值,pg_stat_statements_reset() 重置后要重新积累;高流量库的采样窗口内语句种类极多,先用它圈范围再用 EXPLAIN ANALYZE 单点深挖。
升级路径:pg_upgrade 与逻辑复制
大版本升级(14 → 16)是必然要面对的,两条路线取舍:
| 维度 | pg_upgrade | 逻辑复制(初始同步 + 追平) |
|---|---|---|
| 停机窗口 | 分钟级(链接/硬链接模式) | 接近零(追平后秒级切换) |
| 版本跨度 | 支持跨多个大版本 | 新旧可各跑各自版本 |
| 回滚 | 困难(老集群保留可回退) | 容易(旧集群降级保留) |
| 机器与复杂度 | 单机原地或同机并跑 | 需要新旧双集群与网络 |
# pg_upgrade 的干跑:先做 --check,能提前暴露所有阻断项
/usr/lib/postgresql/16/bin/pg_upgrade \
--old-datadir /var/lib/postgresql/14/main \
--new-datadir /var/lib/postgresql/16/main \
--old-bindir /usr/lib/postgresql/14/bin \
--new-bindir /usr/lib/postgresql/16/bin --check
升级纪律:先 pg_upgrade --check 干跑,处理所有报错再实跑;小版本升级走包管理器(apt upgrade)即可;扩展(尤其 pg_stat_statements)新旧版本都要装;升级前后的全量备份与 pg_upgrade --check 是同一批"变更前检查"清单的一部分,新版本上线后 pg_stat_statements 统计清零,巡检基线要重建。
注意事项与边界
- 本文命令以 PG 16 前后的长期稳定接口为背景,参数名和行为以你所用大版本的官方文档为准;小版本间也可能有行为修正。
ALTER SYSTEM写的参数在postgresql.auto.conf里,优先级高于 postgresql.conf——排查"参数不生效"时两个文件都要看,别只盯主配置。- pg_hba.conf 与 postgresql.conf 的修改即使 reload 生效,也要纳入变更记录:连接类故障的复盘第一步是回查这两个文件的最近变更。
- 逻辑备份窗口随库增长非线性恶化,TB 级库的既定路线是物理备份 + 归档,pg_dump 留给中小库和跨版本交付。
- autovacuum 参数调优后要观察膨胀率指标回归,没有基线就调参等于盲调;
pgstattuple扩展可以精确测膨胀但开销大,用于抽查不是巡检。 - 杀
pg_stat_activity里的会话前确认它在做什么:pg_terminate_backend对普通语句安全,但杀掉正在跑长写的语句意味着回滚,回滚本身可能比让它跑完更久。 - 示例中的网段与主机名均为文档占位(10.0.0.0/8、db.internal),替换为实际环境时注意 pg_hba.conf 的掩码写法,掩码写错等于防线写错。
总结
PostgreSQL 服务器侧运维的主线是四件事:入口收口(scram + hostssl + 最小权限账号,pg_hba.conf 从第一行管起)、参数联动(work_mem 的乘法效应、shared_buffers 与 effective_cache_size 各司其职)、备份双轨(pg_dump 做逻辑粒度,基础备份 + WAL 归档做 PITR,归档目录必须被监控)、日常体检(死元组与长事务防膨胀,pg_stat_activity 看当下、pg_stat_statements 看积累)。连接类故障的第一反应永远是先查 pg_stat_activity 定位占用者,而不是调大上限——这也是这篇与连接池那篇的分工:一个管应用怎么要,一个管服务端怎么给。
参考
- PostgreSQL: Client Authentication(pg_hba.conf 与认证方法)
- PostgreSQL: Resource Consumption(shared_buffers / work_mem)
- PostgreSQL: pg_dump 手册
- PostgreSQL: Continuous Archiving and Point-in-Time Recovery
- PostgreSQL: Routine Vacuuming(VACUUM 与 autovacuum)
- PostgreSQL: The Statistics Collector(pg_stat_activity)
- PostgreSQL: pg_stat_statements
- PostgreSQL: pg_upgrade 手册
- Wikipedia: Multiversion concurrency control
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。