IT·리눅스/리눅스

파일 디스크립터와 ulimit - \"Too many open files\" 정체

jmineekim 2026. 5. 29. 10:00

운영 중인 자바 서버가 어느 날 갑자기 java.net.SocketException: Too many open files를 뱉으면서 새 연결을 거부하기 시작하면, 그 새벽이 참 길어집니다. 일단 프로세스를 재시작하면 잠시 잠잠해지지만 몇 시간 뒤 똑같이 또 터지죠. 범인은 두 종류 중 하나입니다. FD 한계가 너무 낮게 잡혀 있거나, 아니면 코드가 FD를 닫지 않고 새는 경우. 둘은 진단도, 해결도 다른데 처음엔 구분이 잘 안 갑니다. 오늘은 파일 디스크립터가 정확히 뭐고, ulimit이 어디까지 책임을 지며, 진짜 누수인지 단순 limit 부족인지 어떻게 가르는지 정리해 볼게요.

개념 / 원리

리눅스에서 프로세스가 열어 둔 모든 자원은 파일 디스크립터(File Descriptor, FD)라는 작은 정수로 다뤄집니다. "파일"이라는 이름과 달리 디스크 파일만이 아니에요.

  • 일반 파일 (open())
  • 디렉터리
  • 소켓 (TCP/UDP/Unix domain)
  • 파이프, FIFO
  • inotify, eventfd, signalfd
  • 익명 메모리 매핑 등

표준 입출력 3개(stdin=0, stdout=1, stderr=2)부터 시작해서, 새 자원을 열 때마다 사용 가능한 가장 작은 정수가 할당됩니다.

리눅스는 프로세스마다 열 수 있는 FD 개수에 상한을 둡니다. 한 프로세스가 시스템 자원을 무한정 잡지 못하게 막는 안전장치죠. 이 상한이 ulimit -n 또는 RLIMIT_NOFILE입니다.

상한에는 두 종류가 있어요.

종류 의미
soft limit 실제로 적용되는 한계. 일반 유저가 hard 이내에서 자유롭게 올릴 수 있음.
hard limit 위로 못 넘는 절대 한계. 올리려면 root 권한 필요.

기본값은 배포판마다 다르지만 일반적으로 soft 1024, hard는 그 위(수만~수십만) 정도가 흔합니다. 일반적인 데스크톱이라면 충분한데, 동시 접속 많은 서버에는 한참 모자랍니다.

여기까지가 정상 상황의 이야기고, FD 누수는 또 별개입니다. 코드가 open()이나 socket()으로 자원을 열고 close()를 안 부르면, 그 FD는 프로세스가 죽기 전까지 계속 점유돼요. 시간이 흐를수록 FD 개수가 우상향하다가, 어느 순간 한계에 부딪히면 Too many open files가 터집니다.

정리: limit 부족 = "FD 개수 자체는 합리적인데 천장이 낮음" / 누수 = "FD가 계속 우상향함, 천장과 무관"

실전 예제

진단부터 가봅시다. 먼저 현재 셸의 FD 한계를 확인합니다.

# soft / hard limit
ulimit -n        # soft
ulimit -Hn       # hard

실제로 문제가 되는 프로세스의 한계는 셸이 아니라 그 프로세스 PID 기준으로 봐야 합니다.

# 프로세스의 실제 적용 limit
cat /proc/<PID>/limits | grep "Max open files"

# 그 프로세스가 지금 열고 있는 FD 개수
ls /proc/<PID>/fd | wc -l

# 또는 lsof로 (좀 더 사람이 보기 좋음)
lsof -p <PID> | wc -l

여기서 첫 단서가 나옵니다. 열린 FD 개수가 limit 가까이 (예: 1024 한계에 1020개) 차 있으면 limit 부족이거나 누수, 한계의 10%도 안 차 있으면 다른 원인일 가능성이 큽니다.

누수가 의심되면 시간을 두고 두 번 측정해서 우상향하는지 봐야 합니다.

# 1분 간격으로 3번
for i in 1 2 3; do
  echo "$(date): $(ls /proc/<PID>/fd | wc -l)"
  sleep 60
done

수치가 계속 증가하면 거의 확실히 누수입니다. 어떤 종류의 FD가 새는지는 이렇게 봅니다.

# FD 종류별 카운트
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn

# 어떤 파일/소켓인지 구체적으로
ls -l /proc/<PID>/fd | head -50

소켓이 계속 늘면 HTTP 클라이언트 재사용 실패 같은 패턴, REG(일반 파일)가 늘면 로그 파일·임시 파일을 안 닫는 코드를 의심합니다.

limit 자체를 올려야 한다면, 우선 셸에서 임시로 올리고 검증한 뒤 영구 설정에 반영합니다.

# 임시 (현재 셸과 자식 프로세스만)
ulimit -n 65535

# 영구 (모든 유저, 재로그인 후 적용)
# /etc/security/limits.conf
*       soft    nofile  65535
*       hard    nofile  65535

그런데 여기서 가장 자주 빠지는 함정이 나옵니다. systemd로 띄우는 서비스에는 limits.conf안 먹힙니다. systemd 서비스 unit 파일에 직접 지정해야 해요.

# /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=65535
# ... 나머지 설정

수정 후엔 systemctl daemon-reload && systemctl restart myapp 잊지 마시고요.

주의할 점

1) ulimit은 셸 단위라 systemd 서비스에는 무력합니다. 위에서 짚었듯이, 가장 흔한 함정입니다. 셸에서 ulimit -n 65535 잘 잡혀 있어도 systemd로 띄운 서비스의 /proc/<PID>/limits는 여전히 1024일 수 있어요. 반드시 unit 파일의 LimitNOFILE을 확인하세요.

2) limit을 무작정 올려도 누수면 시간만 버는 겁니다. 1024가 64Ki가 되면 폭발 시점만 일주일에서 한 달로 미뤄질 뿐, 누수는 그대로예요. 우상향 패턴이면 코드부터 봐야 합니다. 자바라면 try-with-resources, 파이썬이라면 with 블록을 안 쓴 자리부터 의심합니다.

3) 컨테이너 환경은 또 다릅니다. 도커/쿠버네티스에서는 컨테이너 런타임이 정한 ulimit이 적용됩니다. 도커는 --ulimit nofile=65535:65535, 쿠버네티스는 PodSecurityPolicy나 SecurityContext에서 조정. 호스트 ulimit이 아무리 커도 컨테이너 안은 별개예요.

4) lsof가 느리고 무겁다면 /proc/<PID>/fd를 직접 보세요. 운영 서버에서 lsof를 풀로 돌리면 의외로 부하가 큽니다. 그냥 ls /proc/<PID>/fd | wc -l이 가장 가볍고 빠릅니다.

5) Too many open files인데 FD 개수는 멀쩡한 경우도 있습니다. 시스템 전체 한계(/proc/sys/fs/file-max)에 부딪힌 케이스인데, 보통 굉장히 많은 프로세스가 동시에 떠 있는 호스트에서 나옵니다. cat /proc/sys/fs/file-nr로 시스템 전체 사용량 확인이 가능합니다.

마무리

정리하면 세 줄입니다.

  • FD는 파일·소켓·파이프 등 모든 열린 자원을 가리키는 정수, 프로세스마다 RLIMIT_NOFILE로 상한이 있다.
  • Too many open fileslimit 부족이거나 FD 누수/proc/<PID>/fd 개수를 시간 간격 두고 측정해서 구분한다.
  • systemd 서비스는 ulimit이 아니라 unit의 LimitNOFILE=을 봐야 한다. 가장 흔한 함정.
반응형