포스트

디스크 꽉 찼을 때: df, du, lsof로 범인 찾기 + mount·fstab 기초

'No space left on device' 알림부터 시작해서 — 어느 파티션이 찼는지(df), 무엇이 잡아먹는지(du), 지웠는데 안 줄어드는 함정(lsof)까지. 그리고 블록 장치·마운트·fstab의 기초.

디스크 꽉 찼을 때: df, du, lsof로 범인 찾기 + mount·fstab 기초

새벽에 No space left on device 알림이 뜨거나, 배포가 갑자기 실패하거나, 로그가 안 쌓입니다 — 디스크가 꽉 찼다는 신호입니다. 그런데 막상 접속하면 /는 멀쩡해 보이는데 어디가 찬 건지, 파일을 지웠는데도 왜 공간이 안 돌아오는지, df는 남았다는데 왜 못 쓰겠다는 건지에서 자주 막힙니다.

이 글은 디스크가 찼을 때 어느 파티션이(df) → 무엇이(du) → 왜 안 줄어드는지(lsof) 순서로 범인을 좁히고, 그 뒤에 있는 블록 장치·마운트·/etc/fstab 기초를 함께 정리합니다.


1단계 — df로 어느 파티션이 찼는지부터

무작정 du로 파일을 뒤지기 전에, 어느 파일시스템이 찼는지부터 확인해야 헛수고를 안 합니다. /가 아니라 /var나 별도 마운트된 데이터 디스크가 찬 경우가 흔합니다.

1
2
3
df -h              # 모든 파일시스템의 사용량을 사람이 읽기 좋은 단위로
df -h /            # 특정 경로가 속한 파일시스템만
df -h /var/log     # 이 경로가 어느 파일시스템에 있는지도 함께 확인
1
2
3
4
5
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   48G  0.5G  99% /            ← 범인은 여기
/dev/sdb1       500G  120G  355G  25% /data
tmpfs           7.9G     0  7.9G   0% /dev/shm

Use%가 100%에 가까운 줄이 범인입니다. Mounted on 컬럼을 보고 그 마운트 지점 아래에서만 du를 돌려야 합니다. 위 예시라면 /data를 뒤질 게 아니라 / 아래를 봐야 합니다.

df파일시스템(마운트 지점) 단위로 보고합니다. /data가 별도 디스크로 마운트돼 있으면 du /로 세도 /data 용량은 안 잡힙니다(다른 파일시스템이라 건너뜀). 그래서 “전체가 아니라 찬 파일시스템을 골라 그 안을 판다”가 원칙입니다.


2단계 — du로 무엇이 잡아먹는지

찬 파일시스템을 알았으면, 그 안에서 큰 디렉토리를 위에서부터 좁혀 들어갑니다.

1
2
3
du -sh *                              # 현재 디렉토리 바로 아래 항목별 크기 (요약)
du -sh .[!.]* *                       # 숨김 파일·디렉토리까지 포함
du -h --max-depth=1 / | sort -rh | head    # 최상위 1단계 큰 디렉토리 top

옵션 의미:

옵션의미
-ssummarize — 하위를 합산한 총합만 (파일별로 안 늘어놓음)
-h사람이 읽는 단위(K/M/G)
--max-depth=11단계 깊이까지만 집계 (하위 디렉토리 총합을 나란히 비교)
-x다른 파일시스템으로 안 넘어감. /를 훑을 때 /data 같은 별도 마운트 제외

실전 흐름은 “큰 디렉토리로 한 단계씩 내려가기”입니다:

1
2
3
4
5
6
7
8
9
10
11
12
# 1) 루트에서 큰 디렉토리 top
sudo du -h --max-depth=1 -x / | sort -rh | head
# 15G  /var
# 8G   /usr
# ...

# 2) 범인 후보(/var)로 내려가서 다시
sudo du -h --max-depth=1 /var | sort -rh | head
# 12G  /var/log      ← 대개 여기

