数据库半夜拉起失败,这种经历做运维的都懂。PostgreSQL某天突然起不来,日志里报共享内存相关的错,再一看PID文件也不见了,两件事凑在一起,很容易让人走弯路。这篇文章把我这次完整的排障过程、背后原理和复查手段都写清楚,给遇到类似问题的朋友一条能直接抄的路线。
先说结论:这次故障本质上是/dev/shm容量耗尽导致共享内存段创建失败,异常断电又让postmaster.pid丢失,手工启动时pid状态判断失效,几件事叠加起来才显得特别难缠。下面按我当时的排查顺序一步步拆开讲。
1. 故障还原:一场连锁反应式的启动失败
1.1 现场环境与第一眼看到的报错
出问题的是一台Ubuntu 20.04服务器,PostgreSQL 14,内存64GB,数据库跑在宿主机上,由systemd管理。头一天晚上机房有过一次意外断电,UPS撑了几分钟之后系统被强制关机。第二天早上开机,其他服务陆续恢复,只有PostgreSQL一直没起来。
我手动尝试启动时,终端输出很简短:
sudo pg_ctlcluster 14 main start结果:
Error: /usr/lib/postgresql/14/bin/pg_ctl /usr/lib/postgresql/14/bin/pg_ctl start -D /var/lib/postgresql/14/main -l /var/log/postgresql/postgresql-14-main.log -s -o "-c config_file=/etc/postgresql/14/main/postgresql.conf" exited with status 1pg_ctlcluster的报错信息一向不够直观,真正的细节都写在日志文件里。打开/var/log/postgresql/postgresql-14-main.log,拉到最后面,看到这么几条:
2025-03-XX 08:12:01.301 CST [3124] FATAL: could not create shared memory segment for segment "PostgreSQL": No space left on device 2025-03-XX 08:12:01.301 CST [3124] DETAIL: Failed to create shared memory segment. 2025-03-XX 08:12:01.301 CST [3124] HINT: This error usually means that PostgreSQL's request for a shared memory segment exceeded available memory, swap space, or huge pages. To reduce the request size (currently 20973568 bytes), reduce shared_buffers and/or max_connections.注意DETAIL里的单位,20973568 bytes大约是20MB,这并不算大。一个20MB的共享内存段都创建不出来,说明不是数据库参数配得太高,而是系统层面可用的共享内存几乎没有。这是排查方向的重要分水岭:不要一看到shared memory报错就急着调低shared_buffers,先搞清楚系统到底还有没有空间。
1.2 排障框架:先把问题切成两半
看到这类报错之后,我先给自己定了个排查框架,避免被表象带着走。整个问题可以拆成两半:
- 共享内存创建失败:这是直接的启动阻塞点,需要查系统可用内存、
/dev/shm容量、sysv共享内存限制; - PID文件状态异常:
pg_ctl无法正确判断实例是否在运行,导致后续手动操作重复、混乱,必须单独核实进程和文件状态。
实际故障里,如果PID文件是有的,pg_ctl会拒绝启动,并提示"lock file already exists",情况反而清楚。而我这边更拧巴:PID文件不存在,pg_ctl以为没有实例在跑,于是每次启动都会尝试创建共享内存、初始化数据目录,每次都撞到共享内存的墙上。要理清这套连锁反应,得按顺序来。
2. 共享内存冲突:为什么 PostgreSQL 会“无内存可用”
2.1 PostgreSQL 的共享内存模型
PostgreSQL启动时必须创建一块共享内存区域,用来存放buffer pool、锁、事务状态等全局数据。数据库的Shared Buffer Pool、clog、proc array全都挂在这块共享内存里,没有它,数据库进程之间无法协作。
PostgreSQL 9.3及以后版本提供了三种共享内存实现方式,由shared_memory_type参数控制:
mmap:使用POSIX共享内存,挂载在/dev/shm下,这是Linux上的默认值;sysv:使用System V共享内存,走的是shmget/shmat,对应内核参数kernel.shmmax和kernel.shmall;windows:仅在Windows上使用。
很多人在Linux上部署PostgreSQL时,默认用的就是mmap,也就是说PostgreSQL的共享内存段会映射到/dev/shm。当/dev/shm目录被塞满,或者挂载容量已经耗尽,数据库就创建不出新的共享内存段,表现出的报错和我看到的完全一致。
这里有个容易被忽略的点:shared_buffers设的是8GB,不代表只申请8GB。PostgreSQL实际申请共享内存时还要加上其他扩展区域(比如wal_buffers、clog、锁表、max_connections对应的backend数组等)。启动时日志显示的请求大小通常比shared_buffers略大一点。但如果max_connections调得非常高,额外占用会很明显。
我这次出问题的实例shared_buffers=4GB,请求大小却只有约20MB,这是最诡异的地方——为什么实例参数看起来是4GB,实际请求只有20MB?原因是我之前为了快速恢复,已经把shared_buffers改成了128MB并尝试重启,但日志不会覆盖旧记录。这点也提醒大家:看日志时要注意时间线,避免把多次修改参数后的混合输出当成同一次启动的现场。
2.2 检查 /dev/shm 和内核参数
确认方向后,我立刻查看/dev/shm的使用情况:
df -h /dev/shm结果:
Filesystem Size Used Avail Use% Mounted on tmpfs 32G 32G 0 100% /dev/shm/dev/shm被打满了。默认情况下,/dev/shm的大小是物理内存的一半,这台机器64GB内存,所以/dev/shm是32GB。一个32GB的tmpfs被塞满,PostgreSQL想申请20MB都不肯给。
继续看是哪些文件占用了空间:
du -sh /dev/shm/*发现里面有一个大型中间件服务运行时产生的临时IPC文件,单个就有将近30GB。这个中间件会按内存大小预留共享内存区,断电重启后它的清理逻辑没有正常执行,遗留文件也没有回收。系统开机顺序恰好是中间件先启动,等PostgreSQL再起来时,/dev/shm已经没位置了。
内核参数那边我也顺带查了一下:
cat /proc/sys/kernel/shmmax cat /proc/sys/kernel/shmall ipcs -mkernel.shmmax在64位系统上通常是个非常大的值,ipcs -m也没有看到残留的SysV共享内存段。这说明问题不在SysV层面,而是纯粹的POSIX共享内存空间不足。如果你看到ipcs -m里挂着一堆nattch=0的段,那才是历史进程崩溃后没有回收的共享内存残留,需要用ipcrm -m手动清理。
2.3 共享内存冲突的高发场景
把这次故障抽象一下,共享内存相关启动失败大体出现在这几种场景:
/dev/shm被系统里其他进程写满:像上面提到的中间件,或者容器平台在宿主机/dev/shm上留下的大量临时文件。这种最隐蔽,因为数据库自身参数完全合理,问题出现在邻居进程身上。SysV共享内存残留:数据库进程被
kill -9,或者宿主机crash,早期使用sysv配置的实例容易留下孤儿共享内存段。新实例启动时如果shemax/shmall限制严格,会创建失败。容器场景下
shm-size设置过小:Docker和Kubernetes默认的/dev/shm只有64MB。PostgreSQL在容器里如果采用默认mmap方式,shared_buffers稍微大一点就会撞上这堵墙。这也是"PostgreSQL in Docker容易启动失败"常见原因之一。huge_pages配置与系统大页池不匹配:
huge_pages=try时,不够会回退,huge_pages=on时如果系统没有预留足够的大页,启动直接失败。出现"Cannot allocate huge page"这一类的DETAIL时,检查/sys/kernel/mm/hugepages。
排查共享内存问题时,按“当前空间使用率 → 历史残留段 → 内核参数限制 → 是否容器环境”的顺序走,基本能把90%的原因锁住。
3. PID文件丢失:一个容易被忽略的“假故障”
3.1 postmaster.pid 到底有什么用
PostgreSQL启动时,会在数据目录下生成一个postmaster.pid文件,里面记录了主进程PID、数据目录路径、启动时间、监听端口、socket目录等信息。这个文件的意义不是给自己看的,而是给同名数据目录的其他进程和pg_ctl工具看的:
- pg_ctl通过这个文件判断数据目录是否已经被某个实例占用;
- 文件中的端口信息用于识别是否有监听冲突;
- 如果实例已经存在,新进程会拒绝启动,防止同一份数据被两个postmaster同时读写,导致数据文件损坏。
正常情况下,数据库关闭时,这个文件会被正确删除。所以系统里正常运行的实例不会一直摆着一个PID文件在那里。
3.2 哪些操作会导致 PID 文件丢失
PID文件丢失或者损坏,通常是异常路径导致的:
- 系统断电/强制重启:文件系统没有机会完成卸载,文件可能丢失、截断,或者内容不完整;
- 磁盘故障/网络文件系统异常:如果数据目录放在NAS或者云盘上,瞬间断连会造成PID文件不可见;
- 管理员手动删除:有些人遇到"lock file exists"报错时,随手把PID文件删了,但忘记确认进程是否真的存在;
- kill -9主进程:信号直接干掉进程,数据库没走正常清理流程,PID文件留在原地。这种情况更多是文件残留,而不是文件丢失,但误效果类似。
本次故障属于断电导致的文件系统丢失。重启后我发现/var/lib/postgresql/14/main/下面没有postmaster.pid,而这个实例在断电前是正常运行的,理应有这个文件。
3.3 进程还在但 PID 文件没了,怎么办
这是最危险的一种情况。先别急着启动,先确认数据库进程是否还在运行:
ps -ef | grep postgres看输出结果时,不仅要看有没有进程,还要核对进程的启动参数和-D指向的数据目录。比如:
postgres 2150 1 0 ... /usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main还可以通过/proc/<PID>/cmdline进一步确认:
cat /proc/2150/cmdline | tr '\0' ' '确认进程还活着的时候,正确的做法并不是去补一个PID文件,而是直接使用现有进程,让数据库继续对外服务。如果此时盲目执行pg_ctl start,会因为PID文件缺失而把同一个数据目录再拉起一个postmaster,两个主进程同时写数据文件,整库直接处于极端危险状态。
反过来,如果确认进程确实不存在,那么删除遗留/丢失状态的PID文件无妨。
这里有一个判断技巧:/proc目录是内核的实时视图,文件在但进程已死的典型表现是ls -l /proc/<PID>报"no such process"。判断PostgreSQL主进程是否存活,最可靠的就是看/proc/<PID>/cmdline,因为PID可能被系统回收复用,只看进程号不等于安全。
4. 完整修复过程实录
4.1 第一步:清理共享内存并恢复启动
确认/dev/shm被中间件遗留文件占满后,我先联系相关人员确认那个IPC文件是否可以安全删除。删除前记下文件的大小和路径,然后执行清理:
rm -f /dev/shm/xxx_ipc_file df -h /dev/shm/dev/shm的使用率从100%掉到了个位数。此时再尝试启动PostgreSQL:
sudo pg_ctlcluster 14 main start这次很顺利,数据库直接起来了。我立刻做了两个验证:
pg_isready -p 5432 sudo -u postgres psql -c "select version();"都返回正常。这一步验证了共享内存空间确实是唯一阻塞点,PID文件丢失没有影响实际注册进程的启动流程。也就是说,如果当初没有先清理共享内存,而是手动伪造PID文件、反复重启,只会让故障更加混乱——好在排查顺序是先内存后文件,避免了自己把自己带进坑里。
4.2 第二步:调整实例参数与控制 /dev/shm 容量
启动成功后,我顺手检查了实例当前的共享内存配置:
show shared_memory_type; show shared_buffers;结果分别是mmap和4GB。这意味着这台实例的共享内存段会占用/dev/shm的实际空间,而/dev/shm只有内存的一半,一旦邻居进程占用一大块,数据库就非常被动。
针对这台服务器,我做了三项调整:
第一,把shared_buffers从4GB微调到3GB。这个动作的出发点是留出更多系统缓存余量,不是为了迁就/dev/shm容量,因为/dev/shm的最终方案是扩容。经验上shared_buffers设置在物理内存的25%左右比较常见,64GB内存给16GB也不会是问题,但这套库的活跃工作集并不大,3GB已经足够,调低后整体内存压力更小。
第二,调整系统可用容量。如果是物理机,可以重新挂载扩大/dev/shm,但这台机器的/dev/shm本身就是内存的50%,更合理的做法是查清那个中间件的残留文件为什么会存在,并把它的IPC清理机制修好。此外,我加了一条监控规则:对/dev/shm的used百分比做告警,阈值85%,避免下次再被打满。
第三,评估是否需要切换共享内存类型。如果某台机器的/dev/shm长期不可控,可以直接在postgresql.conf里设置:
shared_memory_type = sysv然后重启。sysv不走/dev/shm的tmpfs,独立走shmget接口。但要注意,sysv模式下如果数据库崩溃后残留共享内存段,需要手工用ipcrm清理,维护体验不如mmap清爽。我最终没有切换,因为单纯扩容+告警已经解决了当前痛点,切换类型属于改变运行时语义,对一个稳定运行的实例来说,能不折腾就不折腾。
4.3 第三步:防复发配置和自检
修复完成之后,还要处理PID文件丢失这个隐患。因为文件是被异常断电搞没的,我马上检查了systemd服务配置,确认以下两点:
- PostgreSQL的开机自启服务是否正常enable;
- 是否配置了
Restart=on-failure。
查看服务状态:
systemctl status postgresql@14-main.service systemctl cat postgresql@14-main.service确认的是:服务已enable,由于断电后手动拉起,服务状态正常。systemd配置用的是发行版自带模板,没有特殊改动。这里想提醒一句,很多生产环境为了"不掩盖故障",会故意把Restart关掉,反而需要人工介入。具体取舍看团队策略,但既然这次断电已经暴露出人工响应有延迟的问题,我建议至少保留数据库服务崩溃后的自动拉起能力,以免半夜只有告警没有处理。
再补充一个检查项:数据库数据目录的所有者和权限。异常恢复场景下,有时文件属主会被改掉,导致启动失败报"could not change permissions"。确认一下:
ls -ld /var/lib/postgresql/14/main该目录应为postgres:postgres权限700。一切正常后,我额外跑了一次全表逻辑备份的演练命令,确认数据完整,才把这次故障从事件列表里正式关闭。
5. 常见报错速查与排障技巧
5.1 高频报错对照表
把这次碰到的报错,以及同类启动失败场景整理成一个速查表,方便大家遇到问题时直接对照:
| 报错关键内容 | 成因方向 | 优先处理办法 |
|---|---|---|
could not create shared memory segment: No space left on device | /dev/shm或SysV共享内存空间不足 | df -h /dev/shm、ipcs -m排查占用,扩容或清理后重启 |
could not open shared memory segment ... No such file or directory | 系统重启后共享内存段丢失,跨进程引用失效 | 正常重启数据库即可,属于临时性报错 |
lock file "postmaster.pid" already exists | PID文件残留,或实例确实在运行 | 用ps -ef与/proc/<PID>/cmdline确认进程存活,再决定是否删除文件 |
FATAL: could not create lock file "postmaster.pid": No space left on device | 数据目录所在磁盘满 | 检查数据目录所在分区的剩余空间 |
HINT: preallocation with fallocate failed | 共享内存文件创建时的预分配失败 | 通常是tmpfs空间不足,清空间或扩容 |
could not map anonymous shared memory | 容器/shm太小或资源限制 | Docker加--shm-size,K8s调整emptyDir或设置sharedMemorySize |
Cannot allocate huge page | huge_pages=on但系统没预留大页 | 调整huge_pages=off或正确配置hugepagessize |
这个表不能直接当万能药,但能帮你把问题的方向先定下来,少走弯路。
5.2 我总结的几个排障原则
排障顺序很重要。我这次如果先处理PID文件,或者一看到共享内存报错就调低shared_buffers,都会把方向带偏。总结几条经验:
第一条,先看日志,再下判断。PostgreSQL的日志比systemd的状态输出要精确得多。多花两分钟读errcode和DETAIL,后面能省两个小时。尤其是FATAL级别的错误,通常已经解释了失败的直接原因。
第二条,共享内存问题,先看空间再看参数。只有先把系统的共享内存实际使用情况摸清楚,再敢去动shared_buffers/max_connections。否则你很可能把参数调小了,问题也没解决,因为病因根本不在参数上。
第三条,PID文件绝不能盲删。先确认进程是否存在,再对文件下手。很多人看到"postmaster.pid exists"就直接删,一旦实例其实活着,就是在制造数据文件损坏的风险。
第四条,容器内启动PostgreSQL,先确认shm容量。在Docker里起PostgreSQL时,默认/dev/shm的64MB很容易触发共享内存报错。测试环境建议显式:
docker run -d --name pg --shm-size=1g -e POSTGRES_PASSWORD=xxx postgres:14--shm-size直接对应PostgreSQL的mmap共享内存可用空间,这个参数经常被忽略。
此外还有一个小技巧:使用strace抓启动过程,可以直接看到共享内存相关系统调用的失败原因:
sudo -u postgres strace -f -e trace=shmget,shmat,memfd_create,fallocate,fstat /usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main不过正常排障到不了这步,日志和df通常已经够了。strace只是确认底层行为时的手段。
收尾:一次排障给我留下的习惯
现在每次在服务器上部署PostgreSQL,我都会顺手确认三件事:第一,/dev/shm当前容量和整体压力;第二,shared_memory_type到底是mmap还是sysv;第三,数据目录下PID文件的状态是否和实际进程一一对应。
这套习惯正是从这次故障里长出来的。你很难预料一个中间件服务会把系统共享内存吃干抹净,也很难预料一次断电会让PID文件突然消失。但数据库的启动要求其实很朴素:共享内存要能建得出来,锁文件要能说明当前实例是否在运行。把这两点纳入日常巡检,PostgreSQL的"突然启动失败"大概率可以在5分钟内定位到根因。
如果这篇文章里的某个报错正好是你屏幕上出现的那一条,建议先查df -h /dev/shm和ps -ef | grep postgres,这两个命令能帮你排除掉一半以上的可能。剩下的事情,按日志一步步走,总能解决。