환경  Ubuntu 22.04 · Linux 6.8 · systemd 249 · 2026-09-07 확인

접속이 안 된다는 연락을 받고 로그를 여니 같은 줄만 수백 개 쌓여 있습니다. Too many open files. 서버에 들어가 df 를 쳐 봐도 디스크는 넉넉하고, 이 프로세스가 열어 둔 파일이라고 해 봐야 로그 몇 개가 전부입니다. 메시지에 적힌 '파일'이 우리가 떠올리는 그 파일이 아니라서 생기는 어긋남입니다.

파일 개수가 아니라 프로세스가 받아 간 번호를 센다

프로세스가 무언가를 열면 커널이 작은 정수를 하나 내줍니다. 그게 파일 디스크립터입니다. 0·1·2 는 표준 입력·출력·에러가 이미 차지하고 있고, 그다음부터는 비어 있는 가장 작은 번호가 순서대로 나갑니다.

세는 대상이 파일에 그치지 않습니다. 소켓도, 파이프도, epoll 인스턴스도 전부 같은 번호를 받아 갑니다. 웹 서버가 동시 접속 5천 개를 붙들고 있으면 파일을 한 개도 열지 않아도 디스크립터를 5천 개 쓰고 있는 셈이죠. 메시지가 파일이라고 말하니 디스크 쪽을 뒤지게 되는데, 정작 터진 자리는 대개 연결 쪽입니다.

RLIMIT_NOFILE 이 이 번호의 상한입니다. 정의는 개수가 아니라 '열 수 있는 최대 디스크립터 번호보다 1 큰 값'이고, 여기를 넘어서면 open·pipe·dup 같은 호출이 EMFILE 로 실패합니다.

한도가 1024 인 셸에서 파일을 계속 열어 보면 1021 개에서 멈춥니다. 앞의 세 자리를 표준 입출력이 쓰고 있어서요. 개수를 세는 값이었다면 1024 에서 멈췄을 텐데, 번호의 상한이라 이렇게 나옵니다.

ulimit -Sn
# 1024

python3 - <<'PY'
import errno
fs = []
try:
    while True:
        fs.append(open('/dev/null'))
except OSError as e:
    print(len(fs), errno.errorcode[e.errno], e.strerror)
PY
# 1021 EMFILE Too many open files

한도는 1024 인데 1021 에서 걸립니다. 0·1·2 는 이미 나가 있으니까요

열어 둔 파일이 몇 개인지가 아니라, 번호가 어디까지 갔는지를 보는 숫자입니다.

램이 얼마든 기본값이 1024 인 데는 이유가 있다

한도는 두 겹으로 되어 있습니다. soft 는 커널이 실제로 막는 값이고, hard 는 soft 를 어디까지 올릴 수 있는지 정한 천장입니다. ulimit -n 이 아무 말 없이 보여주는 값은 앞쪽, soft 입니다.

지금 쓰는 우분투 서버에서 찍어 보면 로그인 셸은 1024 와 1048576, systemd 가 띄운 서비스는 1024 와 524288 입니다. soft 는 나란히 1024 인데 천장이 서로 다릅니다. 셸 쪽은 로그인 세션이, 서비스 쪽은 systemd 의 DefaultLimitNOFILE 이 정해 준 값이라 출처가 아예 다릅니다.

soft 만 이렇게 낮게 잡아 두는 건 옛 코드 때문입니다. select() 로 소켓을 지켜보는 프로그램은 1023 번을 넘는 디스크립터를 다루지 못합니다. glibc 가 fd_set 을 1024 짜리 고정 버퍼로 만들어 뒀거든요. 기본값을 크게 잡으면 그런 프로그램이 조용히 어긋나기 때문에, 낮게 두고 필요한 쪽이 스스로 올려 쓰게 남겨 뒀습니다. 그러니 1024 는 서버가 작아서 붙은 값이 아닙니다. 램이 2기가든 128기가든 똑같이 1024 로 시작합니다.

시스템 전체를 막는 값도 따로 있습니다. fs.file-max 는 커널이 통틀어 내줄 수 있는 개수인데, 이 서버에서는 9223372036854775807 이 들어 있습니다. long 이 담을 수 있는 최대치라 사실상 막지 않겠다는 뜻이죠. 반면 fs.nr_open 은 1048576 이고, 이건 프로세스 하나의 hard 를 어디까지 올릴 수 있는지를 정한 진짜 천장입니다.

ulimit -Sn; ulimit -Hn
# 1024
# 1048576

systemctl show -p DefaultLimitNOFILESoft -p DefaultLimitNOFILE
# DefaultLimitNOFILESoft=1024
# DefaultLimitNOFILE=524288

cat /proc/sys/fs/file-max   # 9223372036854775807
cat /proc/sys/fs/nr_open    # 1048576

셸과 서비스는 soft 만 같고 천장이 다릅니다

값을 바꾸는 손이 여럿이라, 마지막에 이긴 값만 남는다

soft 는 프로세스가 자기 손으로 hard 까지 올릴 수 있습니다. 특별한 권한도 필요 없습니다. 그래서 연결을 많이 받는 서버 프로그램은 기동하면서 알아서 끌어올려 놓고 시작하는 경우가 많습니다.

hard 는 반대입니다. 내리는 건 자유지만 한 번 내리면 그 프로세스에서는 되돌리지 못하고, 올리려면 CAP_SYS_RESOURCE 가 있어야 합니다. 같은 파이썬 한 줄로 확인해 보면 soft 는 천장까지 순순히 올라가고, hard 를 1 만큼 더 올리려는 순간 거부당합니다.

