Skip to content

필사 모드: Too many open files 완전 해결 — ulimit을 올려도 안 되는 이유

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 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/limitsulimit -n, limits.conf, systemd LimitNOFILE
RLIMIT_NOFILE hard프로세스 하나설정 거부/proc/PID/limits위와 같음, 낮추기만 비특권 가능
fs.nr_open커널 전역EPERM/EINVALsysctl 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.confDefaultLimitNOFILE, 그것도 없으면 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_connectionsworker_rlimit_nofile보다 크면 의미가 없습니다. 연결 하나가 클라이언트 소켓 하나와 업스트림 소켓 하나를 쓸 수 있으므로, 리버스 프록시라면 worker_rlimit_nofileworker_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
일반 fdRLIMIT_NOFILE프로세스EMFILE (24)
시스템 전체 파일fs.file-max시스템ENFILE (23)
inotify 인스턴스fs.inotify.max_user_instances사용자EMFILE (24)
inotify watchfs.inotify.max_user_watches사용자ENOSPC (28)
epoll watchfs.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입니다.

진단 순서를 압축하면 이렇습니다.

  1. 에러 문구를 정확히 읽습니다. in system이 붙으면 fs.file-max, 아니면 프로세스 한계입니다.
  2. /proc/PID/limits로 실제 적용값을, ls /proc/PID/fd | wc -l로 실사용량을 봅니다. 이 둘만 있으면 한계 문제인지 아닌지 즉시 판정됩니다.
  3. systemd 서비스라면 limits.conf가 아니라 systemctl edit으로 LimitNOFILE을 설정하고 restart 합니다. reload로는 바뀌지 않습니다.
  4. 컨테이너는 런타임 데몬이 값을 정합니다. 컨테이너 안에서 /proc/1/limits를 직접 확인하십시오.
  5. fd 구성을 소켓·파일·파이프로 나눠 봅니다. CLOSE-WAIT 다발은 누수의 확정 증거입니다.
  6. 사용량이 부하와 무관하게 단조 증가하면 한계를 올려도 소용없습니다. 한계 상향은 조사할 시간을 버는 조치일 뿐 해결이 아닙니다.
  7. "Too many open files"가 나는데 fd에 여유가 있다면 inotify 인스턴스 한계를 확인하십시오. 문구가 같습니다.

현재 단락 (1/144)

증상은 이렇게 나타납니다.

작성 글자: 0원문 글자: 10,289작성 단락: 0/144