news 2026/9/29 3:10:28

PostgreSQL运维实战:故障排查、JSON索引优化与备份恢复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL运维实战:故障排查、JSON索引优化与备份恢复

写这篇博文的起因,是我这两年接了不少PG数据库的“救火”需求。2025年了,PostgreSQL在国内用得比前几年广得多,很多团队从其他数据库迁过来,功能上确实很顺手,但一到运维环节就露怯:服务半夜停了不知道先看什么,磁盘报错不知道怎么快速恢复,JSON字段明明用上了查询却慢得像全表扫描。这篇文章不打算讲高深原理,我就把自己在实际运维里沉淀下来的处理链路、常用命令和踩坑教训整理出来,按问题场景来写,希望能给正在维护PG实例的同行一些能直接落地的东西。

1. 服务突然停摆:先别急着重启,按这条链路排查

PG服务意外宕掉,是我遇到频率最高的一类故障,尤其在业务高峰期。很多人的第一反应是执行pg_ctl restart或者重启容器,我强烈建议先忍一下。未定位原因就重启,短时间确实恢复了,但没过几天大概率会再挂一次,而且可能挂在同一个点上。我的习惯是严格按“日志、资源、恢复、加固”四步来。

1.1 第一步:日志里往往已经写明了答案

PG的运行日志是排查停摆问题最直接的入口。不同安装方式日志位置不一样,最常见的路径是$PGDATA/log,这里$PGDATA一般对应/var/lib/postgresql/16/main或者/var/lib/pgsql/16/data。用Docker部署的实例,日志通常直接打到标准输出,可以通过docker logs查看。总体上记住一条:先翻日志再动手。

定位到日志文件之后,我会用下面这条命令看最近200行:

tail -n 200 $PGDATA/log/postgresql-*.log

常见几类日志特征,可以对照排查:

日志关键字大概率对应的原因
could not write block ... No space left on device磁盘满或inode耗尽
server process was terminated by signal 9内存不足触发OOM killer
PANIC: could not locate a valid checkpoint record数据文件损坏或磁盘故障
terminating connection due to administrator command有人执行了重启或主动kill
FATAL: terminating connection due to conflict with recovery主从切换或恢复中的冲突
out of shared memoryshared_buffers或连接内存配置不当

日志会给一个大概方向,但有时候会被日志轮转覆盖掉,或者错误信息比较笼统。这时候就要进入第二步,直接看操作系统层面的资源情况。

1.2 第二步:确认系统资源,排除OOM和磁盘耗尽

如果是内存不足导致的OOM,系统日志里通常会有线索,PG日志里则只留下孤零零的signal 9。我排查的时候会同步看三样东西:

# 查看磁盘剩余 df -h df -i # 查看内存 free -m # 查看系统日志中的OOM记录 dmesg -T | grep -i oom dmesg -T | grep -i postgres

df -i这一步很多人会漏掉。磁盘剩余空间明明很大,但inode用满了,PG一样写不进文件。我曾经遇到过/tmp分区inode耗尽导致数据库无法启动的情况,当时df -h看起来还有将近20G空间,折腾了好一会儿才发现是inode的问题。

内存这一块,PG的OOM通常是因为shared_buffers等参数设得过大,加上机器上还跑着Java应用或Redis这类内存大户,一旦物理内存吃紧,内核的OOM killer会把PostgreSQL主进程当成最值得回收的目标杀掉。之前有客户把 shared_buffers 直接设成16G,而机器只有32G内存,业务一上来,连同操作系统缓存一起超了,最后整机卡顿,PG主进程被强杀。后续我建议他们调整为8G,并给关键进程配置了cgroup的内存限制,问题才稳定住。

1.3 第三步:恢复启动之后,还需要做几件事

确认资源没大问题后,就可以启动数据库了。先看当前数据目录,再执行:

# 如果原来是正常关闭,直接启动 pg_ctl -D $PGDATA start # 如果启动失败,前台运行并把日志打出来 postgres -D $PGDATA