# 3) 그 안에서 실제 파일 확인
sudo du -sh /var/log/* | sort -rh | head

/var/log가 범인인 경우가 압도적으로 많습니다 — 로그 회전이 안 걸려 있거나 특정 서비스가 폭주해서입니다. 로그가 계속 쌓이는 근본 해결은 logrotate로 로그 회전 관리하기에서 다뤘습니다. 지금 급하면 큰 로그부터 비우고, 재발 방지는 그 글로.

대화형으로 훑고 싶다면 ncdudu를 커서로 탐색하는 TUI입니다. 방향키로 큰 디렉토리를 파고들며 그 자리에서 삭제까지 됩니다. apt install ncdu / dnf install ncdu. 한 번 크게 훑을 땐 du | sort보다 압도적으로 빠릅니다. 다만 서버 자동화·스크립트엔 du가 표준.


3단계 — 지웠는데 안 줄어드는 함정 (lsof)

여기가 이 글에서 가장 중요한 부분입니다. 파일을 rm 했는데 df가 그대로인 경험, 서버 만지면 반드시 겪습니다.

리눅스에서 파일을 지워도, 어떤 프로세스가 그 파일을 아직 열고 있으면 디스크 공간이 반환되지 않습니다. 파일 이름(디렉토리 항목)은 사라지지만, 그 파일을 연 프로세스가 살아 있는 한 커널은 실제 데이터 블록을 붙들고 있습니다. 프로세스가 종료되거나 파일을 닫아야 비로소 공간이 돌아옵니다.

가장 흔한 시나리오: 거대한 로그 파일을 rm으로 지웠는데, 그 로그를 쓰던 데몬(nginx, 애플리케이션 등)이 여전히 그 파일 핸들에 계속 로그를 씁니다 — 이름 없는 파일이 계속 커집니다.

1
2
lsof +L1                    # 링크 수가 1 미만인(=삭제됐지만 열린) 파일만
lsof | grep deleted         # 위가 없는 환경에서의 대체 (출력에 '(deleted)' 표시)
1
2
3
4
$ sudo lsof +L1
COMMAND   PID  USER   FD   TYPE ... SIZE/OFF NLINK ... NAME
nginx    1234  root    5w  REG  ...   12.0G     0  ... /var/log/nginx/access.log (deleted)
#                                     └ 12G를 붙들고 있음   └ NLINK=0 (삭제됨)

NAME(deleted)가 붙고 NLINK(링크 수)가 0인 줄이 범인입니다. 해결은 그 프로세스가 파일 핸들을 놓게 하는 것:

1
2
3
4
5
# 방법 1) 프로세스 재시작 (권장 — 안전하게 핸들 해제)
sudo systemctl restart nginx

# 방법 2) 로그 파일이면 truncate로 내용만 비우기 (핸들은 유지, 즉시 공간 회수)
sudo truncate -s 0 /proc/1234/fd/5     # 위 lsof의 PID/FD 사용

애초에 로그는 rm으로 지우지 말고 truncate -s 0으로 비우거나 logrotate에 맡기는 게 맞습니다 — rm 후 재시작 전까지 공간이 안 돌아오는 이 함정을 피할 수 있습니다.

lsof로 “무엇이 이 자원을 잡고 있나”를 역추적하는 패턴은 포트·파일 전반에 통합니다. lsof -p PID(그 프로세스가 연 것 전부), 프로세스를 실제로 정리하는 pkill/kill프로세스 찾고 종료하기에 정리돼 있습니다.


inode 고갈 — 공간은 남았는데 못 쓴다

또 하나의 헷갈리는 함정입니다. df -h공간이 남았다는데도 No space left on device가 뜬다면, 십중팔구 inode가 소진된 것입니다.

파일시스템은 공간(블록)과 별개로, 파일 하나당 하나씩 쓰는 inode(메타데이터 슬롯)를 정해진 개수만큼 갖습니다. 아주 작은 파일이 수백만 개(세션 파일, 캐시, 메일 스풀 등) 쌓이면 용량은 남아도 inode가 먼저 바닥납니다.

1
df -i              # inode 사용량 (df -h 의 inode 버전)
1
2
3
$ df -i
Filesystem      Inodes  IUsed  IFree IUse% Mounted on
/dev/sda1      3276800 3276792     8  100% /            ← inode가 꽉 참

IUse%가 100%면 이게 원인입니다. 어디에 작은 파일이 몰려 있는지는 디렉토리별 파일 개수로 찾습니다(크기가 아니라 개수):

1
2
# 현재 디렉토리 바로 아래, 각 디렉토리의 파일 개수 top
for d in */; do echo "$(find "$d" | wc -l) $d"; done | sort -rn | head

