Published on

Linux EMFILE(Too many open files) 원인과 해결

Authors

서버를 운영하다 보면 갑자기 애플리케이션 로그에 Too many open files가 찍히고, 소켓 연결/파일 읽기/로그 기록까지 줄줄이 실패하는 상황을 만납니다. 이 오류는 리눅스 커널이 파일 디스크립터(File Descriptor, FD) 를 더 이상 할당할 수 없을 때 발생하며, 에러 코드로는 보통 EMFILE(프로세스 한도 초과) 또는 ENFILE(시스템 전역 한도 초과)로 나타납니다.

이 글에서는 EMFILE의 구조를 이해하고, 실제 장애에서 가장 자주 쓰는 진단 명령어, 원인별 해결책, systemd/Kubernetes에서의 영구 설정, 그리고 재발 방지 체크리스트까지 한 번에 정리합니다.

EMFILE/ENFILE의 의미: “파일”은 파일만이 아니다

리눅스에서 FD는 단순히 디스크 파일만 가리키지 않습니다.

  • TCP/UDP 소켓(서버 리슨 소켓, 클라이언트 커넥션)
  • 파이프/유닉스 도메인 소켓
  • inotify, eventfd, epoll fd
  • /dev/null, 로그 파일 핸들

즉, 트래픽이 늘어 소켓이 증가하거나, 로그/파일 핸들을 닫지 않는 버그가 있으면 "파일을 많이 열었다" 로 인식됩니다.

정리하면:

  • EMFILE: 해당 프로세스가 가질 수 있는 FD 개수 제한(ulimit)을 넘음
  • ENFILE: 시스템 전체의 open file table(전역) 제한을 넘음

가장 빠른 1분 진단 플로우

장애 중에는 “무엇이 얼마나 열려 있는지”를 빠르게 잡는 게 중요합니다.

1) 프로세스별 FD 사용량 확인

# PID 확인
ps -ef | grep myapp

# 열린 FD 개수
ls -1 /proc/<PID>/fd | wc -l

# 어떤 FD가 열려 있는지(상위 몇 개만)
ls -l /proc/<PID>/fd | head

2) 현재 프로세스 한도(soft/hard) 확인

# 현재 쉘 기준
ulimit -n

# 특정 PID의 limits 확인
cat /proc/<PID>/limits | grep -i "open files"

3) 시스템 전역 한도 확인

cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr  # allocated, unused, max
  • /proc/sys/fs/file-nr는 보통 allocated unused max 형태로 보이며, allocatedmax에 근접하면 ENFILE 가능성이 커집니다.

4) 누수/삭제된 파일 핸들(디스크도 같이 찰 때) 확인

FD가 많아지면 디스크 100%와 함께 터지는 경우도 흔합니다. 특히 삭제했는데 프로세스가 계속 잡고 있는 파일은 공간이 회수되지 않습니다.

lsof -p <PID> | head
lsof -p <PID> | grep deleted | head

이 케이스는 아래 글과 원인이 맞닿아 있습니다.

원인 1: ulimit(프로세스 FD 한도)가 낮다

가장 흔한 케이스입니다. 특히 다음 상황에서 자주 터집니다.

  • systemd 서비스 기본 LimitNOFILE이 낮음
  • 컨테이너 런타임/쿠버네티스에서 기본 ulimit가 낮음
  • SSH로 들어가서 올린 ulimit와 서비스의 ulimit가 다름

즉시 대응(현재 쉘)

ulimit -n 65535
# 이후 해당 쉘에서 실행한 프로세스에만 적용

하지만 서비스 프로세스에는 적용되지 않는 경우가 많습니다.

systemd 서비스에 영구 적용

/etc/systemd/system/myapp.service.d/override.conf를 만들고:

[Service]
LimitNOFILE=1048576

적용:

sudo systemctl daemon-reload
sudo systemctl restart myapp

# 확인
cat /proc/$(pidof myapp)/limits | grep -i "open files"

서비스가 계속 재시작되는 상황이라면(EMFILE로 크래시/헬스체크 실패) 함께 점검하면 좋습니다.

원인 2: 애플리케이션의 FD 누수(파일/소켓 close 누락)

한도를 올려도 다시 터지면 “진짜 원인”은 보통 누수입니다.

누수의 전형적인 패턴

  • 예외/타임아웃 경로에서 close()가 호출되지 않음
  • HTTP 클라이언트 keep-alive 풀을 무한정 키움
  • DB 커넥션 풀/소켓 풀 설정이 잘못되어 커넥션이 쌓임
  • 로그 롤링/파일 교체 로직에서 핸들을 놓지 않음

프로세스 내 FD 증가 추적(간단 모니터링)

PID=<PID>
watch -n 1 "ls -1 /proc/$PID/fd | wc -l"

FD 수가 트래픽과 무관하게 계속 우상향하면 누수 가능성이 큽니다.

어떤 타입의 FD가 많은지 요약

PID=<PID>
ls -l /proc/$PID/fd \
  | awk '{print $NF}' \
  | sed 's/->.*$//' \
  | sed 's/\[.*\]//' \
  | head

# 소켓이 대부분인지 확인
ls -l /proc/$PID/fd | grep -c socket:

lsof로 “어디로” 열리고 있는지 확인