启动成功后,不要急着马上把业务切回主库。先验证几个点:

  • pg_isready -h 127.0.0.1 -p 5432返回accepting connections;
  • psql -U postgres -c "select 1"能正常执行;
  • 查看日志里有没有新增的报错,比如内存不够或者无法解析配置文件;
  • 如果有从库,检查流复制延迟是否正常。

如果启动失败,日志显示FATAL: lock file "postmaster.pid" already exists,说明postmaster.pid是残留文件。确认没有其他PG进程后,手动删除这个pid文件再启动。这里有句老话值得记住:不要反复重启一个起不来的数据库,看日志永远比盲目试启动更快。

恢复之后,还要做加固工作:为PG配置系统的自动重启策略(systemd或docker restart),但要加合理的启动退避,防止数据库反复崩溃重启把错误掩盖掉;同时把这次故障时间点和根因记入运维手册,下次再出现类似日志,能直接对应到解决方案。

2. JSON字段操作:从取值、展开到GIN索引优化

PostgreSQL的JSON支持是很多人从其他数据库迁移过来的重要原因。但实际运维中,我经常发现有人把PG当文本数据库用,JSON字段不做任何约束,查询全靠like,数据量一上去性能立刻崩。其实PG的jsonb类型加上一票内置函数,完全能做到像查普通表字段一样高效。

2.1 四个取值操作符的区别,一次讲透

先看一个简单场景。假设订单表里有一个items jsonb字段,存储了商品明细:

{ "order_no": "A1001", "user": { "id": 1001, "name": "张三" }, "items": [ {"product_id": 101, "price": 19.9, "quantity": 2}, {"product_id": 102, "price": 5.0, "quantity": 5} ] }

想取order_no,有几种写法:

SELECT items -> 'order_no' FROM orders; -- 返回 "A1001"(带双引号,属于jsonb类型) SELECT items ->> 'order_no' FROM orders; -- 返回 A1001(文本类型)

->和->>的区别是很多新手最先卡住的地方。->返回结果是JSONB类型,->>返回的是文本。测试时看起来差不多,但一旦把结果传给函数或者做条件判断,类型不匹配的坑就出来了。

对于嵌套结构,比如取user.name,可以用#>和#>>,这两个操作符接受一个路径数组:

SELECT items #>> '{user,name}' FROM orders; -- 返回 张三

我把四个操作符整理成一个表,方便收藏:

操作符用途返回类型示例
->取key值,按key取值jsonbitems -> 'order_no'
->>取key值,按key取文本textitems ->> 'order_no'
#>按路径取jsonbjsonbitems #> '{user,id}'
#>>按路径取文本textitems #>> '{user,id}'

记住“带>返回的是JSON,带>>返回的是文本”这个规律,大部分用法就通了。

2.2 批量展开JSON数组做统计:常用函数与写法

实际业务里最常用的场景是:JSON数组展开成一行行数据,然后参与聚合计算。以前我见到有人用jsonb_array_elements时不知道怎么和主表字段关联,查出来的结果都是笛卡尔积,数据量一大查询直接爆。正确写法是配合CROSS JOIN LATERAL使用:

SELECT o.id, o.items ->> 'order_no' AS order_no, item.value ->> 'product_id' AS product_id, (item.value ->> 'price')::numeric AS price, (item.value ->> 'quantity')::int AS quantity FROM orders o CROSS JOIN LATERAL jsonb_array_elements(o.items -> 'items') AS item WHERE o.id = 12345;

这段SQL会把刚才示例里的两条商品明细拆成两行,每条明细和订单号关联在一起,之后就可以正常group by和统计了。

如果要取JSON里面的所有key,用jsonb_object_keys很合适,比如查看某张表里JSON字段到底有哪些字段:

SELECT DISTINCT jsonb_object_keys(items) AS key_name FROM orders;