세션·캐시 디렉토리(/var/lib/php/sessions, /tmp 등)가 흔한 범인입니다. 오래된 것부터 정리하면 됩니다.

ext4는 파일시스템을 만들 때 inode 총수가 고정되어 나중에 늘리기 어렵습니다. 반면 XFS는 inode를 동적으로 할당해 이 문제에 강합니다. 작은 파일이 폭발적으로 늘어나는 워크로드라면 파일시스템 선택 단계에서 고려할 지점입니다.


블록 장치와 마운트 — lsblk, blkid, mount

여기서부터는 “디스크를 붙이고 떼는” 영역입니다. 새 디스크를 달았거나, 마운트가 풀렸을 때 씁니다.

1
2
3
4
5
lsblk                       # 블록 장치를 트리로 (디스크·파티션·마운트지점 한눈에)
lsblk -f                    # 파일시스템 종류·UUID·마운트지점까지
blkid                       # 각 파티션의 UUID·파일시스템 타입
sudo mount /dev/sdb1 /mnt   # sdb1 파티션을 /mnt 에 마운트
sudo umount /mnt            # 마운트 해제
1
2
3
4
5
6
$ lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   50G  0 disk
└─sda1   8:1    0   50G  0 part /
sdb      8:16   0  500G  0 disk
└─sdb1   8:17   0  500G  0 part /data

lsblk로 장치 트리를, blkid로 그 파티션의 UUID를 확인하는 게 시작입니다. UUID는 다음 fstab에서 쓰입니다.

umounttarget is busy로 거부되면, 그 마운트 지점 안에서 무언가가 파일을 열고 있는 것입니다. lsof /mnt 또는 fuser -m /mnt로 범인을 찾아 정리한 뒤 다시 시도하세요 — 위 3단계의 lsof 패턴과 같습니다.


/etc/fstab — 부팅 시 자동 마운트

mount 명령은 재부팅하면 사라집니다. 부팅할 때마다 자동으로 마운트되게 하려면 /etc/fstab에 등록합니다. 한 줄이 6개 필드로 구성됩니다:

1
UUID=abcd-1234  /data  ext4  defaults  0  2
필드이름의미
1장치마운트할 장치. /dev/sdb1보다 UUID=... 권장 (아래)
2마운트 지점어디에 붙일지 (/data)
3파일시스템ext4 · xfs · vfat
4옵션defaults, noatime, ro, nofail 등 (쉼표 구분)
5dump백업(dump) 대상 여부. 거의 항상 0
6pass부팅 시 fsck 검사 순서. /1, 나머지는 2, 검사 안 함은 0

UUID를 쓰는 이유: /dev/sdb, /dev/sdc 같은 장치명은 연결 순서·포트에 따라 바뀔 수 있습니다. 디스크를 하나 추가했더니 기존 sdbsdc가 되어 엉뚱한 디스크가 /data에 마운트되는 사고가 실제로 납니다. UUID는 파일시스템에 고정되어 장치명이 바뀌어도 항상 같은 디스크를 가리킵니다.

1
2
blkid /dev/sdb1
# /dev/sdb1: UUID="abcd-1234-..." TYPE="ext4"

