리눅스 장애 대응 · 디스크와 inode · 이론
No space left on device 의 다섯 가지 얼굴
한 줄 요약
ENOSPC 는 "블록이 없다"가 아니라 "커널이 이 쓰기를 수용할 자원이 없다" 는 뜻이다. 그 자원은 블록일 수도, inode 일 수도, 예약분 제외 여유일 수도 있다.
왜 이게 필요했나
df -h /var 가 여유 35G 를 보여 주는데 touch /var/log/test 가 No 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 -i 의 IUse% 가 100% 면 확정이다. XFS 는 동적 할당이라 사실상 이 문제가 없다.
2. 삭제됐지만 열려 있는 파일. 앞 코스에서 배운 그 구조다. rm 은 링크만 지우고, 링크 수 0 그리고 열린 fd 0 일 때 비로소 블록이 반환된다. du 는 경로를 걷기 때문에 디렉터리 엔트리가 없는 그 파일을 못 센다. df 와 du 의 괴리가 결정적 신호다. 원인의 단골은 logrotate 가 회전 후 재오픈 신호를 안 보낸 경우다.
3. 예약 블록. ext 계열은 기본 5% 를 root 전용으로 남긴다. 목적이 둘 있다 - 꽉 차도 관리자가 정리할 여지를 남기고, 할당기가 연속 블록을 찾을 여지를 줘 단편화를 늦춘다. 증상이 특징적이다. root 로는 되는데 서비스 계정으로만 실패한다.
4. 마운트 가림. 파일이 있는 디렉터리 위에 다른 파일시스템을 마운트하면 원래 파일은 안 보이지만 아래 파일시스템의 공간은 계속 점유한다. du 는 위쪽만 걷기 때문에 어떤 도구로도 안 보인다.
5. inotify. watch 상한을 넘으면 inotify_add_watch 가 ENOSPC 를 반환한다. 디스크와 전혀 무관한데 문구는 똑같다.
현장에서 만나는 모습
df 20G / du 3.1G. 17GB 차이가 나면 삭제된 열린 파일을 의심한다. lsof -nP +L1 의 NLINK 열이 0 인 항목이 그것이고, SIZE 가 반환되지 않은 바이트다. lsof 가 없는 최소 이미지에서는 /proc/*/fd 를 직접 훑어 (deleted) 를 찾는다. 실습에서 하는 방식이 바로 이것이다.
응급 처치의 함정. truncate -s 0 /proc/PID/fd/N 은 파일을 비워 공간을 즉시 돌려준다. 하지만 파일이 O_APPEND 로 열려 있지 않으면 프로세스가 예전 오프셋에 계속 써서 sparse 파일이 되고, ls -l 크기는 그대로 크게 보인다. 응급 처치 전용이고, 정식 해결은 재오픈 신호(kill -USR1 등)나 재기동이다.
진단이 장애를 키우는 경우. 이미 I/O 가 포화된 디스크에서 find / 를 전체 스캔하면 상황이 더 나빠진다. -xdev 로 파일시스템 경계를 고정하고 범위를 좁혀서 시작한다.
다음 실습에서 할 것
inode 사용률과 디렉터리 용량을 직접 읽고, 빈 파일 500개가 왜 용량을 먹는지 확인한다. 그런 다음 삭제됐지만 열려 있는 파일을 직접 만들어 /proc 에서 붙잡고 있는 fd 를 찾고, 그 fd 경로로 내용을 복구한다. 마지막에는 시스템 전체에서 그런 유령 파일을 찾아내는 스크립트를 작성한다.