fork: retry: Resource temporarily unavailable 30초 진단 런북
지금 터미널이나 로그에서 아래 셋 중 하나를 보고 복붙 검색으로 들어왔다면, 결론부터 말합니다. 거의 항상 둘 중 하나입니다 — nproc(프로세스/스레드 생성) 한도 고갈, 아니면 메모리 부족.
bash: fork: retry: Resource temporarily unavailable(셸/스크립트)pthread_create failed (EAGAIN)(C/네이티브)java.lang.OutOfMemoryError: unable to create new native thread(JVM)
이름과 표현은 다르지만 셋 다 커널이 새 실행 흐름을 만들려다 EAGAIN(=Resource temporarily unavailable)을 돌려준 결과입니다. 즉 메시지가 달라도 뿌리는 동일합니다. 아래 순서대로 명령만 던지면 30초 안에 "한도냐 메모리냐"가 갈립니다.
| 단계 | 명령 | 판정 |
|---|---|---|
| 1 | ulimit -u vs ps -eLf 카운트 | 한도≈사용량 → nproc 고갈 |
| 2 | free -h / dmesg | grep oom | 메모리 빠듯·OOM → 메모리 부족 |
| 3 | systemctl show / pids.max | 영구 상향 위치 결정 |
30초 1차 분기: nproc 한도부터 때려본다 🟢
설명은 나중에. 일단 진단부터 (전부 읽기 전용, 안전):
# 🟢 현재 셸의 soft / hard nproc 한도
ulimit -u
ulimit -Hu
# 🟢 특정 유저가 지금 쓰고 있는 "스레드(LWP) 총합"
ps -eLf | grep '^appuser ' | wc -l
# 또는 전체 LWP 수
ps -eLf --no-headers | wc -l
# 🟢 문제 프로세스 1개의 스레드 수
ps -o nlwp= -p <pid>
cat /proc/<pid>/status | grep Threads
# 🟢 시스템 전역 천장 (개별 유저 한도와 별개)
cat /proc/sys/kernel/threads-max
cat /proc/sys/kernel/pid_max판정 기준은 단순합니다. ulimit -u로 나온 값과 ps -eLf | grep <user> | wc -l 값이 거의 같으면(예: 한도 4096, 사용 4090) → nproc 한도 고갈 확정입니다. 여기서 핵심은 리눅스의 RLIMIT_NPROC이 "프로세스 수"가 아니라 사실상 유저별 스레드(LWP) 총합을 센다는 점입니다. 그래서 멀티스레드 런타임(JVM, Go, Node 워커)이 스레드를 폭증시키면 프로세스 개수는 몇 개 안 돼도 한도에 부딪힙니다.
2차 분기: 한도는 넉넉한데 또 터진다 → 메모리 부족 🟡
ulimit -u는 만 단위인데도 fork가 실패한다? 그럼 두 번째 용의자는 메모리입니다.
# 🟡 가용 메모리 / 스왑 — available가 바닥이면 의심
free -h
# 🟡 OOM Killer가 다녀갔는지 흔적 확인
dmesg -T | grep -i -E 'oom|out of memory|killed process'
# 🟡 스택 크기 확인 (스레드당 가상메모리 소비량)
ulimit -s
cat /proc/sys/vm/max_map_count
cat /proc/sys/vm/overcommit_memorypthread_create는 스레드마다 스택(기본 ulimit -s, 보통 8MB)을 가상메모리로 예약합니다. 8MB × 수천 스레드 = 수십 GB 가상메모리 요구가 overcommit 정책이나 max_map_count에 막혀 EAGAIN이 나는 것이죠. Java라면 스레드 스택은 -Xss로 잡히고, 이 메모리는 힙(-Xmx) 바깥의 네이티브 영역에서 나옵니다. 그래서 unable to create new native thread는 힙이 멀쩡해도 터집니다 — -Xmx를 키우면 오히려 네이티브 여유가 줄어 악화될 수 있습니다.
실무 팁(경험담): JVM에서 이 에러가 나면 반사적으로
-Xmx부터 올리는 분이 많은데, 십중팔구 역효과입니다. 먼저 스레드 수(jstack <pid> | grep -c 'java.lang.Thread.State')를 세서 커넥션 풀·스레드 풀이 새는지 확인하고, 그래도 정상 부하면-Xss를 512k로 낮춰 스레드당 메모리부터 줄이는 게 빠른 응급 처치였습니다.
라벨링: nproc 멀쩡 + 메모리 빠듯/OOM 흔적 = 네이티브 메모리·스택 고갈.
3종 에러 = 1원인 매핑
| 보이는 메시지 | 발생 위치 | 실제 원인 |
|---|---|---|
fork: retry: Resource temporarily unavailable | bash/셸 | RLIMIT_NPROC 또는 메모리로 fork() EAGAIN |
pthread_create failed (EAGAIN) | C/네이티브 | nproc 한도 또는 스택×스레드 가상메모리 |
OutOfMemoryError: unable to create new native thread | JVM | 위 두 가지가 JVM에서 표출된 것 |
영구 해결: limits.conf vs systemd TasksMax 함정 🔴
원인을 잡았으면 한도를 올립니다. 단, 어디서 떴느냐에 따라 고칠 파일이 완전히 다릅니다. 적용 전 영역을 반드시 확인하세요.
(a) 가장 흔한 함정: limits.conf가 systemd 서비스엔 안 먹힌다
# /etc/security/limits.conf 또는 limits.d/*.conf 🔴 (재로그인 필요)
appuser soft nproc 65536
appuser hard nproc 65536이건 PAM 로그인 세션(SSH, su 등)에만 적용됩니다. systemctl start로 뜬 서비스는 PAM을 거치지 않으므로 이 설정이 무시됩니다. "limits.conf 다 고쳤는데 왜 안 되지?"의 99%가 이 케이스입니다.
# 🟢 서비스에 실제로 적용된 값 확인 — 진실은 여기에
systemctl show myapp.service -p TasksMax -p LimitNPROCDefaultTasksMax는 보통 시스템 nproc의 15%로 잡혀 있어, RHEL 8/9·Ubuntu 22.04+에서 스레드 천장으로 자주 걸립니다.
# 🔴 /etc/systemd/system/myapp.service.d/override.conf (drop-in 권장)
[Service]
TasksMax=infinity
LimitNPROC=65536# 🔴 적용 (daemon-reload + 재시작 필수, 롤백: drop-in 파일 삭제 후 동일 절차)
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p TasksMax -p LimitNPROC # 검증(b) 컨테이너 차이 박스 📦
호스트
ulimit -u는 충분한데 파드/컨테이너 안에서만 fork 실패한다면? 범인은 ulimit이 아니라 **cgrouppids.max**입니다.
# cgroup v2 (최신 배포판 기본) 🟢
cat /sys/fs/cgroup/<슬라이스경로>/pids.max
# cgroup v1 🟢
cat /sys/fs/cgroup/pids/<경로>/pids.max- Docker:
docker run --pids-limit=4096 ...(기본은 무제한이지만 cgroup 천장에 걸림) - Kubernetes: kubelet
--pod-max-pids또는 노드 설정으로 파드별 PID 제한
결론: 분기표 한 장으로 끝내기
fork/pthread/native thread 에러
│
ulimit -u ≈ ps -eLf 사용량?
├─ YES → nproc 한도 고갈 → limits.conf(로그인) / TasksMax(systemd) / pids.max(컨테이너) 상향
└─ NO → free·dmesg에 OOM?
├─ YES → 메모리 부족 → 스레드 누수 점검 / -Xss·ulimit -s 축소 / 메모리 증설
└─ NO → 전역 천장(threads-max, pid_max) 확인이 에러는 자원고갈 클러스터의 한 형제일 뿐입니다. 고갈된 자원만 다르고 진단 방식은 똑같습니다 — Too many open files(파일 디스크립터/EMFILE), No space left on device(inode·디스크), OutOfMemoryError: Java heap space(힙), connection refused(백로그·포트). "무엇이 고갈됐나"만 바꿔 같은 런북을 돌리면 됩니다.
자주 묻는 질문 (FAQ)
Q. limits.conf에서 nproc을 올렸는데 서비스가 계속 fork 실패합니다. 왜인가요?
A. systemd로 뜬 서비스는 PAM을 거치지 않아 limits.conf가 적용되지 않습니다. systemctl show <svc> -p TasksMax -p LimitNPROC로 실제값을 확인하고, 유닛 drop-in에 TasksMax=와 LimitNPROC=를 넣은 뒤 daemon-reload+restart 하세요.
Q. 호스트 ulimit은 충분한데 컨테이너 안에서만 터집니다.
A. 컨테이너의 진짜 천장은 호스트 ulimit이 아니라 cgroup pids.max입니다. /sys/fs/cgroup/.../pids.max를 확인하고 Docker는 --pids-limit, 쿠버네티스는 kubelet의 PID 제한을 조정하세요.
Q. Java에서 -Xmx를 올렸더니 native thread 에러가 더 자주 납니다.
A. 정상입니다. 스레드 스택은 힙 바깥 네이티브 메모리에서 나오므로 힙을 키우면 네이티브 여유가 줄어듭니다. 스레드 수가 비정상이면 누수를 잡고, 정상 부하면 -Xss를 낮춰 스레드당 메모리를 줄이세요.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.