lsof -p <PID> | awk '{print $5, $9}' | head

# TCP 연결이 폭증하는지
lsof -p <PID> -iTCP -sTCP:ESTABLISHED | wc -l

# 특정 파일이 반복적으로 열리는지(로그 등)
lsof -p <PID> | grep "/var/log" | head

(예시) Python에서 흔한 누수와 수정

# 나쁜 예: 파일을 열고 예외가 나면 close가 보장되지 않음
f = open("/tmp/data.txt")
process(f)

# 좋은 예: 컨텍스트 매니저로 close 보장
with open("/tmp/data.txt") as f:
    process(f)

(예시) Node.js에서 소켓/요청 누수 방지 포인트

import http from 'node:http';

const agent = new http.Agent({
  keepAlive: true,
  maxSockets: 256,
  maxFreeSockets: 64,
  timeout: 30_000,
});

// 요청마다 새 Agent를 만들면 소켓이 누적될 수 있음(나쁜 패턴)
// 공용 agent를 재사용하고, 타임아웃/에러 처리를 확실히 한다.

원인 3: 시스템 전역 file-max 또는 커널 리소스 한계(ENFILE)

프로세스 ulimit는 충분한데도 시스템 전체가 바닥나면 ENFILE이 납니다. 대규모 멀티테넌트/노드에서 흔합니다.

file-max 확인 및 조정

sysctl fs.file-max

# 임시 적용
sudo sysctl -w fs.file-max=2097152

영구 적용(/etc/sysctl.conf 또는 /etc/sysctl.d/99-custom.conf):

fs.file-max = 2097152

적용:

sudo sysctl --system

함께 봐야 하는 네트워크 관련 상한

EMFILE처럼 보이지만 실제로는 네트워크 추적 테이블이 포화되어 연결이 불안정해지는 경우도 있습니다(특히 Kubernetes/EKS에서).

원인 4: “FD는 충분한데 왜 EMFILE?” — 프로세스/스레드/라이브러리의 숨은 FD 사용

일부 런타임/라이브러리는 내부적으로 많은 FD를 사용합니다.

  • 파일 감시(inotify) 기반 핫리로드/워처
  • metrics exporter가 소켓을 다량 생성
  • gRPC/HTTP2에서 스트림과 커넥션 관리 설정 오류
  • 너무 많은 워커/스레드가 각자 커넥션 풀을 가짐(풀 * 워커 수 만큼 증가)

이 경우 “커넥션 풀 크기”를 단일 프로세스 기준이 아니라 프로세스 복제 수(워커 수)까지 곱해서 계산해야 합니다.

해결 전략: 한도 상향 vs 누수 제거, 무엇이 먼저인가

운영 관점에서 권장 순서는 다음입니다.

  1. 즉시 장애 완화: ulimit/LimitNOFILE 상향으로 서비스 복구(가능하면)
  2. 원인 규명: FD가 어떤 타입으로 증가하는지(lsof, /proc)
  3. 근본 해결: 누수 제거, 풀/워커/keep-alive 설정 조정
  4. 재발 방지: 모니터링/알람 + 배포 전 부하 테스트

한도 상향은 시간을 벌어주지만, 누수가 있으면 결국 다시 터집니다.

운영 체크리스트(재발 방지)

1) 모니터링 지표

  • 프로세스별 열린 FD 수
  • 시스템 file-nr의 allocated 추세
  • ESTABLISHED 소켓 수, TIME_WAIT 수
  • 애플리케이션 레벨: 요청 수 대비 커넥션 수/풀 사용량

2) 알람 기준 예시

  • 프로세스 FD 사용량이 ulimit의 70~80% 초과
  • file-nr allocated / file-max가 70% 초과
  • FD 수가 트래픽과 무관하게 지속 증가(누수 시그널)

3) 로그/코어 덤프

  • EMFILE 발생 시점의 스택 트레이스 확보
  • lsof -p <PID> 스냅샷을 장애 시 자동 수집

간단한 수집 스크립트 예시:

#!/usr/bin/env bash
PID="$1"
OUT="/tmp/fd-dump-$PID-$(date +%s)"
mkdir -p "$OUT"

cat /proc/$PID/limits > "$OUT/limits.txt"
ls -l /proc/$PID/fd > "$OUT/fd-list.txt"
lsof -p $PID > "$OUT/lsof.txt" 2>&1
lsof -p $PID -iTCP > "$OUT/lsof-tcp.txt" 2>&1

echo "saved to $OUT"

마무리

Too many open files는 단순히 “파일을 닫아라” 수준의 문제가 아니라, 프로세스 한도(EMFILE), 시스템 전역 한도(ENFILE), 애플리케이션 누수, 커넥션 풀/워커 설계가 교차하는 운영 이슈입니다.

가장 중요한 것은 (1) /proc/<pid>/fd, lsof, ulimit, file-nr어디가 병목인지 즉시 판별하고, (2) 한도 상향으로 시간을 번 뒤, (3) FD가 증가하는 “종류”를 특정해 누수/설정 문제를 근본적으로 제거하는 것입니다.

장애 상황에서 FD가 많아지며 디스크 공간 회수가 안 되는 현상까지 동반된다면, 위에서 소개한 deleted-but-open 케이스도 반드시 같이 확인하세요.