LabHub
배우기 러닝패스 코스

Linux Incident Response

The Five Faces of "No space left on device"

LabHub 에서 이어서 보기

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

한 줄 요약

ENOSPC 는 "블록이 없다"가 아니라 "커널이 이 쓰기를 수용할 자원이 없다" 는 뜻이다. 그 자원은 블록일 수도, inode 일 수도, 예약분 제외 여유일 수도 있다.

Concept map: "커널이 이 쓰기를 수용할 자원이 없다" · 0. 경로 착각. · 1. inode 고갈. · 포맷 시점에 inode 개수를 확정

왜 이게 필요했나

df -h /var 가 여유 35G 를 보여 주는데 touch /var/log/testNo space left on device 로 실패한다. 애플리케이션은 로그를 못 쓰고, 배포는 임시 파일 생성에서 죽고, DB 는 WAL 을 못 쓴다.

여기서 "df 가 틀렸다"고 결론 내리면 진전이 없다. df 는 자기가 아는 것(블록 사용량)을 정직하게 보고했을 뿐이고, 실패의 원인이 블록이 아니었을 뿐이다.

어떻게 동작하나

가능성을 확률 순으로 정리하면 다섯 가지다.

0. 경로 착각. / 는 여유가 있는데 /var 가 별도 파티션이고 그쪽이 꽉 찬 경우. 30초면 확인된다 - findmnt -T /var/log/app.log 로 그 경로가 어느 파일시스템에 속하는지부터 확정한다. /tmp 가 tmpfs 면 디스크와 무관하게 ENOSPC 가 나고 동시에 그만큼 메모리를 먹는다.

1. inode 고갈. ext2/3/4 는 포맷 시점에 inode 개수를 확정하고 이후 늘릴 수 없다. 기본 프로파일이 대략 16KB 당 하나이므로 100GB 파일시스템에 약 655만 개다. 평균 파일 크기가 그보다 작은 워크로드(세션 파일, 메일 큐, 캐시)면 블록보다 inode 가 먼저 떨어진다. df -iIUse% 가 100% 면 확정이다. XFS 는 동적 할당이라 사실상 이 문제가 없다.

2. 삭제됐지만 열려 있는 파일. 앞 코스에서 배운 그 구조다. rm 은 링크만 지우고, 링크 수 0 그리고 열린 fd 0 일 때 비로소 블록이 반환된다. du 는 경로를 걷기 때문에 디렉터리 엔트리가 없는 그 파일을 못 센다. df 와 du 의 괴리가 결정적 신호다. 원인의 단골은 logrotate 가 회전 후 재오픈 신호를 안 보낸 경우다.

3. 예약 블록. ext 계열은 기본 5% 를 root 전용으로 남긴다. 목적이 둘 있다 - 꽉 차도 관리자가 정리할 여지를 남기고, 할당기가 연속 블록을 찾을 여지를 줘 단편화를 늦춘다. 증상이 특징적이다. root 로는 되는데 서비스 계정으로만 실패한다.

4. 마운트 가림. 파일이 있는 디렉터리 위에 다른 파일시스템을 마운트하면 원래 파일은 안 보이지만 아래 파일시스템의 공간은 계속 점유한다. du 는 위쪽만 걷기 때문에 어떤 도구로도 안 보인다.

5. inotify. watch 상한을 넘으면 inotify_add_watchENOSPC 를 반환한다. 디스크와 전혀 무관한데 문구는 똑같다.

현장에서 만나는 모습

