- Published on
Too many open files 완전 해결 — ulimit을 올려도 안 되는 이유
- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — accept4() failed (24: Too many open files)
증상은 이렇게 나타납니다.
2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)
2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)
또는 자바라면 이렇습니다.
java.net.SocketException: Too many open files
at java.base/sun.nio.ch.Net.accept(Native Method)
Caused by: java.io.IOException: Too many open files
여기서 거의 모든 사람이 같은 일을 합니다. ulimit -n 65536을 실행하고, 서비스를 재시작하고, 여전히 같은 에러를 봅니다. 그 이유는 한계가 하나가 아니라 셋이고, 셸에서 바꾼 값이 서비스에는 적용되지 않기 때문입니다.
errno 24는 EMFILE입니다. 문구를 정확히 읽어야 합니다.
errno 24 23
# EMFILE 24 Too many open files
# ENFILE 23 Too many open files in system
in system이 붙으면 다른 문제입니다. 이 차이 하나가 진단의 절반을 갈라 줍니다.
한계는 하나가 아니라 셋입니다
세 가지가 각각 독립적으로 존재하고, 어느 하나라도 걸리면 파일을 열 수 없습니다.
| 한계 | 범위 | 초과 시 errno | 확인 | 설정 위치 |
|---|---|---|---|---|
| RLIMIT_NOFILE soft | 프로세스 하나 | EMFILE (24) | /proc/PID/limits | ulimit -n, limits.conf, systemd LimitNOFILE |
| RLIMIT_NOFILE hard | 프로세스 하나 | 설정 거부 | /proc/PID/limits | 위와 같음, 낮추기만 비특권 가능 |
| fs.nr_open | 커널 전역 | EPERM/EINVAL | sysctl fs.nr_open | /etc/sysctl.d/ |
| fs.file-max | 시스템 전체 | ENFILE (23) | cat /proc/sys/fs/file-nr | /etc/sysctl.d/ |
관계를 정리하면 이렇습니다.
fs.file-max : 시스템 전체가 동시에 열 수 있는 파일 개수 (모든 프로세스의 합)
└─ fs.nr_open : 한 프로세스의 RLIMIT_NOFILE이 넘을 수 없는 천장
└─ RLIMIT_NOFILE hard : 관리자가 정한 그 프로세스의 상한
└─ RLIMIT_NOFILE soft : 실제로 적용되는 값. 프로세스가 hard까지 스스로 올릴 수 있음
현재 상태를 한 번에 봅니다.
sysctl fs.file-max fs.nr_open
# fs.file-max = 6553600
# fs.nr_open = 1048576
cat /proc/sys/fs/file-nr
# 13984 0 6553600
file-nr의 세 숫자는 각각 할당된 파일 핸들 수, 할당됐지만 미사용인 수(최근 커널에서는 항상 0), 최대값입니다. 첫 번째가 세 번째에 근접하면 시스템 전역 문제이고 이때 나오는 것이 ENFILE입니다. 위 예에서는 13984 대 655만이므로 여유가 압도적입니다. 대부분의 현대 리눅스에서 fs.file-max는 병목이 아닙니다. 커널이 메모리 크기에 비례해 자동 계산하기 때문입니다. 그런데도 많은 튜닝 문서가 이 값부터 올리라고 합니다. 올려도 무해하지만 증상은 그대로입니다.
fs.nr_open은 다릅니다. 기본값 1048576을 넘는 ulimit -n은 설정 자체가 실패합니다.
ulimit -n 2000000
# bash: ulimit: open files: cannot modify limit: Operation not permitted
sudo sysctl -w fs.nr_open=2097152
# fs.nr_open = 2097152
ulimit -n 2000000 # 이제 성공합니다
실제로 적용된 값 확인 — ulimit이 아니라 /proc/PID/limits
여기가 핵심입니다. ulimit -n은 지금 이 셸의 값을 보여 줍니다. 문제가 나는 프로세스의 값이 아닙니다. 둘은 자주 다릅니다.
ulimit -n
# 1048576
pgrep -x nginx
# 14022
# 14025
sudo grep 'Max open files' /proc/14025/limits
# Max open files 1024 524288 files
셸은 100만인데 nginx 워커의 soft 한계는 1024입니다. 이 차이가 문제의 정체입니다. 앞으로 이 증상을 만나면 ulimit -n은 아예 보지 말고 이 명령부터 실행하십시오.
# 프로세스 이름으로 한 번에
for pid in $(pgrep -x nginx); do
printf '%6s %s\n' "$pid" "$(awk '/Max open files/ {print $4, $5}' /proc/"$pid"/limits)"
done
# 14022 1024 524288
# 14025 1024 524288
지금 몇 개를 쓰고 있는지도 같이 봅니다.
# 실제 열린 fd 개수 (가장 정확)
sudo ls /proc/14025/fd | wc -l
# 1021
# 사용률을 한 줄로
for pid in $(pgrep -x nginx); do
used=$(sudo ls /proc/"$pid"/fd 2>/dev/null | wc -l)
soft=$(awk '/Max open files/ {print $4}' /proc/"$pid"/limits)
printf '%6s %6s / %-8s (%d%%)\n' "$pid" "$used" "$soft" $((used * 100 / soft))
done
# 14022 1021 / 1024 (99%)
# 14025 1019 / 1024 (99%)
lsof -p PID | wc -l을 쓰면 숫자가 더 크게 나옵니다. lsof는 현재 작업 디렉터리, 루트 디렉터리, 실행 파일, 메모리 매핑된 라이브러리까지 함께 세는데 이것들은 RLIMIT_NOFILE에 포함되지 않습니다. 한계와 비교할 때는 /proc/PID/fd의 개수를 쓰십시오.
systemd가 ulimit 설정을 무시하는 이유
가장 흔한 함정입니다. 다음 설정을 넣고 재시작했는데 아무것도 바뀌지 않은 경험이 있다면 이 절이 답입니다.
cat /etc/security/limits.d/99-nofile.conf
# * soft nofile 1048576
# * hard nofile 1048576
이 파일은 PAM 모듈 pam_limits가 읽습니다. 그리고 pam_limits는 로그인 세션에서만 동작합니다. SSH 접속, su, login이 여기 해당합니다. systemd가 부팅 시 띄우는 시스템 서비스는 로그인 세션이 아니므로 이 파일을 아예 읽지 않습니다.
서비스의 한계를 정하는 것은 systemd입니다. 우선순위는 유닛의 LimitNOFILE, 없으면 /etc/systemd/system.conf의 DefaultLimitNOFILE, 그것도 없으면 systemd 내장 기본값입니다.
# 유닛에 적용된 값 (하드와 소프트가 따로 노출됩니다)
systemctl show -p LimitNOFILE -p LimitNOFILESoft nginx.service
# LimitNOFILE=524288
# LimitNOFILESoft=1024
# 전역 기본값
systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft
# DefaultLimitNOFILE=524288
# DefaultLimitNOFILESoft=1024
여기서 systemd 240 이후의 기본 동작을 알아 둘 필요가 있습니다. systemd는 하드 한계를 524288로 크게 잡아 두고 소프트 한계는 1024로 낮게 유지합니다. 이유는 호환성입니다. select()는 fd 번호 1024 이상을 다루지 못하고, 일부 오래된 프로그램은 시작할 때 0부터 소프트 한계까지 모든 fd를 닫는 루프를 도는데 그 한계가 100만이면 기동이 몇 초씩 걸립니다. 그래서 "필요한 프로그램이 스스로 올려 쓰라"는 것이 systemd의 설계 의도입니다.
실제로 현대적인 런타임 상당수가 그렇게 합니다. Go는 런타임이 시작 시 소프트를 하드까지 올리고, 그래서 같은 머신에서 Go 서비스는 멀쩡한데 다른 서비스만 EMFILE이 나는 상황이 생깁니다.
올바른 설정 방법입니다.
sudo systemctl edit nginx.service
# 열린 편집기에 아래를 씁니다
# [Service]
# LimitNOFILE=65536
sudo systemctl daemon-reload
sudo systemctl restart nginx
두 가지를 강조합니다. systemctl reload로는 rlimit이 바뀌지 않습니다. 프로세스 생성 시점에 적용되는 값이므로 반드시 restart여야 합니다. 그리고 LimitNOFILE=65536처럼 값을 하나만 쓰면 소프트와 하드가 모두 그 값이 됩니다. 따로 주려면 LimitNOFILE=65536:524288 형식을 씁니다.
적용을 확인합니다.
sudo grep 'Max open files' /proc/$(pgrep -x nginx | head -1)/limits
# Max open files 65536 65536 files
애플리케이션 자체 설정이 또 있는 경우도 있습니다. nginx는 worker_rlimit_nofile이라는 지시어를 따로 가지고 있고, 이 값이 systemd가 준 하드 한계를 넘지 못합니다.
# /etc/nginx/nginx.conf
worker_rlimit_nofile 65536;
events {
worker_connections 32768;
}
worker_connections가 worker_rlimit_nofile보다 크면 의미가 없습니다. 연결 하나가 클라이언트 소켓 하나와 업스트림 소켓 하나를 쓸 수 있으므로, 리버스 프록시라면 worker_rlimit_nofile을 worker_connections의 두 배 이상으로 잡는 것이 안전합니다.
재시작 없이 급한 불을 끄는 방법도 있습니다.
sudo prlimit --pid 14025 --nofile=65536:65536
sudo prlimit --pid 14025 --nofile
# RESOURCE DESCRIPTION SOFT HARD UNITS
# NOFILE max number of open files 65536 65536 files
prlimit은 실행 중인 프로세스의 한계를 바꿉니다. 다만 애플리케이션이 기동 시에 한계를 읽어 내부 자료구조 크기를 정했다면(nginx의 커넥션 풀이 그렇습니다) 효과가 없을 수 있습니다. 진짜 해결은 유닛 파일 수정과 재시작입니다.
컨테이너의 규칙은 또 다릅니다. 컨테이너 안 프로세스의 RLIMIT_NOFILE은 OCI 런타임 스펙의 process.rlimits에서 오고, 그 값은 컨테이너 런타임 데몬이 정합니다. 셸의 ulimit도, 호스트의 limits.conf도 관여하지 않습니다.
# 컨테이너 안에서 확인하는 것이 유일하게 확실한 방법입니다
docker exec api-server cat /proc/1/limits | grep 'Max open files'
# Max open files 1048576 1048576 files
# 컨테이너별로 지정
docker run --ulimit nofile=65536:65536 myimage
# 데몬 전역 기본값
cat /etc/docker/daemon.json
# {
# "default-ulimits": { "nofile": { "Name": "nofile", "Soft": 65536, "Hard": 65536 } }
# }
쿠버네티스 파드 스펙에는 ulimit 필드가 없습니다. 파드 단위로 조정하려면 컨테이너 진입점에서 ulimit -n으로 낮추거나(하드 이하로만 가능), 노드의 containerd 설정을 바꾸거나, 컨테이너 런타임 유닛의 LimitNOFILE을 조정해야 합니다. 런타임 유닛이 LimitNOFILE=infinity인 배포판이 많고, 그 경우 컨테이너가 매우 큰 값을 물려받습니다. 넉넉해서 좋아 보이지만 앞서 말한 "fd 전부 닫기" 루프를 도는 프로그램에서는 기동이 수십 초 걸리는 원인이 됩니다.
무엇이 fd를 먹고 있는가 — 소켓, 파일, 파이프 세분
한계를 올리기 전에 무엇이 차지하는지 봐야 합니다. /proc/PID/fd의 심볼릭 링크 대상이 종류를 알려 줍니다.
sudo ls -l /proc/14025/fd | awk '{print $NF}' \
| sed -E 's#^(socket|pipe|anon_inode):.*#\1#; s#^/.*#regular-file#' \
| sort | uniq -c | sort -rn
# 61204 socket
# 3891 regular-file
# 412 anon_inode
# 8 pipe
# 3 /dev/null
소켓이 6만 개면 네트워크 쪽입니다. 어떤 소켓인지 더 파고듭니다.
# 상태별 소켓 개수
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 38210 ESTAB
# 21044 CLOSE-WAIT
# 2118 TIME-WAIT
# 412 LISTEN
CLOSE-WAIT이 2만 개입니다. 이것이 fd 누수의 결정적 증거입니다. CLOSE-WAIT은 상대가 FIN을 보내 연결을 닫았는데 우리 쪽 애플리케이션이 close()를 호출하지 않은 상태입니다. 커널은 애플리케이션이 닫아 주기를 기다리며 무한정 이 상태로 남습니다. 타임아웃이 없습니다. TIME-WAIT과 혼동하지 마십시오. TIME-WAIT은 커널이 알아서 정리하고 fd도 소비하지 않습니다.
어느 상대와의 연결인지 보면 원인 코드가 좁혀집니다.
sudo ss -tanp state close-wait | head -5
# Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# 1 0 10.0.3.14:44120 10.0.9.31:6379 users:(("java",pid=21874,fd=1042))
# 1 0 10.0.3.14:44122 10.0.9.31:6379 users:(("java",pid=21874,fd=1043))
# 상대 주소별 집계
sudo ss -tan state close-wait | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
# 20981 10.0.9.31
# 63 10.0.9.42
전부 같은 상대(Redis)입니다. Redis 클라이언트 풀이 연결을 반환하지 않고 있다는 뜻이고, 여기서 코드 리뷰 대상이 결정됩니다.
파일 쪽이 범인이라면 어느 파일인지 셉니다.
sudo ls -l /proc/21874/fd | awk '$NF ~ /^\// {print $NF}' \
| sed -E 's#/[^/]+$##' | sort | uniq -c | sort -rn | head -5
# 3204 /var/lib/app/uploads/tmp
# 512 /data/kafka/logs/orders-0
# 88 /usr/lib/x86_64-linux-gnu
/var/lib/app/uploads/tmp에 3204개가 열려 있습니다. 임시 파일을 만들고 닫지 않는 코드 경로가 있다는 뜻입니다.
inotify와 epoll은 별개의 한계입니다
anon_inode 계열은 RLIMIT_NOFILE도 소비하지만 자체 상한이 따로 있습니다. 그래서 fd에 여유가 충분한데도 실패하는 일이 생깁니다.
inotify의 상한은 세 개입니다.
sysctl fs.inotify
# fs.inotify.max_queued_events = 16384
# fs.inotify.max_user_instances = 128
# fs.inotify.max_user_watches = 65536
max_user_instances는 사용자당 만들 수 있는 inotify 인스턴스(inotify_init) 개수이고 초과하면 EMFILE입니다. ulimit -n과 문구가 완전히 같은 에러가 나지만 원인이 다릅니다. max_user_watches는 사용자당 감시 가능한 경로 개수이고 초과하면 ENOSPC, 즉 "No space left on device"가 나옵니다. 디스크와 무관한데 디스크 에러처럼 보입니다.
두 상한 모두 사용자 단위라는 점이 중요합니다. 같은 UID로 도는 컨테이너 수십 개가 하나의 예산을 나눠 씁니다. 노드에 파드를 늘렸더니 갑자기 감시 기반 도구가 죽는 전형적인 패턴이 여기서 나옵니다.
현재 사용량은 이렇게 셉니다.
# 인스턴스를 쥔 프로세스
sudo find /proc/[0-9]*/fd -lname 'anon_inode:*inotify*' 2>/dev/null \
| cut -d/ -f3 | sort | uniq -c | sort -rn | head -5
# 42 9931
# 8 3311
# 2 1042
# 실제 watch 개수 (fdinfo의 inotify 줄 수가 곧 watch 수입니다)
sudo grep -c '^inotify' /proc/9931/fdinfo/* 2>/dev/null | grep -v ':0$' | head
# /proc/9931/fdinfo/18:31204
# /proc/9931/fdinfo/22:8102
cat /etc/sysctl.d/60-inotify.conf
# fs.inotify.max_user_watches = 524288
# fs.inotify.max_user_instances = 1024
sudo sysctl --system
epoll에도 별도 상한이 있습니다.
cat /proc/sys/fs/epoll/max_user_watches
# 3618421
이 값은 사용자당 등록 가능한 epoll 감시 항목 수이고, 커널이 저부역 메모리의 약 4%를 기준으로 자동 산정합니다. 항목 하나가 커널 메모리를 수십 바이트 쓰기 때문에 무제한이 아닌 것입니다. 실무에서 이 한계에 부딪히는 경우는 드물지만, 수백만 커넥션을 다루는 프록시라면 확인 대상입니다.
정리하면 이렇습니다.
| 자원 | 상한 | 단위 | 초과 시 errno |
|---|---|---|---|
| 일반 fd | RLIMIT_NOFILE | 프로세스 | EMFILE (24) |
| 시스템 전체 파일 | fs.file-max | 시스템 | ENFILE (23) |
| inotify 인스턴스 | fs.inotify.max_user_instances | 사용자 | EMFILE (24) |
| inotify watch | fs.inotify.max_user_watches | 사용자 | ENOSPC (28) |
| epoll watch | fs.epoll.max_user_watches | 사용자 | ENOSPC (28) |
진짜 원인은 대개 누수 — 확인법과 응급 처치
한계를 올려서 해결되는 경우는 두 가지뿐입니다. 실제로 동시 연결이 그만큼 필요한 경우, 그리고 기본값이 비상식적으로 낮은 경우입니다. 나머지는 전부 누수이고, 누수에 한계를 올리는 것은 장애 시점을 몇 시간 뒤로 미루는 일입니다.
누수의 정의는 단순합니다. fd 사용량이 부하와 무관하게 단조 증가하면 누수입니다. 부하와 함께 오르내리면 정상적인 용량 문제입니다. 이것을 가르는 데는 몇 분간의 샘플링이면 충분합니다.
PID=21874
while sleep 60; do
printf '%s fd=%-7s estab=%-7s close_wait=%s\n' \
"$(date +%H:%M:%S)" \
"$(sudo ls /proc/$PID/fd | wc -l)" \
"$(ss -tan state established | wc -l)" \
"$(ss -tan state close-wait | wc -l)"
done
# 14:02:11 fd=18204 estab=8102 close_wait=9931
# 14:03:11 fd=19388 estab=8044 close_wait=11102
# 14:04:11 fd=20511 estab=7981 close_wait=12290
# 14:05:11 fd=21702 estab=8120 close_wait=13401
연결 수(estab)는 8000 근처에서 평평한데 fd는 분당 1200씩 오릅니다. 그리고 증가분이 close_wait의 증가분과 정확히 같습니다. 소켓 누수가 확정됐고, 앞 절의 ss -tanp로 상대까지 특정됐다면 조사할 코드 범위는 아주 좁습니다.
어느 코드 경로가 여는지까지 잡으려면 추적이 필요합니다.
# 열리는 파일과 호출 스택 (bcc 도구)
sudo opensnoop-bpfcc -p 21874
# fd를 여는 시스템 콜 빈도
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_openat /pid == 21874 && args->ret > 0/ { @[ustack] = count(); }'
# strace는 부하가 크므로 짧게만
sudo timeout 5 strace -f -e trace=openat,socket,close -p 21874 -c
# % time seconds usecs/call calls errors syscall
# ------ ----------- ----------- --------- --------- ----------------
# 61.02 0.184203 18 10233 socket
# 38.44 0.116041 21 5488 openat
# 0.54 0.001632 4 402 close
socket이 10233번인데 close가 402번입니다. 비율만 봐도 답이 나옵니다.
응급 처치의 선택지는 세 가지이고 순서가 있습니다.
# 1) 프로세스 재시작 — 가장 확실하지만 서비스 중단
sudo systemctl restart myapp
# 2) prlimit으로 한계만 올려 시간을 벌기 (재시작 없음)
sudo prlimit --pid 21874 --nofile=200000:200000
# 3) 특정 fd를 강제로 닫기 — 최후의 수단, 애플리케이션이 깨질 수 있습니다
sudo gdb -p 21874 -batch -ex 'call (int)close(1042)'
3번은 애플리케이션이 그 fd를 여전히 유효하다고 믿고 있는 상태에서 닫는 것이므로 데이터 손상이나 크래시를 각오해야 합니다. 프로덕션에서 마지막으로 남은 선택지일 때만 쓰십시오.
마치며 — 세 개의 한계와 하나의 진짜 원인
하나만 기억한다면 이것입니다. ulimit -n은 지금 이 셸의 값이고 서비스와 아무 관계가 없습니다. 봐야 할 것은 언제나 /proc/PID/limits입니다.
진단 순서를 압축하면 이렇습니다.
- 에러 문구를 정확히 읽습니다.
in system이 붙으면fs.file-max, 아니면 프로세스 한계입니다. /proc/PID/limits로 실제 적용값을,ls /proc/PID/fd | wc -l로 실사용량을 봅니다. 이 둘만 있으면 한계 문제인지 아닌지 즉시 판정됩니다.- systemd 서비스라면 limits.conf가 아니라
systemctl edit으로LimitNOFILE을 설정하고restart합니다.reload로는 바뀌지 않습니다. - 컨테이너는 런타임 데몬이 값을 정합니다. 컨테이너 안에서
/proc/1/limits를 직접 확인하십시오. - fd 구성을 소켓·파일·파이프로 나눠 봅니다.
CLOSE-WAIT다발은 누수의 확정 증거입니다. - 사용량이 부하와 무관하게 단조 증가하면 한계를 올려도 소용없습니다. 한계 상향은 조사할 시간을 버는 조치일 뿐 해결이 아닙니다.
- "Too many open files"가 나는데 fd에 여유가 있다면 inotify 인스턴스 한계를 확인하십시오. 문구가 같습니다.