1. 问题现象与初步判断
那天凌晨3点,我被一阵急促的警报声惊醒。监控系统显示生产环境的PostgreSQL数据库服务异常下线。这本来应该是一次常规的维护重启,但执行pg_ctl restart后却出现了令人意外的错误:
code复制FATAL: pre-existing shared memory block (key 36815078, ID 4161551) is still in use
pg_ctl: PID file "/data/pgdata/data/postmaster.pid" does not exist
这两个错误同时出现时,情况就变得复杂起来。作为有十年经验的DBA,我立即意识到这不是简单的服务重启失败,而是典型的异常关闭后遗症。这种问题在数据库遭遇强制关机、操作系统崩溃或进程被kill -9强制终止后非常常见。
关键提示:当看到"shared memory block is still in use"和"PID file does not exist"同时出现时,90%的可能性是数据库上次关闭时没有完成正常的清理流程。
2. 深入理解问题根源
2.1 PostgreSQL的进程与内存管理机制
要彻底解决这个问题,我们需要先理解PostgreSQL的进程和内存管理机制。PostgreSQL采用多进程架构,主进程(postmaster)负责管理整个数据库实例。当数据库正常启动时:
- postmaster进程会创建并锁定
postmaster.pid文件,记录自己的PID和共享内存信息 - 向操作系统申请共享内存段(用于shared_buffers等)
- 创建后台工作进程(如WAL writer、checkpointer等)
正常关闭时,postmaster会:
- 优雅终止所有子进程
- 释放共享内存和信号量
- 删除
postmaster.pid文件
2.2 异常关闭导致的问题链
当数据库异常关闭时(如服务器断电、kill -9),这个清理流程就被打断了,导致:
- PID文件状态异常:可能被部分删除或残留无效内容
- 共享内存段泄漏:操作系统仍保留着已分配的共享内存
- 信号量未释放:进程间通信用的信号量仍然存在
当再次启动时,新实例会:
- 检查PID文件(发现不存在或无效)
- 尝试申请相同的共享内存key(发现已被占用)
- 为防止数据损坏,立即中止启动过程
3. 系统级问题验证
3.1 检查残留进程
首先确认是否有残留的PostgreSQL进程:
bash复制ps -ef | grep postgres
如果发现任何以postgres用户运行的残留进程(特别是那些指向相同数据目录的),需要先终止它们:
bash复制kill -15 <PID> # 先尝试优雅终止
sleep 3
kill -9 <PID> # 必要时强制终止
3.2 检查共享内存段
使用ipcs命令查看当前系统中的共享内存段:
bash复制ipcs -m | grep -E 'postgres|36815078'
重点关注以下字段:
shmid:共享内存段ID(本例中的4161551)key:共享内存键值(36815078的十六进制是0x0231c0e6)owner:所有者(通常是postgres用户)
3.3 检查信号量集
PostgreSQL同样使用信号量进行进程同步:
bash复制ipcs -s | grep postgres
记录下相关的semid值,以备后续清理使用。
4. 系统资源清理与恢复
4.1 安全清理共享内存
确认共享内存段确实不再使用后,使用ipcrm进行清理:
bash复制ipcrm -m 4161551 # 使用实际的shmid
重要警告:确保没有其他PostgreSQL实例在使用这个共享内存段!误删会导致运行中的数据库崩溃。
4.2 清理残留信号量
如果有残留的信号量集:
bash复制ipcrm -s <semid> # 替换为实际的信号量ID
4.3 检查并清理PID文件
虽然错误显示PID文件不存在,但最好确认一下:
bash复制ls -l /data/pgdata/data/postmaster.pid
rm -f /data/pgdata/data/postmaster.pid # 如果存在且内容无效
5. 安全重启数据库
完成所有清理工作后,可以尝试重启数据库:
bash复制pg_ctl start -D /data/pgdata/data -l /var/log/postgresql/startup.log
启动后立即检查日志:
bash复制tail -f /var/log/postgresql/startup.log
预期应该看到正常的启动序列,包括:
- 共享内存分配成功
- 后台进程启动
- 数据库进入可接受连接状态
6. 深度防御与预防措施
6.1 配置自动清理脚本
对于经常出现异常关闭的环境,可以准备一个清理脚本:
bash复制#!/bin/bash
# 清理残留的PostgreSQL资源
PGDATA=/data/pgdata/data
# 终止相关进程
pkill -u postgres -f "postgres -D $PGDATA"
sleep 2
pkill -9 -u postgres -f "postgres -D $PGDATA"
# 清理共享内存和信号量
ipcs -m | awk '/postgres/ {print $2}' | xargs -I {} ipcrm -m {}
ipcs -s | awk '/postgres/ {print $2}' | xargs -I {} ipcrm -s {}
# 移除PID文件
rm -f $PGDATA/postmaster.pid
6.2 内核参数调优
调整Linux内核参数,减少共享内存泄漏的可能性:
bash复制# /etc/sysctl.conf
kernel.shmall = 4294967296 # 总共可用的共享内存页数
kernel.shmmax = 68719476736 # 最大单个共享内存段大小(64GB)
kernel.shmmni = 4096 # 共享内存段最大数量
应用设置:sysctl -p
6.3 监控系统资源
设置监控告警,及时发现资源泄漏:
bash复制# 监控共享内存使用
*/5 * * * * /usr/bin/ipcs -m | grep -c postgres > /var/log/pg_shm_monitor.log
# 监控异常进程
*/5 * * * * ps -ef | grep -E 'postgres.*defunct' | mail -s "Defunct PG Process" dba-team@example.com
7. 高级故障排查技巧
7.1 使用lsof检查文件锁定
当PID文件问题特别棘手时,可以使用lsof检查:
bash复制lsof /data/pgdata/data/postmaster.pid
这能显示哪个进程正在锁定该文件(即使文件已被删除但句柄未释放)。
7.2 分析系统日志
查看系统日志获取更多线索:
bash复制journalctl -xe | grep -i postgres
dmesg | grep -i kill
7.3 使用gdb调试(高级)
对于极其顽固的案例,可以使用gdb附加到postmaster进程:
bash复制gdb -p <postmaster_pid>
(gdb) call shmdt(<address>)
(gdb) detach
警告:这种方法风险极高,只应由经验丰富的DBA在测试环境尝试。
8. 真实案例经验分享
去年我们遇到过一个特别棘手的案例:一个生产数据库在连续崩溃重启后,出现了多层共享内存冲突。最终发现是因为:
- 有一个自定义的extension在崩溃时没有正确释放资源
- 系统内核参数shmmax设置过小
- 存在一个僵尸进程持续占用旧的共享内存段
解决方案是:
- 更新有问题的extension
- 调整内核参数
- 彻底重启服务器(而不仅仅是数据库服务)
这个案例教会我们:有时候表面看起来是数据库问题,实际上可能是系统配置或第三方组件导致的。