Fuwari Banner
지니제스트Tech Archive
7분 소요

리눅스 Too many open files 에러와 파일 디스크립터 증설

ggeniezst

대량의 동시 연결을 처리할 때 발생하는 Too many open files 에러의 근본 원인을 분석하고, 커널 fs.file-max와 PAM limits 및 Systemd LimitNOFILE의 3단계 파일 디스크립터 한도를 안전하게 증설하는 실무 가이드입니다.

Sponsored

고트래픽을 처리하는 웹 서버나 데이터베이스, 메시지 브로커를 운영하다 보면 애플리케이션 로그에 java.io.IOException: Too many open files 또는 socket: too many open files 에러가 기록되며 신규 클라이언트 접속이 전면 중단되는 장애가 발생합니다. 관리자가 쉘에서 ulimit -n을 실행했을 때는 값이 65535로 넉넉하게 설정되어 있어 원인을 찾지 못하고 혼란에 빠지는 경우가 많습니다.

이 현상은 리눅스 커널의 파일 디스크립터(File Descriptor) 관리 계층과 systemd의 독립적인 리소스 제어 구조가 분리되어 있기 때문에 발생합니다. 리눅스 시스템 전체 레벨, 사용자 세션 레벨, 그리고 개별 서비스 데몬 레벨의 3단계 제한 아키텍처를 정확히 이해하고 무중단으로 파일 디스크립터 한도를 증설하는 실전 해결 방안을 다룹니다.


Too many open files 에러와 로그 분석

Too many open files 에러는 프로세스가 운영체제로부터 새로운 파일 디스크립터 번호를 발급받으려고 시도했으나, 시스템이나 해당 프로세스에 할당된 최대 개수(RLIMIT_NOFILE)를 초과했을 때 커널이 반환하는 EMFILE(에러 코드 24) 또는 ENFILE(에러 코드 23) 에러입니다.

서비스별 장애 로그 패턴

웹 서버나 애플리케이션 런타임마다 에러 메시지가 표출되는 형식은 다르지만 본질적인 원인은 동일합니다.

Nginx 에러 로그:

TEXT
2026/09/20 18:40:12 [alert] 28104#28104: *1592034 socket() failed (24: Too many open files) while connecting to upstream, client: 192.168.10.45, server: api.service.internal

Java Spring Boot 및 Netty 로그:

TEXT
java.io.IOException: Too many open files
    at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:533)
    at io.netty.channel.socket.nio.NioServerSocketChannel.doReadMessages(NioServerSocketChannel.java:140)

Node.js 런타임 에러:

TEXT
Error: EMFILE: too many open files, open '/var/log/app/access.log'
    at Object.openSync (fs.js:498:3)
    at Object.writeFileSync (fs.js:1529:35)

소켓 및 파일 핸들 상태 진단

장애가 발생한 즉시 터미널에서 전체 시스템의 파일 디스크립터 할당 현황과 특정 프로세스의 제한치를 확인해야 합니다.

BASH
# 시스템 전체 파일 디스크립터 할당 현황 확인 (할당된 수, 미사용 수, 최대치)
cat /proc/sys/fs/file-nr

# 장애가 발생한 프로세스의 실제 적용 제한치(Limits) 확인
PID=$(pgrep -f "nginx|java|node" | head -n 1)
cat /proc/${PID}/limits | grep "Max open files"

# 현재 특정 프로세스가 점유 중인 파일 디스크립터 개수 카운트
ls -1 /proc/${PID}/fd | wc -l

/proc/sys/fs/file-nr의 결과가 18240 0 1048576처럼 출력된다면 시스템 전체 한도(1048576)에는 도달하지 않았으나, 특정 프로세스의 /proc/${PID}/limits에서 Max open filesSoft Limit1024로 묶여 있어 병목이 발생했음을 즉시 판별할 수 있습니다.


리눅스 VFS와 3단계 파일 디스크립터 아키텍처

리눅스는 "모든 것은 파일이다(Everything is a file)"라는 유닉스 철학을 따릅니다. 디스크에 존재하는 일반 텍스트 파일뿐만 아니라 디렉토리, 파이프, IPC 채널, 이벤트 폴링 인스턴스(epoll), 그리고 네트워크 통신에 사용되는 모든 TCP/UDP 소켓이 파일 디스크립터를 하나씩 소비합니다.

과거 다루었던 TIME_WAIT 소켓 누적과 포트 고갈 해결리눅스 디스크 100% 해결 사례처럼, 프로세스가 네트워크 연결을 닫지 않고 방치하거나 삭제된 파일의 핸들을 열어둔 채 유지하면 파일 디스크립터 테이블이 순식간에 포화됩니다.

리눅스에서 파일 디스크립터 한도는 다음 3단계 계층 구조로 엄격히 관리됩니다.

커널 전체 제한 (fs.file-max)

