LabHub
배우기 러닝패스 코스

CPU・メモリリークの見極め

ディスクとファイルディスクリプタ

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

df 에 여유가 있는데 쓸 수 없으면 inode 가 떨어졌거나 지워진 파일을 누가 열고 있다. 둘 다 df -h 로는 안 보인다.

概念マップ: inode 가 떨어졌거나 지워진 파일을 누가 열고 있다.・inode 고갈.・지워졌지만 열려 있는 파일.・한도를 올리는 것은 시간을 버는 것이지 고치는 것이 아니다.

왜 이게 필요했나

디스크 관련 신고에서 가장 자주 나오는 두 가지가 이것이다.

inode 고갈. 파일 하나하나가 inode 를 하나씩 쓴다. 작은 파일이 수백만 개면 용량은 남아도 inode 가 먼저 떨어진다. df -i 로 봐야 보인다.

지워졌지만 열려 있는 파일. 로그 파일을 rm 했는데 프로세스가 아직 그 파일을 열고 있으면, 리눅스는 링크만 지우고 실제 블록은 반환하지 않는다. df 는 여전히 꽉 찼다고 하고 du 는 파일이 없다고 한다. 이 불일치가 결정적 단서다.

# du 와 df 가 다르면 이걸 본다
ls -l /proc/*/fd 2>/dev/null | grep deleted

해결은 프로세스에게 파일을 다시 열게 하는 것이다(로그라면 logrotatecopytruncate 또는 데몬에 SIGHUP). 프로세스를 재시작해도 된다.

어떻게 동작하나

파일 디스크립터 누수는 서서히 죽는다.

ls /proc/<PID>/fd | wc -l              # 지금 몇 개 열었나
cat /proc/<PID>/limits | grep 'open files'   # 한도

이 숫자가 시간에 따라 단조 증가하면 닫지 않는 코드가 있다. 한도에 닿는 순간 Too many open files 로 요청이 전부 실패한다. 한도를 올리는 것은 시간을 버는 것이지 고치는 것이 아니다.

흔한 착각

ulimit -n 이 무제한이면 안전하다는 착각. 컨테이너의 기본 RLIMIT_NOFILE 은 사실상 무제한(10억 대)인데, 오래된 데몬 중에는 시작할 때 그 숫자만큼 연결 테이블을 잡으려다 메모리를 다 쓰고 죽는 것이 있다. 무제한이 항상 좋은 것은 아니다.

디스크가 느린지 판정하는 두 숫자

iostat -x 1 의 여러 열 중 실제로 쓰는 것은 둘입니다.

Device  r/s   w/s  rkB/s  wkB/s  r_await  w_await  aqu-sz  %util
nvme0n1 120  380   4800  15200     0.31     0.52    1.24    38.2

%util 은 믿지 마세요. 이 값은 "요청이 하나라도 진행 중이던 시간의 비율" 이라, 병렬 처리가 되는 NVMe 에서는 100% 여도 여유가 남아 있습니다. 회전 디스크 시절의 지표입니다.

어떤 프로세스가 디스크를 쓰는가

# 실시간으로 I/O 상위 프로세스
iotop -oPa            # -o: 실제로 I/O 하는 것만, -a: 누적

# iotop 이 없을 때 (컨테이너에서 흔하다)
cat /proc/<PID>/io    # read_bytes, write_bytes 를 두 번 읽어 차이를 낸다

/proc/<PID>/ioread_bytes실제 디스크에서 읽은 양 이고 rchar 는 읽기 시스템 호출로 넘긴 양입니다. 둘의 차이가 크면 페이지 캐시가 잘 듣고 있다는 뜻이라 좋은 신호입니다. 반대로 둘이 비슷하면 캐시가 안 먹고 매번 디스크를 칩니다.

컨테이너에서 I/O 를 제한하기

CPU·메모리와 달리 쿠버네티스에는 I/O 제한 필드가 없습니다. cgroup v2 에는 io.max 가 있지만 파드 스펙으로 노출되지 않습니다. 그래서 실무의 대응은 다릅니다.

ionice 는 CFQ·BFQ 스케줄러에서만 듣습니다. 요즘 NVMe 기본값인 none(멀티큐) 에서는 아무 효과가 없으니 cat /sys/block/nvme0n1/queue/scheduler 로 먼저 확인합니다.

실무에서 진짜 중요한 것

로그 로테이션 설정에서 createcopytruncate 의 차이가 여기에 걸린다.

어느 쪽도 공짜가 아니다. 애플리케이션이 SIGHUP 을 처리하면 create, 못 하면 copytruncate 를 쓰되 유실 가능성을 안다.