如果想知道哪个订单买过product_id=101,可以用@>包含操作符,这也是GIN索引最配合的条件写法:

SELECT id, items ->> 'order_no' AS order_no FROM orders WHERE items -> 'items' @> '[{"product_id": 101}]';

这种写法对JSON数组语义的匹配非常准确,也适合走索引。

2.3 大表JSON检索性能优化:GIN索引与表达式索引

JSON字段导致慢查询,绝大多数原因是没建索引。PG提供了GIN索引来加速@>、?等JSONB运算符的查询。去年给一张千万级订单表做过性能优化,原来某个JSON过滤查询要跑7秒多,加了GIN索引后降到几十毫秒,效果非常明显。

常规做法是:

CREATE INDEX idx_orders_items_gin ON orders USING GIN (items -> 'items');

如果你的查询经常直接对items整个字段做包含判断,那么可以直接建:

CREATE INDEX idx_orders_items_all ON orders USING GIN (items);

还有一类场景是:JSON里某个key在业务里被频繁当作过滤条件,比如items ->> 'order_no需要精确匹配。这种场景推荐建表达式索引:

CREATE INDEX idx_orders_order_no ON orders ((items ->> 'order_no'));

注意,函数写法必须和查询时完全一致,多一个空格或少一个括号都可能导致索引用不上。我在给团队培训时反复强调:先看执行计划,确认Node Type里出现Index Scan using才算真正走到索引,否则只是白白多占存储空间。

关于jsonb和json类型的选择,我的建议是:新表一律用jsonb,它支持索引,同时存储时自动去除重复key,处理速度也更快;老的json类型只在必须保留原始输入顺序或特殊兼容场景下才考虑,毕竟它不支持GIN索引,查起来基本都是全表扫。

3. 备份与恢复:这组命令要经常在测试环境里真实跑一遍

“备份在,不怕一万”这句话在PG运维里不太适用。我见过有人每天crontab跑一次pg_dump,日志显示completed successfully,结果某天要恢复时才发现备份文件是0字节,或者恢复出来的库缺了好几张表。备份的真正价值在于“能恢复”,所以恢复演练的优先级比做备份还要高。

3.1 逻辑备份:pg_dump和pg_restore的标准组合

逻辑备份就是导出SQL或自定义格式文件,适合单库或单表级别的备份。我最常用的命令是:

pg_dump -h 127.0.0.1 -U postgres -Fc -d yourdb -f yourdb.dump

参数说明:

  • -Fc:自定义格式,压缩体积,支持并行恢复;
  • -d:目标数据库名;
  • -f:输出文件名。

如果要备份某个大表,可以加上-t tablename只导出指定表,减少耗时和文件大小。备份角色、表空间等全局对象,用pg_dumpall:

pg_dumpall -h 127.0.0.1 -U postgres --globals-only -f global.dump

恢复时,先建数据库,再使用pg_restore:

createdb -U postgres yourdb_restored pg_restore -h 127.0.0.1 -U postgres -d yourdb_restored -j 4 -c yourdb.dump

-j 4表示用4个并行线程恢复大表,速度能明显提升。这里要注意,恢复前最好关闭目标库上的外部连接,不然会出现对象冲突报错。

3.2 物理备份与时间点恢复(PITR)的基本链路

逻辑备份容易做,但很多场景下无法满足“恢复到误操作之前的某一秒”这种需求。物理备份配合WAL日志可以实现PITR,这也是生产环境最常用的降级方案。

我一般先开启WAL归档,以PG16为例,在postgresql.conf中配置:

archive_mode = on archive_command = 'test ! -f /backup/pg_wal/%f && cp %p /backup/pg_wal/%f' wal_level = replica max_wal_senders = 10

然后执行基础备份:

pg_basebackup -h 127.0.0.1 -U postgres -D /backup/base -Fp -Xs -P

这样会在/backup/base下生成一个数据目录的完整镜像,WAL文件也一并收集。之后每天的增量都体现在新的WAL归档里。