이 UUID를 fstab 첫 필드에 넣습니다. 편집 후에는 재부팅하지 말고 아래로 검증합니다:

1
sudo mount -a       # fstab의 모든 항목을 지금 마운트 시도 (문법·마운트 테스트)

잘못된 fstab은 부팅을 막을 수 있습니다. 존재하지 않는 장치·오타·잘못된 옵션이 있으면 부팅이 emergency 모드로 떨어져 콘솔 없이는 복구가 어렵습니다. 그래서 편집 후 반드시 mount -a로 먼저 검증하고, 에러가 없을 때만 재부팅하세요. 필수가 아닌 데이터 디스크엔 옵션에 nofail을 넣어두면, 그 디스크가 없어도 부팅은 되게 만들 수 있습니다.


파일시스템 만들기 — mkfs (짧게)

새 디스크에 파일시스템이 없으면 만들어야 마운트됩니다.

1
2
sudo mkfs.ext4 /dev/sdb1      # ext4 파일시스템 생성
sudo mkfs.xfs  /dev/sdb1      # xfs 파일시스템 생성

ext4xfs 한 줄 비교: ext4는 리눅스의 오랜 기본값으로 어디서나 안정적이고, xfs는 대용량·병렬 I/O·inode 동적 할당에 강해 RHEL 계열의 기본값입니다. 일반적인 서버라면 배포판 기본을 따르면 되고, 작은 파일이 폭증하거나 초대용량이면 xfs를 고려합니다.

mkfs파일시스템을 새로 만들며 기존 데이터를 지웁니다. 장치명(/dev/sdb1)을 lsblk로 두세 번 확인하고 실행하세요. 파티션이 아닌 디스크 전체(/dev/sdb)에 실수로 걸면 파티션 테이블째 날아갑니다.

디스크를 여러 개 묶어 유연하게 늘리고 줄이는 LVM(논리 볼륨)은 이 글의 범위를 넘습니다 — 디스크 확장·스냅샷이 필요해지는 다음 단계에서 별도로 다룰 주제입니다.


실전 흐름

디스크 꽉 참 알림을 받았을 때의 순서:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1) 어느 파일시스템이 찼나
df -h

# 2) 공간이 남았다는데 안 써지면 inode 확인
df -i

# 3) 찬 파일시스템 안에서 큰 디렉토리 추적
sudo du -h --max-depth=1 -x / | sort -rh | head
sudo du -h --max-depth=1 /var | sort -rh | head    # 대개 /var/log

# 4) 지웠는데도 공간이 안 돌아오면 — 삭제됐지만 열린 파일
sudo lsof +L1

# 5) 해당 프로세스 재시작 (또는 truncate)
sudo systemctl restart <service>

대중성·대안

  • df/du/lsof/mount/lsblk/blkid/fstab전부 리눅스 표준. 어느 배포판에나 기본 포함이고, 이걸 벗어날 이유가 거의 없습니다. 디스크 진단의 사실상 표준 조합.
  • ncdudu의 대화형 TUI 대안. 한 번 크게 훑을 땐 압도적으로 편하지만 기본 설치는 아니라 별도 설치 필요. 자동화·스크립트엔 여전히 du.
  • duf(Go) — df의 컬러·정렬 개선판. 보기 좋지만 표준은 아니고, 서버 기본으로 깔려 있지 않음. 로컬 편의용.
  • dust(Rust) — du의 시각적 대체. 트리·막대그래프로 예쁘지만 니치. 탐색용으로는 좋음.

결론: 디스크가 찼을 땐 df -h(어디가) → du --max-depth=1 | sort -rh(무엇이) → lsof +L1(왜 안 줄어드나) → 공간 남는데 안 써지면 df -i(inode), 이 네 가지 흐름이면 진단은 사실상 끝납니다. 마운트는 UUID로 fstab에 적고 편집 후엔 반드시 mount -a로 검증하세요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.