半夜两点被电话叫起来,数据库实例怎么也拉不起来,终端里就剩一行红字:"数据库启动时发生报错,致命错误:无法创建信号量"。如果你也遇到过这种场面,第一反应多半是怀疑数据库软件坏了,或者数据文件出了问题。实际上这句话是操作系统在告诉你:数据库向内核申请进程同步资源时被拒绝了,这个资源叫System V信号量。本文就把这条报错从头到尾拆开讲清楚,涉及信号量机制、kernel.sem四个内核参数怎么调、残留信号量怎么清理、容器和systemd环境下为什么改了没生效,以及怎么把这类故障挡在巡检之外。适合DBA、运维工程师,以及自己搭库做开发的读者。
1. "无法创建信号量"背后是操作系统在拒绝:先理解信号量与kernel.sem
1.1 数据库为什么离不开信号量
数据库绝大多数是多进程架构,拿Oracle来举例,启动时会拉起PMON、SMON、DBWR、LGWR等一系列后台进程,这些进程共同操作SGA里的数据块、日志缓冲区、锁结构。如果没有任何同步机制,两个进程同时修改同一块内存,数据早就烂掉了。信号量就是内核提供的一把"令牌",进程在做关键操作前必须拿到令牌,做完再交还,其他人才能进来。
你可以把信号量想象成餐厅门口取号等位:信号量集合就是整排排队通道,信号量数量就是座位/排队号的数量。数据库启动时一次性向内核申请一批信号量,如果系统里已经没有足够名额,内核直接返回失败,数据库实例自然起不来。PostgreSQL、达梦数据库这类产品同样依赖信号量,只是具体调用方式略有差异。所以这条报错不是数据库软件自身故障,而是所在操作系统给不了它想要的资源。
需要额外说一句:数据库死锁发生在应用事务层,是两个会话互相持有对方需要的锁,跟内核信号量是两码事。排查"无法创建信号量"时不要把死锁分析扯进来,方向完全不同。
1.2 kernel.sem四个参数,分别卡在哪个环节
Linux内核用kernel.sem这个参数统一管理System V信号量,它包含四个值,顺序固定,缺一不可:
| 参数 | 全称含义 | 通俗解释 | 常见默认值 |
|---|---|---|---|
| SEMMSL | 每个信号量集合最多能容纳的信号量个数 | 一根队列最多排多少人 | 250 |
| SEMMNS | 整个系统范围内信号量总数上限 | 所有队列加起来最多多少人 | 32000 |
| SEMOPM | 一次semop系统调用最多能同时操作的信号量个数 | 一次最多处理多少个排队号 | 32 |
| SEMMNI | 整个系统范围内信号量集合总数上限 | 系统最多允许开多少条队 | 128 |
"无法创建信号量"并不是一个固定的失败点,这四个参数任何一个命中上限,报错都可能以这个形式出现:
- SEMMNI满了:系统里信号量集合数量太多,新集合创建不出来。
- SEMMNS满了:整个系统的信号量总数被占光,集合里再加信号量就超限。
- SEMMSL太小:每个集合能容纳的信号量个数小于数据库申请的个数。
- SEMOPM太小:数据库的某个操作一次性要操作很多信号量,超过单次上限。
所以遇到报错先别急着套网上现成的kernel.sem值,先看清楚系统到底卡在哪一项。
1.3 一个大实例到底会吃掉多少信号量
以Oracle数据库为例,实例启动时创建信号量集合,集合里信号量的数量大致和processes参数成正比。processes设的是数据库允许的最大进程连接数,每个客户端连接在Oracle里对应一个服务器进程,这些进程都要关联到信号量上。如果processes设成1500,SEMMSL却还是默认的250,数据库连启动阶段都过不去。
这也是为什么很多人在小机上没事、搬到大服务器上就报错的原因——通常DBA习惯于把processes调大应对高并发,却忽略了同机内核的SEMMSL上限。达梦数据库同样遵循类似逻辑,进程模型参考了Oracle设计,信号量需求与配置的最大连接数/工作线程数强相关。PostgreSQL情况稍有不同,它在现代Linux上默认更倾向使用POSIX信号量,对System V信号量需求相对弱一些,但如果你用的是编译期指定SysV信号量、或者系统限制POSIX信号量的场景,同样会被kernel.sem卡住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 现场排查链路:不是所有"无法创建信号量"都是同一层资源告急
2.1 第一步先拿全报错:终端、日志、dmesg一起看
排查这类故障最大的忌讳是只看一句话。数据库启动失败时,终端只显示一行"致命错误:无法创建信号量",但数据库自身的告警日志里往往有更多上下文。Oracle的alert日志会记录启动到哪个阶段失败的,PostgreSQL的日志则可能伴随"could not create semaphores: No space left on device"这类更直白的描述。
同时要看系统日志,dmesg里偶尔会有IPC namespace相关线索。把三处信息拼在一起,基本就能判断是内核参数设置问题,还是系统资源被其他进程占满。别急着调参数,多花三分钟看日志,避免误判方向。
2.2 用ipcs看系统信号量使用状态
确认问题出在信号量之后,第一步用ipcs -u看当前使用量和上限对比:
bash复制ipcs -u
输出大致长这样:
text复制------ Semaphore Status --------
used arrays = 124
allocated semaphores = 28500
maximum arrays = 128
maximum semaphores = 32000
看到没有,used arrays是124,maximum arrays是128,集合数已经快封顶;allocated semaphores也到了28500,离32000只剩一点余量。这种情况下数据库再启动去申请新集合,自然就报"无法创建信号量"。
再用ipcs -s看看具体是谁占着这些信号量:
bash复制ipcs -s -p
这个命令会列出每个信号量集合的key、semid、创建者PID和最后操作PID,帮助你判断是数据库残留、还是其他中间件(比如WebLogic、Tuxedo)占用了大量IPC资源。如果所有信号量都被同一个异常残留的实例占着,接下来的方向就是清理而不是单纯调参。
2.3 核对kernel.sem当前值,以及它有没有被持久化
查看当前内核参数值:
bash复制cat /proc/sys/kernel/sem
sysctl kernel.sem
常见默认值是 250 32000 32 128,但很多数据库部署文档要求至少是 250 32000 100 128,区别在SEMOPM从32提到100。如果是云主机或定制镜像,你看到的默认值可能已经被改过;而异常现场往往是把安装时按文档调过的参数丢了,或者被人为改回默认值。
注意,永久配置和运行时值不是一回事。你执行sysctl -w只是临时生效,重启后是否还在取决于/etc/sysctl.conf或/etc/sysctl.d下的配置。很多人改完参数当时能启动数据库,过了几天机器一重启问题又回来,原因就是参数没持久化。
bash复制grep -r kernel.sem /etc/sysctl.conf /etc/sysctl.d/ /usr/lib/sysctl.d/ /run/sysctl.d/ 2>/dev/null
确保所有会被系统读取的配置文件里都有一致的kernel.sem定义,并且没有互相覆盖。
2.4 判断是"系统总量不足"还是"单实例设置过大"
把当前使用量和上限对比后,会出现两类典型情况:
第一类是全局总量确实被占满,系统里跑了很多实例或中间件,SEMMNI/SEMMNS已经逼近上限。此时即使数据库配置不变,你也得扩容内核参数,或者先停掉一部分非核心服务释放资源。
第二类是单个实例要得太多,比如processes被调到3000,但SEMMSL还是默认250。这个在ipcs -u上可能看不出总量紧张,因为used arrays不多,只是你新启的这个实例想要在一个集合里放3000个信号量,超过单集合上限。判断方法很简单:拿实例需要的信号量数量对比SEMMSL,如果实例进程数加上余量已经逼近甚至超过SEMMSL,那就是单集合要扩容。
这两种情况的处理完全不同,前者动SEMMNI/SEMMNS,后者动SEMMSL。混为一谈容易把参数调大后又遇到其他问题。
3. 解决方案实操:调整内核参数、清理残留、验证启动
3.1 算一组能用的kernel.sem
网上很多帖子直接让你抄 kernel.sem = 250 32000 100 128,这个值在小规模环境没问题,但大连接数场景会踩SEMMSL=250的坑。我的习惯是结合实例配置现算。
以一个具体场景为例:一台物理机要跑2个Oracle实例,每个实例processes参数设置1500。
- SEMMSL至少要大于processes,留一点余量,取1520或者更保守的2000。
- SEMMNS需要覆盖所有实例的进程总量,至少是1500×2=3000,再给系统其他进程留空间,设32000比较宽裕。
- SEMOPM直接按数据库要求设100。
- SEMMNI看实例数和同机中间件数量,两个数据库实例至少需要128,我倾向于设512给未来扩容留空间。
最终配置就是:
text复制kernel.sem = 2000 32000 100 512
如果同机还要跑WebLogic这类重度中间件,SEMMNI和SEMMNS都再往上抬一档,不要卡着刚够用线算。内核参数是全局共享的,多留20%-30%余量能避免下次扩容时再次故障。
3.2 临时修改与永久生效
先临时改,确认数据库能起来再写持久化配置:
bash复制sysctl -w kernel.sem="2000 32000 100 512"
这条命令立即生效,不会影响正在运行的进程,因为只是调高上限。随后直接尝试启动数据库,确认报错消失。验证通过后写入持久化配置,推荐写到/etc/sysctl.d下的独立文件,避免和发行版自带配置混在一起:
bash复制cat > /etc/sysctl.d/99-database-sem.conf <<'EOF'
kernel.sem = 2000 32000 100 512
EOF
然后加载所有配置:
bash复制sysctl --system
如果系统不支持--system参数(老版本),用:
bash复制sysctl -p /etc/sysctl.d/99-database-sem.conf
加载完再执行sysctl kernel.sem,确认运行时值已经变成新配置。记住,改完配置后的验证动作不能省。
3.3 残留信号量的识别与清理
有时候参数明明够大,信号量总量使用率却只增不减,原因多半是数据库异常宕机后残留了一堆信号量集合。比如Oracle实例被kill -9,或者虚拟机直接断电,System V信号量可能没被内核自动回收。
先找到残留集合:
bash复制ipcs -s -p
输出中,哪些集合对应的创建者PID已经不存在了,就说明持有者已经消失,这些就是残留信号量。逐一删除:
bash复制ipcrm -s <semid>
如果要批量删除某个用户(比如oracle)的残留信号量,可以用:
bash复制ipcs -s | awk '/oracle/ {print $2}' | xargs -r -n1 ipcrm -s
这条命令很危险,务必谨慎使用。执行前必须确认目标实例已经完全停止,并且该用户下没有其他正在运行的业务进程,否则会直接影响运行中的服务。我一般在生产环境不批量执行,而是先列出semid清单,人工确认后再一条条删。
同样,如果残留的是共享内存段,用ipcrm -m
3.4 重启数据库并验证新参数是否扛得住
清理完成、参数调整到位后,重启数据库:
- 启动前执行ipcs -u,确认used arrays和allocated semaphores已经回落,没有继续顶着上限。
- 启动后立刻执行ipcs -s,查看新实例的信号量集合是否创建成功,nsems列是否符合预期。
- 连续观察一段时间,确认不会在使用率上升后再次触碰上限。
如果启动后没几分钟又报错,说明你计算的信号量需求偏小,回到步骤3.1重新估算,给到足够余量。不要在这个环节抱有侥幸心理,生产环境宁可参数设大一点,也不要为省资源省出故障。
4. 这次踩坑过程中最容易翻车的几个细节
4.1 容器与云环境:参数改了可能没生效
如果数据库跑在Docker容器里,情况会复杂不少。容器和宿主机共享同一个内核,kernel.sem是全局参数,容器内执行sysctl -w通常只能修改一部分命名空间级参数,而kernel.sem不属于可隔离的命名空间,你在容器里看到的/proc/sys/kernel/sem其实是宿主机内核的值,容器内改了大概率无效。
正确处理是在宿主机层面调整,或者通过容器运行参数传入:
bash复制docker run --sysctl kernel.sem=2000,32000,100,512 ...
注意这个方式受容器运行时权限控制,不一定总是生效。Kubernetes场景下,如果使用安全上下文配置sysctl,kernel.sem属于unsafe类,需要管理员在kubelet层面显式允许,否则Pod创建直接被拒绝。综合来看,生产环境跑数据库最稳妥的做法是:先在宿主机层面把内核参数统一设置好,容器再按这个基线运行。
4.2 修改了sysctl.conf,重启后又变回默认值
这是最容易被"重启后复现"的坑。旧版本Linux发行版只看/etc/sysctl.conf,新版systemd系统会依次加载/usr/lib/sysctl.d、/run/sysctl.d、/etc/sysctl.d,并且后面的配置会覆盖前面的。如果你机器里存在多个配置文件同时定义了kernel.sem,很可能你写的那份被另一份覆盖,sysctl -p当时看是生效的,重启后系统按完整加载顺序重新解析,结果又变回默认值。
排查方法:
bash复制grep -r kernel.sem /etc/sysctl.conf /etc/sysctl.d/ /usr/lib/sysctl.d/ /run/sysctl.d/ 2>/dev/null
把每一处kernel.sem都列出来,确认最终生效的是你想要的那个值。另外注意不要用systemctl restart systemd-sysctl来验证,它只是重新加载一遍配置,真正的验证只有重启机器后看实际值。
4.3 参数调大依然报错:用SEMMNI排查思路
有次我调大了SEMMNS和SEMMSL,重启数据库还是报同样的错,当时一度怀疑数据库软件出了问题。后来用ipcs -u一看,used arrays已经顶到128,而最大arrays正好也是128。也就是说信号量总数还有富余,但集合数量上限被SEMMNI卡死了。
这种场景往往出现在大量短连接频繁起停数据库实例的情况中,每次实例启动创建集合,异常退出后集合又残留,时间一长集合数量不断累积。解决方向有两层:一是调大SEMMNI,从128改成512甚至更高;二是清理残留集合,从源头减少占用。两个动作一起做,否则单纯调大SEMMNI也只是把故障往后推迟。
4.4 信号量、共享内存、进程数限制别混为一谈
数据库启动失败时,几条报错经常一起出现,很多新手会当成同一件事处理。实际上它们是不同的内核资源:
| 报错关键字 | 根因方向 | 涉及参数 |
|---|---|---|
| 无法创建信号量 / could not create semaphores | System V信号量资源不足 | kernel.sem |
| 无法分配共享内存 / ORA-27102: out of memory | 共享内存段超限 | kernel.shmmax, kernel.shmall |
| Resource temporarily unavailable / fork failed | 进程数或线程数达到上限 | ulimit -u, kernel.pid_max, cgroup pids limit |
如果dmesg里同时有out of memory相关记录,还要排查物理内存和overcommit设置。信号量问题调kernel.sem,共享内存问题调kernel.shmmax/kernel.shmall,两者不要混着调,否则不仅解决不了问题,还会引入新的不稳定因素。
5. 如何让这类启动故障不再半夜找上门
5.1 配置层面:让数据库少依赖信号量
能通过配置减少信号量占用的,就不要让数据库敞开了要。Oracle的processes参数设置到实际峰值连接的1.2-1.5倍即可,不要随手写5000;连接数控制靠应用层连接池,而不是无限调大数据库进程上限。
PostgreSQL环境下,合理设置max_connections,并发控制交给PgBouncer这类连接池组件,避免每个业务连接都消耗一个后端进程。对信号量的需求自然就降下来了。
数据库实例异常退出后,自动清理残留信号量的能力有限,所以从运维流程上要养成习惯:实例停止后用ipcs -s快速检查一眼,有没有该清理没清理的集合。
5.2 巡检层面:把ipcs和kernel.sem纳入常规检查
我在巡检脚本里加了一个最简单的检查项,每天定时执行:
bash复制#!/bin/bash
threshold=70
semmns=$(awk '{print $2}' /proc/sys/kernel/sem)
semmni=$(awk '{print $4}' /proc/sys/kernel/sem)
used_arrays=$(ipcs -u | awk '/used arrays/ {print $NF}')
used_sems=$(ipcs -u | awk '/allocated semaphores/ {print $NF}')
echo "[sem] used arrays=${used_arrays:-0}/${semmni}, used sems=${used_sems:-0}/${semmns}"
array_rate=$(( used_arrays * 100 / semmni ))
sem_rate=$(( used_sems * 100 / semmns ))
if [ "$array_rate" -gt "$threshold" ] || [ "$sem_rate" -gt "$threshold" ]; then
echo "WARNING: kernel semaphore usage high (arrays ${array_rate}%, sems ${sem_rate}%)"
fi
把它放进crontab,每天凌晨低峰期跑一次,日志持续留存。当使用率超过70%就能提前收到告警,等到95%再处理已经进入故障区间了。
监控系统能力允许的话,可以把used arrays、used semaphores、kernel.sem四个值都采集进时序数据库,画一张趋势图。信号量使用量增长往往能提前反映出实例残留问题,比等启动失败再排查高效得多。
5.3 变更层面:IPC容量评估清单
数据库扩容、新增实例、同机部署中间件,这些变更之前都应该过一遍信号量容量评估。我自己的变更检查清单,供参考:
- 确认当前kernel.sem实际值,并确认它在持久化配置中的定义。
- 执行ipcs -u记录当前信号量使用基线。
- 评估本次变更新增的信号量需求,参考实例processes/max_connections等参数。
- 检查同机已有实例和中间件的峰值需求,算出总量是否超过SEMMNS/SEMMNI。
- 在变更窗口内调整kernel.sem并验证数据库启动。
- 更新环境基线文档,记录kernel.sem现值、使用率、变更日期。
这套流程不算复杂,但每次变更前花十分钟过一遍,能挡掉绝大部分启动失败类的低级故障。
从我个人的经验看,这类"无法创建信号量"问题,80%以上发生在环境变更后:要么是参数被重置,要么是新实例和旧实例叠加后超过系统上限,要么是异常残留没清理。它本身不是一个难解决的问题,难的是在报错信息很笼统的情况下快速定位准确的原因。把信号量机制和kernel.sem四个参数吃透,再配合一套固定的排查步骤,这个问题基本不会让你在半夜折腾超过半小时。
最后分享一个小技巧:我习惯在数据库启动脚本最前面加上两行检查,输出当前kernel.sem值和ipcs使用量。数据库启动失败时,这两行记录能帮你节省大量回溯时间,甚至不用登录系统就能从启动日志里直接看到问题所在。