到真正恢复时,步骤大致如下:

  1. 停掉当前的PG实例,把原数据目录改名备份,防止误操作覆盖原始数据;
  2. 把基础备份的文件复制到数据目录;
  3. 在数据目录下创建standby.signal文件,同时把postgresql.conf里的恢复目标参数写进去,比如:
recovery_target_time = '2025-03-18 14:32:00'
  1. 启动数据库,PG会从基础备份开始,重放WAL日志,一直恢复到指定时间点。

这个恢复链路必须提前演练,否则真到出问题时,你会发现archive_command路径、WAL文件权限、目录空间任何一个细节都可能卡住。

3.3 恢复演练——我为什么强调不能只看备份成功

简单说一下我自己的教训。有一年我负责的一个业务库数据量涨得特别快,备份任务一直正常,某天开发误删了一张核心配置表。我需要从当天的备份里找回数据。结果pg_restore跑到一半就报错:备份文件里某些对象依赖关系不完整。最后只能回退到前一天的备份,丢失了当天更新的一部分数据,好在影响还能控制。

从那之后我对备份的验证逻辑就变了:不只检查备份命令退出的状态码,还会随机抽取某个备份文件,在隔离环境里真实恢复一次,然后用表数量和关键表记录数比对原库。每次重大变更前,再做一次针对性恢复演练。

你可以用下面这条命令快速验证备份文件的完整性:

pg_restore -l yourdb.dump | wc -l pg_restore --list yourdb.dump | grep -i "TABLE DATA"

前者统计备份内对象数量,后者查找有哪些表的DATA。如果列表数量和实际业务表数量对不上,说明备份可能漏了对象。每周做一次随机恢复演练,比写十篇备份规范都管用。

4. 下载安装到初始密码:新环境落地的关键细节

部署一套新的PG实例,看起来简单,实际上不少团队在初始阶段就埋下了隐患。比如安装完不知道该去哪里改密码,或者改完密码之后本地连接还是需要输入密码。这一节我把从下载到首次登录的完整链路梳理一下。

4.1 不同来源的安装包如何选

PostgreSQL的安装方式主要分三类:操作系统官方源、PGDG官方源、Docker镜像。生产环境里我更推荐操作系统的官方源或PGDG官方源,因为安全和稳定性能跟上;测试和开发环境用Docker最省事。

以Ubuntu 22.04为例,装PG 16:

sudo apt update sudo apt install postgresql-16 postgresql-client-16

安装完成后,默认数据目录在/var/lib/postgresql/16/main,配置文件在/etc/postgresql/16/main/,服务名称为postgresql@16-main。

如果用Docker:

docker run -d \ --name pg16 \ -e POSTGRES_PASSWORD=yourpass \ -e POSTGRES_DB=appdb \ -p 5432:5432 \ -v /data/pg:/var/lib/postgresql/data \ postgres:16

这里提醒一句:Docker环境务必把数据目录挂载到宿主机,否则容器一删数据全没,这是新手最常见的事故来源。

4.2 首次登录、密码设置与pg_hba.conf配合

安装完成后,系统会创建一个名为postgres的操作系统用户,数据库超级用户默认也是postgres。本地使用Unix套接字连接时,认证方式通常为peer,也就是说只要你当前系统用户是postgres,就可以直接进数据库:

sudo -u postgres psql

进入后设置密码:

ALTER USER postgres WITH PASSWORD '你的强密码';

但这时你如果用密码方式连本机,可能会发现连不上:

psql -h 127.0.0.1 -U postgres -W

这多半是pg_hba.conf里针对TCP连接host条目的认证方式没有改。PG默认的pg_hba.conf在Ubuntu上位于/etc/postgresql/16/main/pg_hba.conf,需要添加或调整如下内容:

host all all 127.0.0.1/32 scram-sha-256 host all all 0.0.0.0/0 scram-sha-256