df 20G / du 3.1G. 17GB 차이가 나면 삭제된 열린 파일을 의심한다. lsof -nP +L1NLINK 열이 0 인 항목이 그것이고, SIZE 가 반환되지 않은 바이트다. lsof 가 없는 최소 이미지에서는 /proc/*/fd 를 직접 훑어 (deleted) 를 찾는다. 실습에서 하는 방식이 바로 이것이다.

응급 처치의 함정. truncate -s 0 /proc/PID/fd/N 은 파일을 비워 공간을 즉시 돌려준다. 하지만 파일이 O_APPEND 로 열려 있지 않으면 프로세스가 예전 오프셋에 계속 써서 sparse 파일이 되고, ls -l 크기는 그대로 크게 보인다. 응급 처치 전용이고, 정식 해결은 재오픈 신호(kill -USR1 등)나 재기동이다.

진단이 장애를 키우는 경우. 이미 I/O 가 포화된 디스크에서 find / 를 전체 스캔하면 상황이 더 나빠진다. -xdev 로 파일시스템 경계를 고정하고 범위를 좁혀서 시작한다.

dfdu 가 다른 말을 할 때

디스크가 찼다는 알림을 받고 들어가 보면 du 로는 아무리 더해도 그만큼이 안 나오는 경우가 있다. 셋 중 하나다.

지웠지만 아직 열려 있는 파일. 유닉스에서 파일을 지우는 것은 디렉터리에서 이름을 떼는 것뿐이다. 어떤 프로세스가 그 파일을 열고 있는 한 블록은 반환되지 않는다. 로그 파일을 rm 으로 지웠는데 프로세스가 계속 그 fd 에 쓰고 있는 상황이 전형적이다. du 는 이름을 따라 걷기 때문에 이 용량을 보지 못하고, df 는 블록을 세기 때문에 보인다.

lsof +L1                      # 링크 수가 0인, 즉 지워졌는데 열려 있는 파일
ls -l /proc/<pid>/fd | grep deleted

되찾는 방법은 프로세스를 재시작하는 것이고, 그럴 수 없다면 fd 를 잘라 쓴다. : > /proc/<pid>/fd/3 은 파일을 지우지 않고 크기만 0 으로 만든다. rm 대신 truncate 나 로그 로테이션의 copytruncate 를 쓰는 이유가 이것이다.

inode 가 먼저 떨어진 경우. 블록은 남았는데 파일을 못 만드는 상태다. No space left on device 라는 같은 메시지가 나오므로 증상만 보면 구별되지 않는다. 작은 파일이 수백만 개 쌓이는 캐시 디렉터리, 메일 큐, 세션 파일이 원인이다.

df -i                                    # 사용률이 100%인 파일 시스템
find /var -xdev -type f | cut -d/ -f1-4 | sort | uniq -c | sort -rn | head

-xdev 를 빼면 다른 마운트로 넘어가 엉뚱한 곳을 센다. inode 수는 파일 시스템을 만들 때 정해지므로(ext4 기준) 나중에 늘릴 수 없다. XFS 는 동적으로 할당하지만 imaxpct 상한이 있다.

마운트에 가려진 파일. /data 에 파일을 잔뜩 쓴 뒤에 그 자리에 디스크를 마운트하면, 원래 있던 파일은 보이지 않으면서 루트 파일 시스템의 공간은 계속 차지한다. 마운트를 걷어내야 보인다.

mkdir /mnt/root && mount --bind / /mnt/root
du -shx /mnt/root/data

마지막으로 예약 블록을 기억한다. ext4 는 기본으로 5%를 root 에게 남겨 둔다. 일반 사용자에게는 이미 꽉 찬 것으로 보이지만 df 는 아직 여유가 있다고 말한다. 데이터 전용 디스크라면 tune2fs -m 1 로 줄여도 되지만, 루트 파일 시스템에서는 건드리지 않는다 — 그 여유가 시스템이 스스로를 복구할 마지막 자리다.

다음 실습에서 할 것

inode 사용률과 디렉터리 용량을 직접 읽고, 빈 파일 500개가 왜 용량을 먹는지 확인한다. 그런 다음 삭제됐지만 열려 있는 파일을 직접 만들어 /proc 에서 붙잡고 있는 fd 를 찾고, 그 fd 경로로 내용을 복구한다. 마지막에는 시스템 전체에서 그런 유령 파일을 찾아내는 스크립트를 작성한다.