服务器入坑指北:进程与线程数上限
服务起不来,只报"资源暂时不可用",看不出是撞了哪一层;连接数并不高,进程或线程数却先到顶;同一个限制在两个地方都设过,不知道哪一个在生效。
顺序是:先分清"哪一层在拦" → 再算清"线程栈吃掉多少" → 再看"达上限的确切判据" → 最后才决定改哪一处。
在你动手调上限之前,先把两条前提看清:① 三处闸门取更小者生效——只看一处就断定,一定会有查不出的时候;② 服务由服务管理器启动,取的是它自己那一处限制——在 shell 里调过ulimit,不等于服务也调过。
这篇给三段可直接粘贴的内容:三处现读 → 算栈的账 → 达上限与读回。取值用占位符,参数名、字段名与错误码照写。
三处闸门,取更小者
"进程或线程不够用"可能来自三处,而且生效的是三者中最小的那个:
- 内核级:
pid_max(PID 上限)与threads-max(整机线程数上限)——这是闸门一; - 按用户:
RLIMIT_NPROC一族,也就是Max processes那一行——这是闸门二; - 控制组级:
pids.max——这是闸门三。
为什么要三处都读:它们互相独立,任何一处都可能先到顶。判据:三处都现读一遍,取更小的那个才是真正在拦你的。
三处现读块
cat /proc/sys/kernel/pid_max # 闸门一:PID 上限(版本相关,以现读为准)
sysctl kernel.pid_max # 闸门一:同一读值的 sysctl 形态,回显"名 = 值"
cat /proc/sys/kernel/threads-max # 闸门一:整机线程数上限(版本相关,以现读为准)
ulimit -u # 闸门二:当前会话的软限
ulimit -Hu # 闸门二:硬限——普通用户不能把软限抬过硬限
cat /proc/<PID>/limits # 闸门二:判据行是 Max processes(该进程的实际生效值)
cat /sys/fs/cgroup/<路径>/pids.max # 闸门三:组级上限(值为 max 表示不限;版本相关)
cat /sys/fs/cgroup/<路径>/pids.current # 闸门三:当前计数(线程也计入,可能大于 pids.max)
systemd-cgtop # 宿主级视角:按控制组排出的占用(需在运行 systemd 的宿主上;容器内通常没有)
走一遍"取更小者"(每个数都来自你上面的现读):
- 闸门一读出一个数(
pid_max与threads-max里较小的那个); - 闸门二读该进程实际生效的
Max processes(不是你在会话里设的那个); - 闸门三读
pids.max(读出max就按"不限"处理,这一支不参与比较); - 取三个数里的最小值——它是最可能先拦住你的那个"天花板"。注意别把"上限"读成"剩余":
Max processes与pid_max给的都是上限,不是"还能创建多少";真正还能再创建多少,要用它减去当前用量。
判据:只看一处就断定,一定会有查不出的时候——尤其"一处调大了还是不行"这种情况,说明生效的是另一处。
算栈的账
每个线程都占一段栈,所以线程栈占用量级 ≈ 每线程栈大小 × 线程数——它是乘出来的,不是一个个算的。
ulimit -s # 每线程栈大小(KB),来自 RLIMIT_STACK
ps -o nlwp= -p <PID> # 只输出线程数数值,便于与栈大小相乘
ps -eo nlwp,pid,comm --sort=-nlwp # 定位线程大户:第一列 nlwp 降序
ps -eLf # 全机线程视图:-L 让每行代表一个线程
grep -E 'VmSize|VmRSS' /proc/<PID>/status # 两个量级各读一次做对照
算一遍(数值为演示值):每线程栈 8192 KB、线程数 500 ⇒ 8192 * 500 = 4096000(KB);换算一下:4096000 / 1024 = 4000(MiB),也就是约 3.9 GiB 的量级。一个看起来"没吃多少内存"的服务,光栈就占了这么大一档。
这条账要记在虚拟量级上,不是常驻量级上——这是判据,不是补充说明:
- 栈的内存计入虚拟量级(看
VmSize); - 常驻量级只在页被实际触及时才增长(看
VmRSS)——栈是"要用了才真占页"; - 所以上面那行
grep -E 'VmSize|VmRSS'的对照会显示:虚拟量级明显涨了,常驻量级几乎没动。把账记在错的量上,就会得出"线程数没影响内存"的错误结论。
(与栈相关的还有另一道闸:vm.max_map_count,它是另一个维度,这里只指路。)
达上限的判据
创建路径失败时的错误码是 EAGAIN(文案是"资源暂时不可用"),而这个错误码的文档把"按用户对进程/线程数的限制"与"系统级任务数上限"都列为可能来源:
- 所以错误码本身指不出是哪一处——它只告诉你"创建失败了",分辨靠上面那三处现读;
- 内存不足时,
fork路径还可能返回另一个错误码(ENOMEM)——两者要分开看; - 控制组这一支有独立判据:
pids.events里的max计数——该计数递增,就说明这一层拦过。
cat /sys/fs/cgroup/<路径>/pids.events # 判据键=max 计数:「撞过 pids.max 的次数」(版本相关)
判据:错误码 + 控制组被拦计数互相印证——EAGAIN 出现、而 pids.events 的 max 计数没动,说明拦你的不是控制组这一层;计数在涨,就是它。
改哪一处:两条路的代价
- 提高上限(
sysctl -w kernel.pid_max=<值>、调Max processes、或按单元设TasksMax=):立刻可用;代价是失败会被推迟到更晚——到内存或其他资源先到顶时才爆; - 减少线程(线程池收敛、异步化):不涨任何上限;代价是要改应用。
判据:先算上面那笔栈的账,再决定提不提上限。两条路的代价都要写出来。
达上限与改与读回块
sysctl kernel.pid_max # 改前:记录原值(回滚依据)
sysctl -w kernel.pid_max=<值> # 运行时调整(高风险写操作,需回滚点)
cat /proc/<PID>/limits # 读回:Max processes 那一行变了没有
cat /sys/fs/cgroup/<路径>/pids.max # 读回:组级上限是不是你设的那个
服务侧要落在这两处(它们才是服务真正吃到的那一层):
[Service]
TasksMax=<值> # 该单元可创建的最大任务数(与 pids.max 对应)
TasksAccounting=<取值> # 记账开关:不开则 TasksMax 无从计起
持久化到按用户的限制,落点是 /etc/security/limits.conf 的 nproc 项——判据:该行生效于下次登录会话(只对经 PAM 的登录会话生效,对已经在跑的服务不追溯)。
读回是独立的一步,不许省:改完要重读那三处,确认生效的那一处确实变了,而不是你以为的那一处。改上限属高风险写操作,需回滚点。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 创建失败报"资源暂时不可用" | 撞到某处上限 | 看 EAGAIN,再读三处取值,取更小者 |
| 连接数不高但线程先到顶 | 线程也计入任务数 | 用 ps -eo nlwp,pid,comm --sort=-nlwp 找大户,再算栈的账 |
| 一处调大了还是不行 | 生效的是另一处 | 三处重读,找出最小值那一处 |
| 常驻量看着没涨却起不来更多线程 | 栈的账记错了量级 | 用 VmSize 与 VmRSS 各读一次做对照 |
| 控制组里被拦计数在涨 | 拦你的是控制组这一层 | 读 pids.events 的 max 计数,再调 pids.max 或 TasksMax= |
改完怎么验证:每项一条判据
- 三处现读值都能说出哪一处最小;
- 线程栈那条账能算出来(每线程栈 × 线程数),且记在虚拟量级上;
- 错误码与被拦计数互相印证;
- 改动后生效的那一处确实变了;
- 有回滚形态与原值记录。
这篇不解决什么
- 讲文件描述符与 inotify 上限的那篇解决"每个进程能开多少个句柄";本篇解决"整机与单元能有多少个执行单元"——两者报错都可能表现为"资源不可用",判定入口是:先看报错指向句柄数还是任务数。
- 讲cgroup v2 内存上限与 OOM 取证的那篇是内存维度的上限与取证;本篇是执行单元数量维度——不同资源,同一控制组机制。
- 讲内存泄漏的观察与处置顺序的那篇在"内存占用趋势"层;本篇在"执行单元数量"层,两篇无重叠。
踩坑清单
- 只看一处上限就下结论:三处取更小者,少读一处就查不出来。
- 在 shell 里调了
ulimit以为服务也调了:服务由管理器启动,取的是它自己那一处。 - 把栈的账记在常驻量级上:会得出"线程数不影响内存"的错结论。
- 只看错误码就断定是哪一处:
EAGAIN的文档把两处都列为来源。 - 提了上限却没算栈的账:失败只是被推迟,不是消失。
- 忘了控制组那一层:
pids.max与TasksMax=才是服务真正吃到的那一处。 - 把"进程数"与"句柄数"混为一谈:那是另一道闸,判据与入口都不同。
- 改前不记原值:出问题回不去。
- 点赞
- 收藏
- 关注作者
评论(0)