注意点有两个:一是不要把0.0.0.0/0写在前面覆盖掉更严格的配置,pg_hba.conf是自上而下匹配的,第一条匹配就直接生效;二是不要图省事用trust,trust等于免密放行,一个错误配置可能导致数据库裸奔在公网上。改完之后执行:

sudo systemctl reload postgresql

不需要重启,reload就会重新加载pg_hba.conf。然后就可以用密码登录了:

psql -h 127.0.0.1 -U postgres -p 5432

4.3 安装后最值得先调的几个参数

装完后直接跑默认参数不太现实,尤其是内存和并发这组。我把最基础的调整列在下面,你可以按机器规格先做一个保守估计:

参数默认值建议起步值说明
shared_buffers128MB机器内存的25%PG共享缓存池,太大也没用,配合OS缓存
work_mem4MB32MB~64MB排序、hash操作的内存预算,太高容易爆内存
maintenance_work_mem64MB256MB~512MB优化VACUUM、CREATE INDEX等维护操作
max_connections100200~500连接数上限,注意会占用一定内存
wal_levelreplicareplica生产必须保持replica以上,否则无法做流复制
max_wal_size1GB2GB~4GB控制checkpoint频率和恢复时间

如果有系统参数调优,需要修改postgresql.conf后重启;work_mem这类参数可以在会话级动态设置,但全局值要修改配置文件。我不会一上来就把所有参数调到最大,而是让数据库跑一段时间,再根据监控慢慢调整。

另外,中文环境还建议确认一下lc_monetary和lc_numeric,避免后续金额、数字格式化出现意想不到的结果。如果业务映射和字符集需求较高,初期最好就在模板库里把encoding设置为UTF8。

5. 慢查询和锁等待:性能故障定位的基本功

数据库性能变差,通常表现为业务接口变慢、CPU上涨、连接数打满。这些问题的背后大多是慢SQL、锁等待和不合理的查询计划。性能调优的入门门槛不高,关键是把定位工具用起来。

5.1 用日志和auto_explain捕捉慢SQL

最朴素的方法是开启慢查询日志。在postgresql.conf中设置:

log_min_duration_statement = 1000

这个配置表示执行超过1000毫秒的SQL会被记录到日志。配合auto_explain模块,还能把执行计划自动打印出来,这个信息对调优来说非常宝贵:

shared_preload_libraries = 'auto_explain' auto_explain.log_min_duration = 1000 auto_explain.log_analyze = on

注意shared_preload_libraries需要在启动前配置,修改后必须重启实例。auto_explain.log_min_duration是日志模块里的阈值,设置成和慢查询日志相同,每次慢SQL都会附带执行计划。

重启后,等业务跑一段时间,再去日志文件里搜索带duration: 1000的SQL,通常能找到系统最拖后腿的几个查询。

5.2 pg_stat_statements找出吃资源的大户

如果不想每次都在日志里翻,可以安装pg_stat_statements扩展,它会记录SQL、调用次数、平均耗时、总耗时、读取行数等统计信息。这个扩展默认在contrib包中,需要和auto_explain一样提前配置:

shared_preload_libraries = 'auto_explain,pg_stat_statements'

重启后执行:

CREATE EXTENSION pg_stat_statements;

接下来就能通过SQL查看统计。比如找出累计执行时间最长的10条SQL:

SELECT queryid, calls, total_exec_time, mean_exec_time, rows, query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

这里有个版本差异,PG13之前是total_time,PG13之后改成了total_exec_time,如果脚本报列不存在,检查一下PG版本。用完这类数据后,我会挑出calls次数多且耗时高的SQL,去和开发确认业务场景,再针对性加索引或改写法。

5.3 锁等待的处理:什么时候可以kill

数据库卡住不一定是CPU或磁盘问题,更多时候是行锁、表锁互相等待。我排查锁问题时,第一步永远是查看pg_stat_activity:

