上限は一つではなく四つある
한국어 원문으로 표시합니다.
한 줄 요약
Too many open files 를 보면 거의 모두가 ulimit -n 65536 을 실행하고 재시작한다. 그리고 같은 에러를 다시 만난다. 한도는 하나가 아니라 넷이고, 셸에서 바꾼 값은 이미 떠 있는 서비스에 적용되지 않기 때문이다.
왜 이게 필요했나
nginx: accept4() failed (24: Too many open files) 또는 java.net.SocketException: Too many open files. 이 메시지에는 세 가지 오해가 겹쳐 있다.
- 한계가 하나라고 생각한다. 실제로는 넷이다.
- 셸에서 바꾼 값이 서비스에 적용된다고 생각한다. 적용되지 않는다.
- 한도를 올리면 해결된다고 생각한다. 대개는 누수라서 장애 시점만 몇 시간 뒤로 미뤄진다.
어떻게 동작하나
한도의 포함 관계는 이렇다.
fs.file-max 시스템 전체 합 (현대 리눅스에선 대개 병목이 아님)
fs.nr_open 한 프로세스의 RLIMIT_NOFILE 이 넘을 수 없는 커널 천장
RLIMIT_NOFILE hard 관리자가 정한 상한
RLIMIT_NOFILE soft 실제 적용값 (프로세스가 hard 까지 스스로 올릴 수 있다)
errno 가 층을 갈라 준다. EMFILE(24) 은 Too many open files 로 프로세스 한계, ENFILE(23) 은 Too many open files in system 으로 시스템 전역이다. in system 이라는 두 단어가 진단의 절반이다.
왜 셸의 ulimit 이 소용없나. /etc/security/limits.conf 는 PAM 모듈이 읽고, PAM 은 로그인 세션에서만 동작한다(SSH, su, login). systemd 가 부팅 시 띄운 서비스는 로그인 세션이 아니므로 이 파일을 아예 읽지 않는다. 서비스의 한도는 유닛 파일의 LimitNOFILE 로 정한다. 그리고 rlimit 은 프로세스 생성 시점에 적용되므로 reload 가 아니라 restart 여야 반영된다.
systemd 가 소프트를 낮게 두는 이유. 240 이후 systemd 는 하드를 크게(524288), 소프트를 낮게(1024) 유지한다. 호환성 때문이다 - select() 가 fd 번호 1024 이상을 다루지 못하고, 일부 오래된 프로그램이 기동 시 0부터 소프트 한계까지 모든 fd 를 닫는 루프를 돈다. 그 한계가 백만이면 기동에 몇 초가 걸린다. "필요한 프로그램이 스스로 올려 쓰라"는 설계다. Go 런타임은 실제로 시작할 때 소프트를 하드까지 올리기 때문에, 같은 머신에서 Go 서비스만 멀쩡한 상황이 생긴다.
확실한 확인 방법은 하나뿐이다.
grep 'Max open files' /proc/<PID>/limits # soft / hard
ls /proc/<PID>/fd | wc -l # 실제 사용량
lsof -p PID | wc -l 은 cwd, root, 실행 파일, mmap 된 라이브러리까지 세는데 이것들은 RLIMIT_NOFILE 에 포함되지 않아 숫자가 더 크게 나온다. 한계와 비교할 값이 아니다.
현장에서 만나는 모습
소켓 누수의 전형적인 모양. fd 종류를 세어 보면 socket 이 6만 개인데 ss 로 상태를 보면 CLOSE-WAIT 가 2만 개이고, 상대 주소별로 보니 전부 같은 Redis 서버다. CLOSE-WAIT 는 상대가 FIN 을 보냈는데 우리 쪽이 close() 를 호출하지 않은 상태이고, 커널이 무한정 기다리며 타임아웃이 없다. 즉 애플리케이션이 커넥션을 반환하지 않고 있다는 뜻이고, 코드 리뷰 범위가 그 자리에서 확정된다.
반대로 TIME-WAIT 는 커널이 알아서 정리하고 fd 도 소비하지 않는다. 둘을 혼동하면 엉뚱한 커널 파라미터를 만지게 된다.
누수인지 부하인지 구분하는 법. 1분 간격으로 fd 수와 ESTABLISHED 수를 함께 기록한다. 연결 수는 평평한데 fd 만 단조 증가하면 누수다. 그때 한도를 올리는 것은 장애를 몇 시간 뒤로 미루는 일일 뿐이다.
소켓도 fd 다. 파일만 세다 보면 놓치는데, 소켓·파이프·epoll·inotify 인스턴스가 전부 fd 를 소비한다. /proc/PID/fd 의 심링크를 readlink 해 보면 socket:[12345], pipe:[67890], anon_inode:[eventpoll] 처럼 종류가 드러난다.
한도가 실제로 어디서 오는가
Too many open files 를 만나면 ulimit -n 을 올리는 것이 첫 반응인데, 그것이
안 먹는 경우가 절반이다. 한도는 네 곳에서 오고, 어느 것이 이겼는지는 실행 중인
프로세스에서만 확인된다.
cat /proc/<pid>/limits | grep -i 'open files'
이 값이 진실이다. 셸에서 ulimit -n 을 아무리 바꿔도 이미 떠 있는 프로세스
에는 적용되지 않는다.
| 자리 | 적용 대상 |
|---|---|
ulimit -n |
그 셸과 그 자손 |
/etc/security/limits.conf |
PAM 을 거쳐 로그인한 세션 |
systemd 유닛의 LimitNOFILE |
그 서비스 — limits.conf 를 보지 않는다 |
| 컨테이너 런타임 설정 | 컨테이너 안 |
systemd 로 뜨는 서비스가 가장 많이 걸린다. limits.conf 를 고치고 재시작해도
안 바뀌는 이유가 이것이다. 유닛에 직접 적어야 한다.
[Service]
LimitNOFILE=65535
시스템 전체 한도도 따로 있다. 프로세스별 한도를 올려도 fs.file-max 에
걸리면 거기서 막힌다.
sysctl fs.file-max fs.nr_open
cat /proc/sys/fs/file-nr # 현재 사용, 미사용, 최대
무엇이 fd 를 먹고 있는지 센다. 소켓인지 파일인지에 따라 처방이 다르다.
ls -l /proc/<pid>/fd | awk '{print $NF}' | sed 's/:.*//' | sort | uniq -c | sort -rn | head
socket: 이 대부분이면 연결을 안 닫고 있는 것이고, 같은 파일이 수천 개면
파일을 열고 안 닫는 코드다. 한도를 올리는 것은 시간을 버는 것이지 고치는 것이
아니다.
엄청나게 큰 값을 넣지 않는다. LimitNOFILE=infinity 로 두면 어떤 프로그램은
0부터 그 수까지 전부 닫으려 시도하며 몇 분씩 멈춘다. 실제로 필요한 값보다 조금
크게 잡는다.
다음 실습에서 할 것
/proc/1/limits 에서 진짜 한도를 읽는 것으로 시작해, 같은 파일을 50번 열어 fd 는 여러 개인데 inode 는 하나로 수렴하는 구조를 확인한다. 한도를 낮춘 셸에서 EMFILE 을 직접 재현하고, 소켓도 fd 라는 것을 눈으로 본 뒤, fd 상위 프로세스를 뽑는 누수 추적 도구를 만든다.