그래서 유닛 파일에 적힌 값과 실제로 도는 프로세스의 값이 다를 수 있습니다. 이 서버의 php-fpm 은 1024 와 524288 로 유닛 설정 그대로인데, 아파치는 8192 와 8192 로 돌고 있습니다. 유닛이 넘겨준 값과 다르니 기동 과정 어딘가에서 프로세스가 스스로 바꾼 겁니다. 무엇이 바꿨는지는 그 프로그램을 따라가 봐야 알 수 있고, 여기서 중요한 건 설정 파일만 보고 단정하면 안 된다는 사실입니다.

그러니 판정은 언제나 /proc/<PID>/limits 에서 합니다. 지금 그 프로세스에 실제로 걸려 있는 두 값이 그대로 적혀 있습니다. 재시작할 여유가 없으면 prlimit 로 돌고 있는 프로세스의 한도를 그 자리에서 바꿀 수도 있습니다. 다만 이건 급할 때 쓰는 응급 조치라, 다음 재시작이면 유닛 설정으로 돌아갑니다.

python3 -c "import resource as r
s,h = r.getrlimit(r.RLIMIT_NOFILE); print('시작', (s,h))
r.setrlimit(r.RLIMIT_NOFILE, (h,h)); print('올린 뒤', r.getrlimit(r.RLIMIT_NOFILE))
r.setrlimit(r.RLIMIT_NOFILE, (h,h+1))"
# 시작 (1024, 1048576)
# 올린 뒤 (1048576, 1048576)
# ValueError: not allowed to raise maximum limit

grep 'open files' /proc/$(pgrep -f 'php-fpm: master' | head -1)/limits
# Max open files            1024                 524288               files

sudo prlimit --pid 12345 --nofile=4096:8192   # 재시작 없이 붙인다

soft 는 스스로 올라가고, hard 는 넘지 못합니다

설정 파일에 적힌 값이 아니라 /proc/<PID>/limits 에 적힌 값이 그 프로세스에 걸려 있는 값입니다.

ulimit 로 올렸는데 서비스는 그대로인 이유

여기서 가장 자주 헛발질하는 자리가 ulimit 입니다. ulimit -n 4096 은 지금 이 셸과 여기서 갈라져 나올 자식에게만 걸립니다. systemd 가 띄운 서비스는 그 셸의 자식이 아니라 PID 1 에서 갈라져 나온 프로세스라, 셸에서 아무리 올려도 값이 닿지 않습니다. /etc/security/limits.conf 도 마찬가지입니다. 그건 로그인 세션에 붙는 설정이지 서비스에 붙는 설정이 아닙니다.

서비스의 한도는 유닛의 LimitNOFILE= 로 바꿉니다. 값 하나만 쓰면 soft 와 hard 가 같아지고, 콜론으로 나눠 쓰면 둘을 따로 정합니다. 유닛 파일을 직접 고치는 대신 드롭인을 하나 얹으면 패키지를 업데이트해도 설정이 살아남습니다. 고친 다음에는 daemon-reload 와 restart 를 둘 다 해야 합니다. reload 만 하면 설정만 읽히고 돌고 있는 프로세스에는 붙지 않습니다.

메시지도 두 가지를 구분해 읽습니다. 프로세스 한도에 걸리면 EMFILE 이고 문장은 Too many open files 입니다. 시스템 전체 한도에 걸리면 ENFILE 이고 뒤에 in system 이 붙습니다. 앞서 봤듯 요즘 서버는 file-max 가 사실상 열려 있어서 뒤엣것을 볼 일은 드뭅니다. 문장 끝에 in system 이 없다면 sysctl 을 건드릴 자리가 아니라는 뜻입니다.

숫자를 올리기 전에 왜 늘었는지부터 봅니다. 열린 디스크립터를 세어 봤을 때 같은 소켓이나 같은 로그 파일이 수천 개 잡혀 있으면 그건 한도가 좁은 게 아니라 닫지 않는 코드가 있는 겁니다. 그 상태에서 한도만 키우면 터지는 날짜만 뒤로 밀립니다.

sudo systemctl edit myapp.service
# [Service]
# LimitNOFILE=16384:65536

sudo systemctl daemon-reload && sudo systemctl restart myapp
grep 'open files' /proc/$(systemctl show myapp -p MainPID --value)/limits

# 지금 몇 개를 쥐고 있는지, 무엇을 쥐고 있는지
sudo ls /proc/<PID>/fd | wc -l
sudo ls -l /proc/<PID>/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head

고친 뒤 확인까지 한 묶음으로 합니다

조치 후 확인할 것

  • 바뀐 값은 ulimit 말고 /proc/<PID>/limits 에서 확인한다. 서비스는 daemon-reload 와 restart 를 둘 다 해야 새 값이 붙는다.
  • ls /proc/<PID>/fd | wc -l 로 지금 쥐고 있는 개수를 세어 한도와 얼마나 떨어져 있는지 본다.
  • 같은 대상이 수천 개 잡혀 있으면 한도가 아니라 닫지 않는 코드가 원인이다. 숫자만 올리면 며칠 뒤 같은 자리에서 다시 걸린다.
  • hard 를 fs.nr_open(기본 1048576) 위로 올리려 하면 거부당한다. 그보다 큰 값이 필요하면 sysctl 부터 손댄다.
  • 에러 문장 끝에 in system 이 붙어 있는지 본다. 없으면 프로세스 한도 문제라 시스템 전체 설정은 건드릴 필요가 없다.

정리하면 이 한도는 파일 개수가 아니라 번호의 상한이고, soft 와 hard 로 두 겹이며, 셸과 서비스는 서로 다른 곳에서 값을 받아 옵니다. 이 셋만 손에 쥐고 있으면 다음에 같은 문장을 만났을 때 어디를 열어 봐야 하는지가 바로 잡힙니다. /proc 의 그 한 줄부터 보면 됩니다.