파일 디스크립터 고갈 시 나타나는 증상

1,696 단어·4 분·원문(.md)

FD 고갈은 고성능 서버나 대규모 데이터를 다룰때 마주하게 되는 현상이다.

File Descriptor #

file descriptor는 추상화된 자원에 접근하기 위한 정수 핸들이다.

유닉스 리눅스 철학인 everythings is a file에 따라 일반 파일뿐만 아니라 socket, pipe, device등 모든 io자원을 fd를 통해 관리한다.

커널 내부 3단계 매핑 구조 #

프로세스가 파일을 열 때, 커널은 단순히 id만 주는게 아니라 복잡한 테이블 구조를 생성한다.

  1. process descriptor (task struct): 각 프로세스는 자신만의 fd table을 가진다. 여기서 fd 값 0 1 2은 배열의 인덱스 역할을 한다.
  2. open file table (system-wide): 모든 프로세스가 공유하는 테이블이다 파일의 오프셋, 읽기 쓰기 권한등 어떻게 열려있는가에 대한 상태를 저장한다.
  3. Inode table(Vnode Table): 실제 디스크상의 파일 데이터나 하드웨어 장치가 연결되는 무엇인가에 대한 정보를 담고있다.

파일 디스크립터 고갈시 나타나는 증상 #

시스템이나 프로세스가 할당받을 수 있는 fd 상한치 limit에 도달하게 되면 새로운 자원을 확보할 수 없게 되어 서비스가 불능 상태에 빠진다.

소프트웨어 레벨의 오류 EMFILE, ENFILE #

  • EMFILE(Too many open files): 단일 프로세스가 자신에게 할당된 per process limit을 다 썼을때 발생한다.
  • ENFILE(File table overflow): 시스템 전체가 사용할 수 있는 최대 fd 개수 (system-wide limit)에 도달했을때 발생한다. 이는 시스템 전체가 마비될 수 있는 위험한 신호다.

프로세스 단위랑 전체 시스템 단위로 구분지을 수 있다.

네트워크 서비스 중단. Accept Fail #

서버 애플리케이션에서 치명적인 현상인데

  • 새로운 클라이언트가 접속을 시도하면 커널은 accept()를 통해 새로운 소켓 fd를 생성해야한다.
  • fd가 고갈되면 accept가 실패하여 클라이언트는 connection refused나 무한 대기를 겪게 된다. 기존 연결은 유지되지만 신규 유입이 차단된다.

  • 혹은 에러가 발생해서 로그를 남기려해도 로그 파일 자체를 open할 fd가 없기 때문에 로그 누락, 디버깅이 불가능해지는 silent failure 구간에 진입하게 되어 원인파악이 힘들어질 수 있으며

파이프 및 ipc 통신 불능도 발생할 수 있는데 프로세스간 통신을 위한 Pipe나 Eventfd 생성도 불가능해져 멀티 프로세스/스레드 환경에서 동기화나 데이터 전달이 멈춰 앱이 zombie or deadlock 처럼 보일 수 있다.


FD Limit 관리 #

리눅스 시스템에서 이 한계치를 확인하고 제어하는 방법이 있다.

ulimit -Sn # 프로세스가 현재 사용할 수 있는 권장 한도

ulimit -Hn # 루트 권한으로만 수정 가능한 최대 한도 (S Soft, H Hard)

cat /proc/sys/fs/file-max # 커널 전체에서 열 수 있는 최대 fd 수

lsof -p <pid> | wc -l # 특정 프로세스가 사용중인 fd 개수 확인

예시를 조금 살펴보면

예를들어 애플리케이션 로그에

java.io.IOException: Too many open files
# 혹은
Accept error: errno = 24 (EMFILE)

이런 로그가 찍혔다고 해보자. 이걸 분석해볼것이다.

처음앤 가장 먼저 내 시스템의 그릇이 얼마나 큰지를 확인해야한다.

$ ulimit -Sn  # Soft limit: 현재 적용되는 제한
1024
$ ulimit -Hn  # Hard limit: 최대 확장 가능한 제한
4096

이 프로세스는 fd를 1024개까지만 열수있는 상황인것이다. 서버급에선 1024는 비교적 작은수라 이를 수만개로 설정해둬야한다.

이후에 범인 프로세스를 색출한다.

어떤 프로세스가 fd를 다 잡아먹고있는지 찾는다.

$ lsof | awk '{print $2}' | sort | uniq -c | sort -nr | head -n 5
   1020 12345  # PID 12345가 1,020개를 사용 중 (거의 한계치!)
    150 6789
     45 1

아래처럼 특정 프로세스 상세분석도 가능함

$ ls /proc/12345/fd | wc -l
1020

여기서 ulimit이 1024인데 12345가 1020개를 쓰고있어서 이 프로세스는 곧 죽거나 새로운 요청을 못받는걸 알았다.

단순히 사용자가 많아서 그런건지 코드가 잘못되어 fd가 새고있는지 마지막으로 한번 더 판단한다.

$ lsof -p 12345
COMMAND  PID  USER   FD   TYPE  DEVICE SIZE/OFF    NODE NAME
java    12345 user   10u  IPv4  123456      0t0     TCP *:8080 (LISTEN)
java    12345 user   11u  REG    8,1     4096  789012 /tmp/tempfile.tmp (deleted)
java    12345 user   12u  REG    8,1     4096  789013 /tmp/tempfile.tmp (deleted)
... (수백 개 반복) ...

파일명 옆에는 deleted라고 붙어있는데 fd가 열려있다면 프로그램이 파일 삭제는 했는데 close를 호출하지 않아

커널이 여전히 fd를 붙잡고있는 전형적인 resouce leak이 난걸 확인했다.

일단 임시 방편은 당연히 프로세스 한도를 늘리는거고 이땐 prlimit 을 쓴다.

# PID 12345의 Soft/Hard limit을 모두 65535로 변경
$ sudo prlimit --pid 12345 --nofile=65535:65535

서버가 재부팅되면 이는 반영안되는데 영구설정하려면 /etc/security/limits.conf를 수정해둬야한다.

$ sudo vi /etc/security/limits.conf

# 맨 아래에 추가
* soft    nofile    65535
* hard    nofile    65535

근본적으로는 lsof에서 발견한 누수지점을 찾아 try with resource (java)나 defer file.Close() (Go) 에서처럼 리소스를 닫는 코드가 누락된건 아닌지 사용하는 오픈소스에서 이런게 없는건 아닌지 등을 찾아서 분석하고 해결하면 된다.

SRE/question/q_20.md