服务器入坑指北:进程与线程数上限

举报
Tulpartner 发表于 2026/11/08 09:55:14 2026/11/08
【摘要】 服务起不来,只报"资源暂时不可用",看不出是撞了哪一层;连接数并不高,进程或线程数却先到顶;同一个限制在两个地方都设过,不知道哪一个在生效。 在你动手调上限之前,先把两条前提看清:① 三处闸门取更小者生效——只看一处就断定,一定会有查不出的时候;② 服务由服务管理器启动,取的是它自己那一处限制——在 shell 里调过 ulimit,不等于服务也调过。

服务起不来,只报"资源暂时不可用",看不出是撞了哪一层;连接数并不高,进程或线程数却先到顶;同一个限制在两个地方都设过,不知道哪一个在生效。
顺序是:先分清"哪一层在拦" → 再算清"线程栈吃掉多少" → 再看"达上限的确切判据" → 最后才决定改哪一处。
在你动手调上限之前,先把两条前提看清:① 三处闸门取更小者生效——只看一处就断定,一定会有查不出的时候;② 服务由服务管理器启动,取的是它自己那一处限制——在 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 的宿主上;容器内通常没有)

走一遍"取更小者"(每个数都来自你上面的现读):

  1. 闸门一读出一个数(pid_max 与 threads-max 里较小的那个);
  2. 闸门二读该进程实际生效的 Max processes(不是你在会话里设的那个);
  3. 闸门三读 pids.max(读出 max 就按"不限"处理,这一支不参与比较);
  4. 取三个数里的最小值——它是最可能先拦住你的那个"天花板"。注意别把"上限"读成"剩余":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= 才是服务真正吃到的那一处。
  • 把"进程数"与"句柄数"混为一谈:那是另一道闸,判据与入口都不同。
  • 改前不记原值:出问题回不去。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。