운영체제 커널 전체에서 동시에 열 수 있는 파일 디스크립터의 총합입니다. 메모리 용량에 비례하여 시스템 부팅 시 커널에 의해 자동 산정되며, 이 한도에 도달하면 모든 사용자 프로세스가 ENFILE 에러를 반환받습니다.

사용자 및 세션 제한 (PAM limits.conf)

PAM(Pluggable Authentication Modules) 기반으로 사용자가 SSH나 터미널 로그인 세션을 열 때 적용되는 한도입니다. 일반 사용자가 시스템 자원을 독점하여 서버 전체를 마비시키는 서비스 거부(DoS) 현상을 방지하는 목적을 갖습니다. soft limit은 프로세스가 실행 중에 스스로 늘릴 수 있는 상한선이며, hard limit은 루트 권한 없이는 절대 넘을 수 없는 절대 한계선입니다.

서비스 데몬 제한 (systemd cgroup)

최신 리눅스 배포판에서 가장 많은 오해가 발생하는 영역입니다. systemd로 구동되는 백그라운드 서비스는 PAM의 limits.conf 설정을 전혀 참조하지 않고 자체 설정값으로 cgroup을 구성합니다. 따라서 /etc/security/limits.conf에 아무리 큰 값을 지정해 두어도 systemctl로 기동한 서비스는 기본값인 1024에 묶이게 됩니다.


단계별 파일 디스크립터 한도 증설 및 영구 적용

파일 디스크립터 고갈을 완전히 해결하기 위해서는 커널, 사용자 세션, 그리고 systemd 서비스 유닛의 세 영역 모두에 영구 설정을 적용해야 합니다.

커널 전체 한도 확장

/etc/sysctl.d/99-file-max.conf 파일을 생성하여 시스템 전체 파일 디스크립터 상한을 대폭 상향합니다.

INI
# /etc/sysctl.d/99-file-max.conf

# 시스템 전체 파일 디스크립터 상한선 (약 209만 개)
fs.file-max = 2097152

# epoll 인스턴스 및 inotify 큐 용량 확장
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 1024

설정 후 즉시 커널에 반영합니다.

BASH
# sysctl 설정 즉시 반영
sudo sysctl --system

# 반영 여부 검증
sysctl fs.file-max

PAM 사용자 세션 한도 증설

사용자 및 애플리케이션 계정이 로그인할 때 적용될 한도를 /etc/security/limits.d/99-nofile.conf에 명시합니다. 기존 기본 파일(/etc/security/limits.conf)을 직접 수정하는 것보다 별도 드롭인 파일을 생성하는 것이 패키지 업데이트 시 설정 유실을 막는 모범 사례입니다.

INI
# /etc/security/limits.d/99-nofile.conf

# 모든 일반 사용자(*) 및 루트 계정의 Soft/Hard 한도 지정
*        soft    nofile    65536
*        hard    nofile    1048576
root     soft    nofile    65536
root     hard    nofile    1048576

systemd 서비스 유닛 한도 지정

systemd로 관리되는 서비스(예: nginx.service, docker.service, myapp.service)의 한도를 올리기 위해서는 유닛 오버라이드 설정을 사용해야 합니다.

BASH
# 대상 서비스에 대한 드롭인 설정 디렉토리 생성 및 편집
sudo systemctl edit nginx

편집기 창이 열리면 다음 구문을 입력하고 저장합니다.

INI
[Service]
LimitNOFILE=65536:1048576

콜론 앞의 값은 Soft Limit이고 뒤의 값은 Hard Limit입니다. 설정을 저장한 후 데몬 설정을 다시 로드하고 서비스를 재시작합니다.

BASH
# systemd 설정 리로드 및 서비스 재시작
sudo systemctl daemon-reload
sudo systemctl restart nginx

만약 서버 내의 모든 systemd 서비스에 공통으로 높은 한도를 부여하고 싶다면 /etc/systemd/system.conf.d/50-limits.conf를 생성하여 전역 기본값을 지정할 수 있습니다.

INI
# /etc/systemd/system.conf.d/50-limits.conf
[Manager]
DefaultLimitNOFILE=65536:1048576

실행 중인 프로세스 무중단 prlimit 적용 및 검증

운영 중인 프로덕션 데이터베이스나 결제 API 서버의 경우 파일 디스크립터 한도를 올리기 위해 서비스를 재시작하는 다운타임을 허용할 수 없습니다. 이때 prlimit 유틸리티를 사용하면 서비스 중단 없이 실행 중인 프로세스의 RLIMIT_NOFILE을 즉시 변경할 수 있습니다.

무중단 prlimit 한도 갱신

BASH
# 대상 프로세스 PID 확인
TARGET_PID=$(pgrep -f "nginx: master" || pgrep -f "mysqld")

# 현재 적용된 한도 조회
prlimit --pid ${TARGET_PID} --nofile

