새벽 3시, 로그에 'Too many open files'가 찍혔다
개념 설명은 나중에. 지금은 서비스가 소켓을 못 열고 있고, 로그에 Too many open files(EMFILE, errno 24)가 계속 쌓이는 중이다. 이 런북은 에러 원문 매칭 → 상태 확인 명령 → 계층별 복붙 복구 순서로만 구성돼 있다. 위에서 아래로 순서대로 붙여넣으면 된다.
한 가지만 기억하자. 이 에러는 "파일 디스크립터(FD)"가 한계에 닿았다는 뜻이고, 문제는 항상 세션 / 프로세스(서비스) / 워커 / 커널 네 계층 중 하나에 있다. 어느 계층인지만 30초 안에 판별하면 복구는 명령 한두 줄이다.
적용 범위: Linux(RHEL/CentOS 79, Ubuntu 18.0424.04), systemd 기반 서비스, Nginx 1.1x, Node.js/Java 애플리케이션, Docker/containerd·K8s 컨테이너 환경.
30초 진단 — 에러 원문 3종 매칭표
로그에 찍힌 원문을 아래 표에서 찾으면 어느 계층 문제인지 바로 나온다.
| 에러 원문 (로그에서 검색) | 발생 계층 | 우선 확인 명령 |
|---|---|---|
bash: cannot create temp file for here-document: Too many open files | 세션(로그인 셸의 ulimit) | ulimit -Sn |
accept() failed (24: Too many open files) (Nginx error.log) | 워커 프로세스 | cat /proc/$(pgrep -o nginx)/limits | grep "open files" |
worker_connections are not enough + too many open files | Nginx 워커 설정 | nginx -T | grep -E 'worker_(rlimit_nofile|connections)' |
java.io.IOException: Too many open files | JVM 프로세스 | cat /proc/$(pgrep -f java)/limits | grep "open files" |
java.net.SocketException: Too many open files | JVM 프로세스(소켓 누수 의심) | lsof -p $(pgrep -f java) | grep -c 'TCP' |
Error: EMFILE: too many open files (Node.js) | Node 프로세스 | cat /proc/$(pgrep -f node)/limits | grep "open files" |
핵심 판별 규칙 한 줄:
- 셸에서 명령 실행조차 안 되면 → 세션 계층.
ulimit -n만 올리면 된다. - 서비스 로그에만 찍히고 셸은 멀쩡하면 → 프로세스/서비스 계층. systemd·nginx 설정을 봐야 한다.
file-nr이file-max에 근접했으면 → 커널 계층(전체 시스템). 이건 드물지만 치명적이다.
현재 상태 확인 명령 세트
복구 전에 "지금 얼마나 열려 있고 한계가 얼마인지"를 확인한다. 여기서 ulimit -n보다 /proc/PID/limits를 신뢰하라. 데몬은 로그인 셸과 다른 한계로 뜨는 경우가 많기 때문이다.
1) 실제 프로세스가 연 FD 수
# 앱 프로세스가 현재 열고 있는 FD 개수
lsof -p $(pgrep -f myapp) | wc -l예상 출력:
48322) 그 프로세스에 실제 적용된 한계 (가장 신뢰)
cat /proc/$(pgrep -f myapp)/limits | grep "open files"예상 출력:
Max open files 1024 4096 files
# ^soft(현재 적용) ^hard(상향 가능 최대)위 예시라면 soft가 1024이고 현재 4832개를 열려던 셈 → 한계 초과가 명백하다.
3) 세션 ulimit (셸 계층 확인용)
ulimit -Sn # soft limit
ulimit -Hn # hard limit예상 출력:
1024
1048576soft가 1024로 낮으면 셸에서 뜬 앱은 1024에 걸린다. hard가 크면 soft만 올리면 즉시 해결.
4) 시스템 전체 (커널 계층)
cat /proc/sys/fs/file-nr
# 출력: 사용중 미사용(할당됐다 반납) 최대치예상 출력:
9856 0 2097152
#할당 미사용 file-max첫 숫자가 세 번째(file-max)의 80%를 넘으면 커널 상한 자체가 부족한 것이다. 대부분은 여기까지 오지 않고 프로세스 계층에서 끝난다.
판단 기준 요약: lsof 개수 / soft limit가 0.8 이상이면 위험, 1.0 근접이면 이미 에러 발생 중.
계층별 복구 — 위에서 아래로 복붙
문제 계층에 해당하는 블록만 실행하면 된다. 어디까지 올려야 할지는 각 단계 끝의 분기를 보라.
① 세션 임시 상향 (지금 당장, 재부팅 시 사라짐)
ulimit -n 65535 # 현재 셸에만 적용
ulimit -n # 확인이 셸에서 재기동하는 프로세스에만 적용된다. 여기서 앱을 다시 띄우면 즉시 복구되지만, 재부팅하면 원상복귀 → 반드시 아래 영구 설정을 함께 한다.
분기: hard limit보다 큰 값은 일반 사용자가 못 올린다(
ulimit: value exceeds hard limit). 그럴 땐 root로 hard까지 올리거나 ②로 간다.
② 영구 설정 — /etc/security/limits.conf
sudo tee -a /etc/security/limits.conf <<'EOF'
* soft nofile 65535
* hard nofile 65535
root soft nofile 65535
root hard nofile 65535
EOFpam_limits가 활성화돼야 이 파일이 적용된다. 확인:
grep pam_limits /etc/pam.d/common-session /etc/pam.d/login 2>/dev/null
# 출력에 session required pam_limits.so 가 있어야 함주의: limits.conf는 로그인 세션에만 적용된다. 즉 systemd로 뜨는 데몬에는 안 먹는다. 이게 "limits.conf 고쳤는데 왜 안 돼?"의 8할이다. 그럴 땐 ③으로.
③ systemd 서비스 — LimitNOFILE (데몬은 여기가 정답)
Nginx, 애플리케이션 등 systemd가 관리하는 서비스는 unit의 LimitNOFILE이 우선한다. drop-in으로 안전하게 추가:
sudo systemctl edit myapp.service열린 편집기에 아래를 입력:
[Service]
LimitNOFILE=65535적용:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
# 검증 — 실제 적용됐는지 반드시 확인
cat /proc/$(pgrep -f myapp)/limits | grep "open files"예상 정상 결과:
Max open files 65535 65535 files분기: 여전히 1024로 나오면 →
daemon-reload를 빠뜨렸거나 drop-in 경로(/etc/systemd/system/myapp.service.d/override.conf)가 다른 unit을 가리킨 것.systemctl show myapp -p LimitNOFILE로 최종값을 확인하라.
④ Nginx — worker_rlimit_nofile ↔ worker_connections
Nginx는 systemd LimitNOFILE과 별개로 자체 지시어가 필요하다. worker_connections가 실제 열 수 있는 FD보다 크면 accept() failed (24)가 난다.
# /etc/nginx/nginx.conf 최상단(main 컨텍스트)
worker_rlimit_nofile 65535;
events {
worker_connections 16384; # worker_rlimit_nofile 이하로
}관계식(대략): 필요 FD ≈ worker_connections × 2 (클라이언트+업스트림) + 예비. 그래서 worker_rlimit_nofile은 worker_connections의 2배 이상 여유를 준다.
sudo nginx -t # 문법 검사
sudo systemctl reload nginx
cat /proc/$(pgrep -o nginx)/limits | grep "open files" # 65535 확인⑤ 커널 상한 — fs.file-max (전체가 부족할 때만)
file-nr이 file-max에 근접한 극단적 상황에서만:
# 즉시 적용
sudo sysctl -w fs.file-max=2097152
# 영구화
echo 'fs.file-max = 2097152' | sudo tee /etc/sysctl.d/99-nofile.conf
sudo sysctl --system대부분 서버는 기본 file-max가 이미 수백만이라 여기까지 올 일은 거의 없다. 이 단계까지 왔다면 십중팔구 FD 누수를 의심해야 한다. 아래로.
FD 누수 vs 한계 부족 — 재발을 막는 결정적 구분
한계만 올리고 끝내면 며칠 뒤 더 큰 값으로 또 터진다. 둘을 반드시 구분하라.
추이 관찰
# 5초마다 FD 개수 추이 관찰
watch -n5 'ls /proc/$(pgrep -f myapp)/fd | wc -l'- 재시작 직후부터 계속 우상향 → FD 누수. 코드에서 소켓/파일/커넥션을
close()안 하고 있다. - 평상시 낮다가 트래픽 피크에서만 한계 접근 → 한계 부족. 위 계층별 상향으로 끝.
누수 의심 시 FD 타입 집계
lsof -p $(pgrep -f myapp) | awk '{print $5}' | sort | uniq -c | sort -rn예상 출력:
38210 IPv4 # 소켓이 압도적 → 커넥션 close 누락 / keep-alive 과다
412 REG # 일반 파일
88 pipeIPv4/sock이 비정상적으로 많으면 → HTTP 클라이언트 커넥션 풀 미반환, DB 커넥션 미반환, keep-alive 설정을 점검한다. REG가 계속 늘면 → 파일 핸들을 안 닫는 코드다.
컨테이너·K8s 환경의 함정
호스트에서 ulimit -n을 올려도 컨테이너 안 프로세스는 별개다. Docker/containerd 기본 nofile이 호스트와 달라 혼선이 잦다.
# 컨테이너 실행 시 명시
docker run --ulimit nofile=65535:65535 myimage
# 실행 중 컨테이너 내부 실제 한계 확인
docker exec <cid> sh -c 'cat /proc/1/limits | grep "open files"'K8s는 노드의 containerd 기본값과 Pod의 securityContext를 함께 확인해야 한다. "호스트는 65535인데 왜 컨테이너는 1024?"라면 여기가 원인이다.
정상 / 이상 판별 기준표
| 지표 | 정상 | 주의 | 위험(조치) |
|---|---|---|---|
lsof 개수 / soft limit | < 50% | 50~80% | ≥ 80% → 상향 검토 |
| 재시작 후 FD 추이 | 안정(수평) | 완만한 증가 | 지속 우상향 → 누수 |
IPv4/sock 비율 | 워크로드에 비례 | 급증 추세 | 압도적 다수 → 커넥션 close 점검 |
file-nr 1열 / file-max | < 50% | 50~80% | ≥ 80% → fs.file-max 상향 |
이 표 기준으로 "지금 올릴지, 코드를 볼지"가 갈린다.
리소스 한계 트러블슈팅 — 함께 보면 좋은 런북
이 글은 "FD 한계" 편이다. 리소스 한계 장애는 대부분 한계 확인 → 임시 상향 → 영구 상향 → 누수 판별의 같은 4단 흐름으로 해결된다. 다른 자원도 같은 패턴으로:
fork: Resource temporarily unavailable— 프로세스/스레드 한계(nproc,pids.max) 편- PostgreSQL
too many clients already— DB 커넥션 한계(max_connections, 커넥션 풀) 편
세 편 모두 "한계인가 누수인가"를 구분하는 지점이 핵심이다.
자주 묻는 질문 (FAQ)
Q. limits.conf를 65535로 고쳤는데 서비스에는 왜 안 먹나요?
A. /etc/security/limits.conf는 로그인 세션(PAM)에만 적용됩니다. systemd로 뜨는 데몬은 unit의 LimitNOFILE이 우선하므로 systemctl edit으로 drop-in을 추가하고 daemon-reload && restart 해야 합니다. 적용 여부는 cat /proc/PID/limits로 확인하세요.
Q. ulimit -n을 올렸는데 재부팅하니 원래대로 돌아갑니다.
A. ulimit은 현재 셸에만 적용되는 임시 설정입니다. 영구화하려면 세션은 limits.conf, 데몬은 systemd LimitNOFILE, Nginx는 worker_rlimit_nofile을 각각 설정해야 합니다.
Q. 한계를 올려도 며칠 뒤 또 터집니다.
A. FD 누수 가능성이 높습니다. watch -n5 'ls /proc/PID/fd | wc -l'로 추이를 보고, 재시작 직후부터 계속 우상향이면 코드에서 소켓/파일/커넥션 close() 누락을 점검하세요. 한계 상향은 임시방편일 뿐입니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.