朕喜技术(ZhenxiJS)
2026.09.05
1 分钟阅读

并发 Worker 频繁崩溃 "can't start new thread" —— RLIMIT_NPROC 隐藏限制排查

技术栈:Linux、Docker、Python threading、RLIMIT_NPROC / ulimit / nproc、cgroup、并发线程 关键词:can't start new thread / unable to create new native thread / pthread_create EAGAIN / RLIMIT_NPROC / ulimit -u / docker ulimits nproc / 线程创建失败 / 线程数上限 / per-user process limit 日期:2026-09-05


一、问题背景

运维监控告警:一个"每来一个任务就 new 一个线程"的并发调度服务,运行到任务高峰时突然崩溃退出,日志末尾出现:

RuntimeError: can't start new thread

对应 Java 进程经典报错是 java.lang.OutOfMemoryError: unable to create new native thread——本质上都是操作系统无法再创建新线程,pthread_create() 返回了 EAGAIN。

奇怪的是:内存、CPU 都很充裕,为什么创建不了线程?

二、复现步骤

环境:Docker Compose 复现

docker-compose.yml 关键配置(触发问题的条件):

ulimits:
  nproc: 300        # 压低 RLIMIT_NPROC
command: ["python3", "-u", "thread_spawner.py"]

复现命令:

docker compose up -d
docker compose logs app

输出:

[worker] unbounded 模式:target=20000

[worker] FAILED to create thread #0: can't start new thread
[worker] 线程创建失败,进程即将因并发模型缺陷而退出。

[worker] 本次共成功创建 0 个线程

关键现象:连第 0 个线程都创建失败!这说明不是简单的"线程数到了上限慢慢耗尽",而是从一开始就超出了进程/线程配额。

进一步排查:容器内 docker exec 执行任何需要 fork 的命令都报 Cannot fork 或 Resource temporarily unavailable。

单容器复现(更简洁)

docker run --rm --ulimit nproc=300 python:3.11-slim python3 -c "
import threading
threading.Thread(target=lambda: None).start()
print('thread created')
"
# RuntimeError: can't start new thread

三、根本原因

1. RLIMIT_NPROC 是 per-UID 限制,不是 per-process

RLIMIT_NPROC(对应 ulimit -u / max user processes)限制的是该 user namespace 中同一 UID 的所有进程 + 线程总数,而不是单个进程能创建的线程数。

2. Docker daemon 以 root 运行消耗了大量配额

在默认 Docker 环境(未启用 user namespace)下,容器内 UID 0(root)与宿主机 UID 0 共享同一个进程计数空间。

Docker daemon(dockerd + containerd + runc 等)以 root 运行,本身就创建了大量线程(在本环境中实测约 450+ 个)。

3. 容器 nproc=300 已经低于宿主机已用量

当容器设置 ulimits.nproc: 300 时,容器内 PID 1(root)的 RLIMIT_NPROC = 300。但由于宿主机 UID 0 已有约 450+ 个线程,300 远小于 450,容器内 PID 1 刚启动就超出了限制,任何 clone() 调用(创建线程/进程)都立即返回 EAGAIN。

探测方法(确定宿主机已用量)

# 用二分法测试不同 nproc 值
for n in 300 400 450 460 500; do
  docker run --rm --ulimit nproc=$n python:3.11-slim \
    python3 -c "import threading; threading.Thread(target=lambda: None).start(); print('nproc=$n OK')" 2>/dev/null || echo "nproc=$n FAIL"
done

输出显示:nproc=450 失败,nproc=460 成功 → 宿主机 UID 0 大约用了 450 个线程。

四、解决办法

方案1:环境层——去掉/调大 nproc 限制

修改 docker-compose.yml,删除或注释 ulimits.nproc 配置(继承 Docker 默认的 unlimited),或设置为足够大的值(如 nproc: 4096)。

# 删除以下配置
# ulimits:
#   nproc: 300

方案2:应用层——改用有界并发模型(最佳实践)

不要"每任务一线程"无限创建,使用信号量或线程池限制最大并发:

import threading

sem = threading.Semaphore(100)  # 最多同时 100 个并发

def worker():
    try:
        # 执行任务
        pass
    finally:
        sem.release()

for task in tasks:
    sem.acquire()  # 超过上限时阻塞等待
    t = threading.Thread(target=worker)
    t.start()

对应到本题的 thread_spawner.py,已内置 --bounded 模式,在 docker-compose.yml 中设置:

command: ["python3", "-u", "thread_spawner.py", "--bounded"]
environment:
  MAX_CONCURRENCY: "100"

五、验证

修复后重启服务:

docker compose up -d
docker compose logs -f app

输出:

[worker] bounded 模式:max_concurrency=100, target=20000
[worker] 已提交 1000 个任务,已完成 900 ...
...
[worker] 已提交 20000 个任务,已完成 19918 ...
[worker] 有界模式:20000 个任务全部完成,未触发线程创建失败

验证通过:20000 个任务全部成功处理,信号量将并发限制在 100 线程内,全程无 can't start new thread 错误。

六、经验与避坑

  1. 连第 0 个线程都创建失败 → 立即想到 RLIMIT_NPROC 已被同 UID 的宿主机进程占满(Docker daemon 消耗了大量线程)。这种情况不是"慢慢耗尽",而是从一开始就超出限制。
  2. docker exec 报 Cannot fork → 即使容器内 /proc 只显示极少数进程,也可能是 RLIMIT_NPROC 的 per-UID 限制被其他进程占满导致。
  3. RLIMIT_NPROC 的计数范围:如果 Docker 未启用 --userns-remap(user namespace),容器内 UID 0 与宿主机 UID 0 共享计数空间。要用 docker run --userns=host 或检查 Docker daemon 的 userns-mode 配置。
  4. 排查命令清单:
    • cat /proc/self/limits — 查看 RLIMIT_NPROC
    • ulimit -u — 查看 max user processes
    • docker exec <ctr> ls /proc | grep -c "^[0-9]" — 看容器内进程数
    • 改变 nproc 值做二分法探测,确定宿主机同 UID 已用进程数
  5. 最佳实践:生产环境服务绝不应使用"每任务一线程"的无界并发模型。用 ThreadPoolExecutor、Semaphore 或连接池等有界机制限制并发度,避免线程无上限增长最终打满任何系统限制(RLIMIT_NPROC、cgroup pids、栈内存)。
  6. Docker 默认 nproc 是 unlimited:容器内默认 RLIMIT_NPROC = -1(unlimited)。docker-compose.yml 里主动设置 ulimits.nproc 时要格外小心——不要设得太低。

七、可复现命令汇总

# 复现问题(nproc=300)
docker run --rm --ulimit nproc=300 python:3.11-slim \
  python3 -c "import threading; threading.Thread(target=lambda: None).start()"
# → RuntimeError: can't start new thread

# 确认正常(不设限制)
docker run --rm python:3.11-slim \
  python3 -c "import threading; [threading.Thread(target=lambda: None).start() for _ in range(100)]; print('100 threads OK')"
# → 100 threads OK

# 完整场景复现
cd scenario/2026-09-05-0800
docker compose up -d
docker compose logs app

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-05-0825-can-not-start-new-thread-rlimit-nproc

分享这篇文章