# 재시작 없이 Soft Limit을 65536, Hard Limit을 1048576으로 즉시 상향
sudo prlimit --pid ${TARGET_PID} --nofile=65536:1048576

적용 결과 실시간 검증

커널의 가상 파일 시스템(/proc)을 직접 조회하여 변경된 한도가 프로세스에 즉시 투영되었는지 확인합니다.

BASH
# 프로세스 내부 limits 정보 출력
cat /proc/${TARGET_PID}/limits | grep -E "Limit|open files"

출력 결과에서 Max open filesSoft Limit65536, Hard Limit1048576으로 갱신되었다면 신규 소켓 연결 및 파일 입출력이 즉시 정상화됩니다.

누수 프로세스 및 FD 유형별 전수 조사

한도를 증설했음에도 점유 숫자가 계속 증가한다면 애플리케이션 코드 내에서 커넥션이나 파일 핸들을 닫지 않는 누수(Leak)가 발생하고 있는 것입니다. 아래 명령어로 점유 중인 파일 디스크립터의 유형별 분포를 파악합니다.

BASH
# 프로세스가 열고 있는 FD의 유형(소켓, 파일, 파이프 등) 상위 10개 집계
ls -l /proc/${TARGET_PID}/fd | awk '{print $NF}' | sed -E 's/.*:\[[0-9]+\]/socket/' | sort | uniq -c | sort -nr | head -n 10

만약 socket 항목이 수만 개를 차지하고 있다면 HTTP Keep-Alive 설정 미비나 DB 커넥션 풀 누수를 의심해야 하며, 특정 로그 파일 경로가 수천 개 이상 점유되어 있다면 로깅 라이브러리의 스트림 미반환 버그를 추적해야 합니다.


실무 엔지니어가 자주 겪는 사이드 이펙트 FAQ

ulimit -n 값을 올렸는데 왜 systemd 서비스에는 반영되지 않나요?

ulimit 명령어는 현재 로그인한 쉘 세션의 환경을 제어하며, 이 설정은 /etc/security/limits.conf와 PAM 모듈을 통해 주입됩니다. 반면 systemd는 PID 1번으로 커널 부팅 초기부터 독자적인 cgroup 트리 구조를 구축하므로 PAM 인증 스택을 거치지 않습니다. 따라서 systemd 서비스는 반드시 유닛 파일 내에 LimitNOFILE 지시어를 지정하거나 /etc/systemd/system.confDefaultLimitNOFILE을 수정해야 합니다.

한도를 100만 단위로 무제한 늘려도 서버 메모리에 문제가 없나요?

파일 디스크립터 자체는 단순한 정수 인덱스에 불과하지만, 커널 내부적으로 각 파일 디스크립터마다 struct file, dentry, inode 등의 커널 구조체가 할당됩니다. 통상 FD 하나당 커널 메모리를 약 1KB2KB 내외로 소비하므로, 100만 개의 파일이 동시에 열려 있을 경우 약 1GB2GB의 물리 메모리가 커널 공간에 고정 점유(Slab Allocator)됩니다. 서버의 총 메모리가 4GB 미만인 저사양 인스턴스에서는 과도한 한도 상향이 커널 OOM을 유발할 수 있으므로 가용 메모리를 계산하여 설정해야 합니다.

select 시스템 콜을 사용하는 레거시 애플리케이션의 한계는 무엇인가요?

C/C++ 기반의 고전 네트워크 데몬이 다중 입출력을 처리할 때 select() 시스템 콜을 사용하는 경우, C 라이브러리의 FD_SETSIZE 상수가 기본 1024로 하드코딩되어 있습니다. 이 경우 커널이나 서비스 한도를 65536으로 늘려도 프로세스에 1024 이상의 파일 디스크립터 번호가 할당되는 순간 메모리 오버플로우가 발생하며 세그멘테이션 폴트(Segmentation Fault)로 프로세스가 비정상 종료됩니다. 대규모 동시 접속을 다루는 현대적 서버는 반드시 epoll 또는 kqueue, io_uring을 사용하는 런타임으로 현대화해야 합니다.

fs.file-max와 fs.file-nr의 두 번째 숫자가 항상 0으로 표시되는 이유는 무엇인가요?

과거 리눅스 2.4 커널 시절에는 할당되었으나 현재 해제되어 유휴 상태인 파일 디스크립터 슬롯을 커널이 재사용 풀에 보관했기 때문에 두 번째 숫자에 free allocated file handles 수가 기록되었습니다. 그러나 리눅스 2.6 커널 이후부터는 파일 핸들 할당 메커니즘이 동적 슬랩 할당 방식으로 변경되면서 해제된 핸들은 즉시 메모리로 반환됩니다. 따라서 최신 커널에서 두 번째 숫자는 항상 0으로 표시되며 이는 지극히 정상적인 동작입니다.

Sponsored