새벽 3시, df -h엔 용량이 남았는데 배포가 안 된다
배포 파이프라인이 멈추고, 애플리케이션 로그에는 딱 한 줄만 반복해서 찍힌다.
write /var/log/app/app.log: no space left on deviceno space left on device는 커널 레벨에서 ENOSPC(errno 28)로 올라오는 에러다. 이름만 보면 "디스크 꽉 참"이지만, 실제로 장애 대응 현장에서 가장 헷갈리는 지점은 바로 이거다.
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 50G 31G 17G 65% /용량은 17G나 남았는데 왜 쓰기가 안 될까? 여기서 당황하면 30분이 그냥 날아간다. 사실 ENOSPC를 유발하는 원인은 "파티션 용량 초과" 하나가 아니라 최소 5가지다. 이 글은 그 5가지를 5개 명령으로 30초 안에 분기하고, 원인별 복붙 명령으로 즉시 복구한 뒤 재발까지 막는 런북이다. 장애 중에 바로 복사해 쓸 수 있도록 명령 블록 위주로 구성했다.
참고: 아래 명령은 일반적인 Linux(systemd 기반) 환경 기준이다. 배포판·파일시스템·컨테이너 런타임에 따라 결과 해석이 달라지는 지점은 각 섹션에서 "환경에 따라 다름"으로 표기했다.
30초 판정 트리 — 5개 명령으로 원인 좁히기
장애가 나면 고민하지 말고 위에서부터 아래로 순서대로 5개를 친다. 대부분 3번째 명령 안에서 원인이 특정된다.
# 1. 파티션 용량이 실제로 꽉 찼나?
df -h
# 2. inode가 고갈됐나? (용량은 남았는데 여기서 100%면 그거다)
df -i
# 3. 삭제됐지만 프로세스가 붙잡고 있는 파일이 있나?
sudo lsof +L1 2>/dev/null | head -20
# 4. 어느 디렉터리가 용량을 먹고 있나? (마운트 경계 안에서)
sudo du -xh --max-depth=1 / 2>/dev/null | sort -rh | head
# 5. Docker 오버레이/이미지/볼륨이 범인인가?
docker system df이 결과를 아래 표에 대입하면 원인이 나온다.
| 명령 | 이 값이 나오면 | 원인 | 다음 액션 |
|---|---|---|---|
df -h | 해당 마운트 Use% = 100% | 파티션 용량 초과 | du -xh로 상위 디렉터리 추적 (4번) |
df -i | IUse% = 100% (용량은 여유) | inode 고갈 | 대량 소형 파일 디렉터리 찾아 삭제 |
lsof +L1 | 삭제된 파일을 잡은 PID 표시 | 삭제됐지만 열린 파일 핸들 | 프로세스 재시작 or fd truncate |
du -xh | 특정 디렉터리가 비정상적으로 큼 | 로그/코어덤프/대용량 파일 | 해당 파일 정리 |
docker system df | Images/Containers/Volumes 거대 | /var/lib/docker 누적 | docker system prune (주의) |
핵심은 df -h와 df -i를 항상 같이 본다는 것이다. df -h만 보고 "용량 남았네" 하고 넘어가면 함정 2종에 그대로 걸린다.
"용량은 남았는데 안 되는" 함정 2종
ENOSPC가 유독 사람을 미치게 만드는 이유는, df -h가 멀쩡해 보이는 두 가지 케이스 때문이다.
함정 A: inode 고갈 — 용량 60%인데 파일을 못 만든다
파일시스템은 데이터 블록과 별개로 inode라는 메타데이터 슬롯을 쓴다. 파일 하나당 inode 하나가 필요한데, 세션 파일·캐시·메일 큐처럼 아주 작은 파일이 수백만 개 쌓이면 용량은 멀쩡한데 inode가 먼저 바닥난다.
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 50G 30G 20G 60% / # 용량은 60%
$ df -i /
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p1 3276800 3276800 0 100% / # inode는 100%!IUse%가 100%면 확정이다. 이제 어디가 inode를 먹었는지 찾는다.
# 하위 디렉터리별 파일 개수 카운트 (범인 찾기)
for d in /tmp /var/tmp /var/lib/php/sessions /var/spool; do
echo -n "$d: "; find "$d" -xdev -type f 2>/dev/null | wc -l
done환경에 따라 다름:
ext4는 포맷 시점에 inode 개수가 고정된다(나중에 못 늘림). 반면xfs는 inode를 동적 할당하므로df -i의IFree가 상황에 따라 다르게 표시되고 일반적으로 고갈이 덜 발생한다. 즉 이 함정은 ext4에서 특히 자주 보고된다.
함정 B: 삭제됐지만 프로세스가 붙잡은 파일
로그가 커져서 rm -f app.log로 지웠는데도 용량이 안 돌아오는 상황. du로는 안 잡히는데 df는 여전히 꽉 차 있다.
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 50G 50G 0 100% /
$ sudo du -xh --max-depth=1 / | sort -rh | head -3
12G /var
8.0G /usr
3.0G /home
# 다 더해도 50G가 안 됨 → du로 안 잡히는 용량이 있다
$ sudo lsof +L1
COMMAND PID USER FD TYPE ... SIZE/OFF NLINK NODE NAME
java 2314 app 5w REG ... 23622320128 0 1835012 /var/log/app/app.log (deleted)이게 정답이다. 파일을 rm으로 지워도 프로세스가 파일 핸들(fd)을 잡고 있으면 inode가 해제되지 않고, 용량도 반환되지 않는다. NLINK 0에 (deleted)가 붙은 게 스모킹 건이다. 여기서 NAME 앞의 PID(2314)와 FD 번호(5)를 기억해 둔다.
원인별 복구 복붙 세트
원인을 특정했으니 이제 고친다. 위험한 명령에는 경고를 달았으니 그대로 넘기지 말자.
1) systemd journal / 로그 정리
로그가 /var/log/journal에 무한정 쌓이는 경우가 흔하다.
# 현재 저널 용량 확인
journalctl --disk-usage
# 200M 남기고 정리 (예시 수치 — 워크로드별 조정)
sudo journalctl --vacuum-size=200M
# 시간 기준으로도 가능
sudo journalctl --vacuum-time=3d
# 정리 후 재확인 (예상: Archived and active journals take under 200.0M)
journalctl --disk-usage환경에 따라 다름:
journalctl은 systemd 기반 배포판에서만 동작한다. systemd가 없거나 로그를/var/log/*.log파일로 직접 남기는 환경에서는 이 명령이 없다. 그럴 땐 아래 대용량 파일 탐색으로 간다.
2) 대용량 파일 / 코어덤프 탐색
# /var 안에서 100M 넘는 파일 찾기 (마운트 경계 유지: -xdev)
sudo find /var -xdev -type f -size +100M -exec ls -lh {} \; 2>/dev/null
# 코어덤프 잔해 찾기
sudo find / -xdev -name 'core.*' -type f 2>/dev/null
sudo find / -xdev -name 'core' -type f 2>/dev/null
# 확인 후 삭제 (경로 반드시 눈으로 검증하고!)
# sudo rm -f /var/dump/core.12345-xdev를 붙이면 다른 마운트로 넘어가지 않아 원인 파티션에만 집중할 수 있다.
3) Docker — /var/lib/docker 누적 정리
컨테이너/CI 러너 환경에서 ENOSPC의 대표 원인이다. 오버레이 레이어·중단된 컨테이너·dangling 이미지·빌드 캐시가 계속 쌓인다.
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 48 6 22.3GB 18.1GB (81%)
# Containers 12 3 1.2GB 900MB
# Build Cache 210 0 9.4GB 9.4GBRECLAIMABLE이 크면 정리 대상이다. 안전한 순서로 좁혀 간다.
# (가장 안전) 중지된 컨테이너·dangling 이미지·미사용 네트워크·빌드 캐시만 정리
docker system prune
# 미사용 이미지까지 전부 (실행 중이 아닌 이미지 제거)
docker system prune -a
# 빌드 캐시만 따로
docker builder prune⚠️ 위험 경고 —
--volumes옵션Bashdocker system prune -a --volumes # ← 프로덕션에서 함부로 치지 말 것
--volumes는 컨테이너에 연결되지 않은 볼륨의 데이터를 영구 삭제한다. DB 컨테이너가 잠깐 내려가 있거나, named volume이 일시적으로 detach된 상태라면 프로덕션 DB 데이터가 통째로 날아갈 수 있다. 볼륨은 반드시docker volume ls로 목록을 확인하고 개별 삭제하는 편이 안전하다.
환경에 따라 다름:
/var/lib/docker경로는daemon.json의data-root설정으로 커스텀 위치일 수 있다. 스토리지 드라이버(overlay2 등)에 따라 레이어 저장 방식도 다르다.docker info | grep -e "Docker Root Dir" -e "Storage Driver"로 실제 위치를 먼저 확인하자.
4) 삭제됐지만 열린 파일 — 무중단 트렁케이트
정석은 파일을 잡고 있는 프로세스를 재시작하는 것이다. 재시작하면 fd가 닫히고 용량이 즉시 반환된다.
# 확인했던 PID(2314)의 프로세스를 재시작 (예: systemd 서비스)
sudo systemctl restart app.service재시작이 불가능한(무중단 필요) 상황이라면, /proc/<PID>/fd/<N>를 통해 열린 파일을 직접 비운다. 삭제된 파일이라도 fd 경로로 접근 가능하기 때문이다.
# lsof에서 확인한 PID=2314, FD=5 였다면
# (반드시 lsof +L1로 대상이 맞는지 다시 검증 후!)
sudo truncate -s 0 /proc/2314/fd/5
# 또는
sudo sh -c ': > /proc/2314/fd/5'이렇게 하면 프로세스는 계속 그 fd에 쓰지만 파일 내용이 0바이트가 되어 용량이 반환된다. 단, 이건 임시 조치다. 프로세스는 여전히 "존재하지 않는 파일"에 쓰고 있으므로, 여유가 생기면 반드시 재시작해 fd를 정상화하고 로그로테이션을 손봐야 한다.
5) inode 고갈 — 대량 소형 파일 삭제
용량이 아니라 개수가 문제이므로, 큰 파일이 아니라 작은 파일 덩어리를 지운다. 지우기 전에 개수부터 세서 대상이 맞는지 확인한다.
# 먼저 카운트 (예: /tmp에 파일이 몇 개인가)
find /tmp -xdev -type f 2>/dev/null | wc -l
# 확인 후 삭제 (오래된 임시파일만 지우고 싶으면 -mtime 조합)
sudo find /tmp -xdev -type f -mtime +3 -delete
# 세션 파일 등 특정 디렉터리 대량 삭제
# sudo find /var/lib/php/sessions -xdev -type f -mtime +1 -deleterm -rf *는 인자 개수가 많으면 argument list too long으로 실패하니, 대량 삭제는 find ... -delete가 안전하다.
재발 방지 — 복구 후 30분 안에 걸어둘 것
불을 껐으면 다시 안 붙게 만들어야 한다.
logrotate 설정
로그 미로테이션은 ENOSPC의 단골이다. 특히 ephemeral 노드나 컨테이너처럼 오래 살지 않는 환경일수록 놓치기 쉽다.
# /etc/logrotate.d/app
/var/log/app/*.log {
size 100M # 100M 넘으면 로테이션 (예시 — 조정 필요)
rotate 5 # 5개 보관
compress # gzip 압축
delaycompress
missingok
notifempty
copytruncate # 앱 재시작 없이 원본을 비움 (fd 유지형 로그에 유용)
}copytruncate는 함정 B(삭제된 열린 파일)를 예방하는 데 특히 유용하다. 파일을 지우는 대신 내용을 비우므로 fd가 유지된다.
# 설정 문법 검증 및 강제 실행 테스트
sudo logrotate -d /etc/logrotate.d/app # dry-run
sudo logrotate -f /etc/logrotate.d/app # 강제 실행모니터링 임계치 — 용량과 inode를 별도로 감시
가장 중요한 교훈은 이거다. 용량(df -h)만 감시하면 함정 2종에 또 걸린다. inode 사용률을 반드시 별도 지표로 잡아야 한다.
| 지표 | 소스 | Warning | Critical |
|---|---|---|---|
| 디스크 사용률 | df -h / node_exporter filesystem_avail | 80% | 90% |
| inode 사용률 | df -i / filesystem_files_free | 80% | 90% |
/var/lib/docker 크기 | 별도 스크립트 | 조정 필요 | 조정 필요 |
위 80%/90%는 예시 값이다. 로그 폭증 속도가 빠른 워크로드라면 더 낮게, 안정적인 워크로드면 더 높게 잡는다. 워크로드별 조정이 필요하다.
Prometheus를 쓴다면 node_exporter의 node_filesystem_files_free(inode)와 node_filesystem_avail_bytes(용량)를 두 개의 별도 알람으로 걸어두는 게 핵심이다.
판정 트리 요약 카드
장애가 나면 이 순서만 기억하자.
1. df -h → 100%면 용량 초과 → du -xh로 큰 파일 추적
2. df -i → 100%면 inode 고갈 → 대량 소형 파일 find -delete
3. lsof +L1 → (deleted)면 열린 파일 → 프로세스 재시작 or truncate
4. du -xh / → 범인 디렉터리 특정
5. docker system df → 오버레이 누적이면 prune (--volumes 주의!)df -h와 df -i를 항상 세트로 본다는 것, 그리고 --volumes는 함부로 치지 않는다는 것. 이 두 가지만 지켜도 새벽 3시의 ENOSPC는 30초짜리 문제가 된다.
자주 묻는 질문 (FAQ)
Q. df -h에는 용량이 남았다고 나오는데 왜 no space left on device가 뜨나요?
A. 대표적으로 두 가지입니다. (1) inode 고갈 — 작은 파일이 너무 많아 df -i의 IUse%가 100%인 경우, (2) 삭제됐지만 프로세스가 fd로 붙잡고 있는 파일 — lsof +L1에 (deleted)로 뜨는 경우입니다. df -h만 보지 말고 df -i와 lsof +L1을 함께 확인하세요.
Q. docker system prune -a --volumes를 쳐도 안전한가요?
A. 안전하지 않을 수 있습니다. --volumes는 실행 중 컨테이너에 연결되지 않은 볼륨 데이터를 영구 삭제합니다. DB 컨테이너가 잠시 내려가 있는 순간이라면 프로덕션 데이터가 유실될 수 있습니다. 먼저 docker volume ls로 확인하고, 웬만하면 --volumes 없이 docker system prune -a부터 시도하세요.
Q. inode 고갈은 df -i가 100%인데, 그냥 용량을 늘리면 해결되나요?
A. 파일시스템에 따라 다릅니다. ext4는 포맷 시 inode 개수가 고정되어 있어 디스크만 키운다고 inode가 늘지 않습니다(재포맷이나 mkfs 옵션 조정 필요). xfs는 동적 할당이라 상대적으로 여유가 있습니다. 근본 해결은 대량 소형 파일을 만드는 원인(세션·캐시·임시파일)을 정리하고 정기 삭제 스케줄을 거는 것입니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.