SELECT pid, usename, state, wait_event_type, wait_event, now() - query_start AS query_duration, left(query, 120) AS query FROM pg_stat_activity WHERE pid <> pg_backend_pid() ORDER BY query_duration DESC NULLS LAST;

这个查询会列出所有会话,按已执行时间排序,同时显示等待类型。如果某个pid的wait_event_type是Lock,说明它在等锁。进一步可以查到它到底被谁阻塞:

SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE blocked.wait_event_type = 'Lock';

确认锁源之后,再决定怎么处理。通常先和开发确认锁住的会话是否可以结束;如果可以,执行:

SELECT pg_terminate_backend(阻塞pid);

如果阻塞的会话一直处于idle in transaction状态,说明事务开了但没提交,时间长了会导致数据库连接被占满,这种一般可以直接终止。但如果是大批量更新任务执行到一半被kill,回滚带来的资源消耗也需要评估,不能盲目下手。

6. 这些年在PG运维上保留的几个习惯

最后聊几个我在长期运维里坚持的习惯,算是给同样在一线的朋友们一点参考。

备份的验证频率,我给自己定的标准是一周至少一次。不是只看备份脚本退出码,而是真正把备份拉到一台干净的环境里恢复,然后用几条基础SQL对比表记录数。这个动作能暴露出的问题,比你想的要多得多,比如某个表因为权限问题根本没被备份进去、某些依赖对象恢复顺序不对,这些在恢复前都很难看出来。

日志和指标的记录,我会用最简单的脚本把关键指标落到本地文件:CPU、内存、磁盘、PG的活跃连接数、慢查询数量,每天轮转。这些历史数据看起来原始,但出问题时回头看,往往能还原出故障前几个小时的变化,比任何监控大屏都实在。不要为了上监控而上监控,数据留存本身就有价值。

遇到锁等待和连接数打满的情况,我养成了第一个动作不是直接kill,而是先看pg_stat_activity里每条连接的state。很多所谓的死锁实际上是事务没提交导致的,找到源头之后,和业务团队确认再处理,比盲目清session稳妥得多。这个习惯救过我几次,也让团队避免了不少误杀。

PostgreSQL运维没有什么特别的秘诀,无非是平时多看日志、多跑恢复演练、多关心慢SQL和锁等待背后的事实。把这几个基本功练扎实,2025年的PG运维,就不会总在救火中度过了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 3:10:25

Mumble自建语音服务器:低延迟、可控、离线可用的开源方案

1. 项目概述&#xff1a;为什么一个“老派”语音工具还在被硬核用户反复提起&#xff1f;Mumble——这个名字在2024年的技术圈里&#xff0c;听起来有点像翻出抽屉底下的机械键盘&#xff1a;不 flashy&#xff0c;没算法推荐&#xff0c;不搞AI降噪&#xff0c;甚至界面还带着…

作者头像 李华
网站建设 2026/9/29 3:09:22

什么是大模型?大模型全面解析:定义、特点、应用场景及行业前景一网打尽,一文彻底搞懂!

大模型是指具有大规模参数和复杂计算结构的机器学习模型。本文从大模型的基本概念出发&#xff0c;对大模型领域容易混淆的相关概念进行区分&#xff0c;并就大模型的发展历程、特点和分类、泛化与微调进行了详细解读&#xff0c;供大家在了解大模型基本知识的过程中起到一定参…

作者头像 李华
网站建设 2026/9/29 3:09:15

Nordic nRF54L高性价比多协议SoC:架构解析与开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:08:15

Handy 离线语音转文字指南:5分钟搭起不联网的转录工作流

Handy 离线语音转文字指南&#xff1a;5分钟搭起不联网的转录工作流 【免费下载链接】Handy A free, open source, and extensible speech-to-text application that works completely offline. 项目地址: https://gitcode.com/GitHub_Trending/handy11/Handy Handy 是一…

作者头像 李华
网站建设 2026/9/29 3:03:26

Ubuntu 20.04 下安装 Cursor 并配置 TaoToken 统一 API 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华