Published on

리눅스 디스크 100%인데 용량이 안 줄 때 - deleted-but-open(lsof)

Authors

서버에서 df -h는 100%를 가리키는데, 분명히 로그/덤프 파일을 rm로 지웠고 du -sh로 봐도 큰 파일이 안 보이는 상황이 있습니다. 이때 많은 경우 원인은 deleted-but-open 파일입니다. 즉, 파일은 디렉터리 엔트리에서 삭제되었지만(그래서 du에 잘 안 잡힘), 어떤 프로세스가 그 파일을 열린 파일 디스크립터(FD) 로 계속 잡고 있어 커널이 블록을 회수하지 못하는 상태입니다.

이 글에서는 (1) 왜 이런 일이 생기는지, (2) lsof로 어떻게 정확히 찾아내는지, (3) 서비스 중단 없이/최소화하며 공간을 회수하는 방법, (4) 컨테이너·Kubernetes 환경에서의 변형 케이스까지 실전 중심으로 정리합니다.

1) 왜 rm 했는데도 디스크가 안 비나?

리눅스 파일 삭제는 “데이터 블록을 즉시 지우는” 동작이 아니라, 디렉터리에서 파일 이름(링크)을 제거하는 동작에 가깝습니다.

  • 프로세스가 파일을 열면 커널은 inode와 데이터 블록을 참조하는 FD를 유지합니다.
  • rm은 파일 이름을 지우고 링크 카운트를 감소시킵니다.
  • 링크 카운트가 0이더라도 열려 있는 FD가 남아 있으면 커널은 그 inode/블록을 유지합니다.
  • 그 프로세스가 종료되거나 FD를 닫을 때 비로소 블록이 회수되어 df 사용량이 줄어듭니다.

그래서 다음과 같은 전형적인 증상이 나타납니다.

  • df -h : 사용량이 높고 줄지 않음
  • du -sh / : 큰 사용량이 안 보이거나 df와 차이가 큼
  • 로그 로테이션 직후, 애플리케이션이 오래된 로그 파일 핸들을 계속 잡고 있음

2) 먼저 증상 확인: df vs du 불일치

deleted-but-open을 의심하기 전에, 기본적으로 dfdu의 관점 차이를 확인해두면 디버깅이 빨라집니다.

# 파일시스템 관점(할당된 블록)
df -hT

# 디렉터리 트리 관점(보이는 파일 합)
du -xh / | sort -h | tail -n 30
  • df: 파일시스템이 “할당한 블록” 기준
  • du: 현재 경로에서 “보이는 파일” 기준

rm로 삭제된 파일은 디렉터리에서 보이지 않으므로 du에 안 잡히지만, 블록은 여전히 할당 상태일 수 있어 df에는 남습니다.

3) 핵심 진단: lsof로 deleted-but-open 찾기

가장 확실한 방법은 lsof삭제되었지만 열려 있는 파일을 찾는 것입니다.

3.1 lsof로 (deleted) 파일 필터링

# (deleted) 표시가 있는 열린 파일 찾기
sudo lsof +L1 | grep -i deleted
  • +L1은 링크 카운트가 1 미만(즉 0)인 파일을 보여줍니다.
  • 출력에 deleted가 붙으면 거의 확정입니다.

자주 쓰는 형태는 다음입니다.

# 특정 마운트/경로에 대해서만 보고 싶을 때
sudo lsof +L1 /var | grep -i deleted

# 용량 큰 것부터 보고 싶을 때(간단 파이프)
sudo lsof +L1 | awk 'BEGIN{OFS="\t"} {print $7,$2,$1,$9}' | sort -n | tail -n 20

lsof의 SIZE/OFF(바이트) 컬럼이 큰데, 파일 경로가 ... (deleted)로 나오면 그 프로세스가 공간을 붙잡고 있습니다.

3.2 /proc로도 확인 가능

lsof가 없거나 최소 환경일 때는 /proc/<pid>/fd로 확인할 수 있습니다.

PID=1234
ls -l /proc/$PID/fd | grep deleted

심볼릭 링크가 ... (deleted)로 보이면 동일한 문제입니다.

4) 공간 회수 방법: “프로세스가 FD를 닫게” 만들기

해결은 단순합니다. 해당 프로세스가 그 파일 FD를 닫도록 해야 합니다. 방법은 상황에 따라 3가지로 나뉩니다.

4.1 가장 안전: 서비스 재시작(또는 로그 재오픈)

대부분의 케이스는 로그 파일입니다. 애플리케이션/데몬을 재시작하면 FD가 닫히면서 공간이 회수됩니다.

# systemd 서비스라면
sudo systemctl restart nginx
sudo systemctl restart your-app

# 재시작 후 확인
sudo lsof +L1 | grep -i deleted

df -h

재시작이 자주 필요하거나 재시작 루프가 엮여 있다면 systemd StartLimitHit 같은 이슈가 함께 발생할 수 있습니다. 그 경우는 systemd 재시작 루프(StartLimitHit) 해결법도 같이 참고하면 좋습니다.

4.2 무중단에 가깝게: 로그 재오픈 시그널(HUP/USR1)

일부 데몬은 재시작 없이 로그 파일을 다시 열도록 시그널을 지원합니다.

  • nginx: USR1 (로그 reopen)
  • 많은 데몬: HUP (설정 reload + 로그 reopen)
# nginx 예시: 마스터 PID에 USR1
sudo kill -USR1 $(cat /run/nginx.pid)

# 또는 HUP
sudo kill -HUP <pid>

이 방식은 서비스 중단을 최소화하면서 deleted FD를 정리할 수 있다는 장점이 있습니다.

4.3 최후의 수단: 열린 FD에 직접 truncate(주의)

프로세스를 재시작할 수 없고, 당장 디스크가 꽉 차 장애가 나는 상황이면 열린 FD를 0으로 truncate 해서 공간만 회수하는 방법이 있습니다.

  1. lsof로 문제 FD의 PID를 찾고
  2. /proc/<pid>/fd/<fd>에 대해 : > 또는 truncate 수행
# 예: PID 1234가 FD 5로 큰 deleted 파일을 잡고 있을 때
sudo ls -l /proc/1234/fd/5

# 파일 내용을 0으로 만들어 블록 회수
sudo truncate -s 0 /proc/1234/fd/5
# 또는
sudo sh -c ': > /proc/1234/fd/5'

# 확인
sudo lsof -p 1234 | grep deleted

df -h

주의할 점:

  • 이건 “파일을 닫는” 게 아니라 “내용을 비우는” 것입니다.
  • 애플리케이션이 해당 FD에 계속 쓰면 다시 커질 수 있습니다.
  • 데이터 파일(로그가 아닌 DB 파일 등)에 하면 치명적입니다.

따라서 로그/임시 파일임이 확실하고, 장애 회피 목적일 때만 제한적으로 사용하세요.

5) 자주 발생하는 원인 패턴

5.1 logrotate와 애플리케이션의 궁합 문제

logrotate가 파일을 rename/remove 했는데 애플리케이션이 새 로그 파일로 재오픈하지 않으면, 프로세스는 계속 “삭제된 옛 파일”에 씁니다.

대응:

  • logrotate 설정에 postrotate에서 kill -HUP 또는 데몬별 reopen 시그널을 넣기
  • 애플리케이션 로그 라이브러리 설정에서 파일 핸들 재오픈 옵션 확인

5.2 Java/Node/Python 앱의 stdout/stderr 리다이렉션

운영 중 nohup.out 같은 파일로 stdout이 연결되어 있고, 파일을 지워도 프로세스가 계속 쓰는 케이스가 있습니다.

  • 해결: 프로세스 재시작 또는 출력 재구성(systemd journald 사용 등)

5.3 컨테이너/Kubernetes에서의 변형

컨테이너에서도 동일하게 발생합니다. 특히 다음 상황에서 자주 보입니다.

  • 노드 디스크가 가득 차서 Pod가 이상 동작
  • 컨테이너 내부에서 로그 파일을 직접 관리하며 rotate

진단 포인트:

  • 문제는 보통 노드 파일시스템(예: /var/lib/docker, /var/lib/containerd, /var/log) 에서 발생
  • 컨테이너 PID를 호스트에서 찾아 lsof로 확인해야 할 수 있음

Kubernetes에서 리소스 고갈/장애는 연쇄적으로 Pod 종료 지연, OOM, 스케줄 실패로 번질 수 있습니다. 비슷한 운영 디버깅 맥락으로는 Kubernetes OOMKilled 진단과 메모리 누수 추적 실전도 함께 보면 좋습니다.

6) 실전 트러블슈팅 절차(체크리스트)

장애 상황에서 순서대로 진행하면 안전합니다.

6.1 1단계: 디스크/파티션 확인

df -hT
mount | column -t

어느 마운트가 찼는지 확정합니다. 루트(/)인지, /var인지, 별도 데이터 볼륨인지에 따라 범위가 달라집니다.

6.2 2단계: du로 “보이는” 대용량 제거 여지 확인

sudo du -xh /var | sort -h | tail -n 50

여기서도 안 나오는데 df가 높으면 deleted-but-open 가능성이 큽니다.

6.3 3단계: lsof로 deleted-but-open 확정

sudo lsof +L1 | grep -i deleted
  • PID/COMMAND 확인
  • SIZE/OFF가 큰 항목 우선

6.4 4단계: 영향도 낮은 방식부터 적용

  1. 로그 reopen 시그널
  2. 서비스 restart
  3. (긴급 시) /proc/<pid>/fd/<fd> truncate

적용 후:

sudo lsof +L1 | grep -i deleted

df -h

7) 예방: “삭제 대신 truncate”와 로깅 아키텍처 정리

운영에서 재발을 줄이려면 다음을 권장합니다.

  • 로그 파일을 애플리케이션이 직접 들고 쓰는 구조라면, rotate 후 reopen이 확실히 되도록 구성
  • 가능하면 systemd 환경에서는 파일 로깅보다 journald로 수집하고, 외부로는 fluent-bit/vector 등으로 전달
  • 임시 파일/덤프 파일은 생성 위치를 별도 볼륨로 분리(예: /var/tmp 전용 파티션)
  • 디스크 100%는 연쇄 장애를 부르므로 알람을 “90%/95%/98%” 단계로 분리

8) 요약

  • rm 했는데 df가 안 줄면 deleted-but-open을 의심합니다.
  • sudo lsof +L1 | grep deleted가 가장 빠른 확정 방법입니다.
  • 해결은 “파일을 지우는 것”이 아니라 프로세스가 FD를 닫게 하는 것(재시작/로그 재오픈)입니다.
  • 긴급 시 /proc/<pid>/fd/<fd>에 truncate로 공간만 회수할 수 있지만, 로그 등 안전한 대상에서만 제한적으로 사용해야 합니다.

운영 환경에서 이 문제는 한 번 겪으면 다음부터는 5분 안에 해결할 수 있는 유형입니다. 다만 DB 데이터 파일, 트랜잭션 로그 같은 민감한 파일에 대해 성급히 truncate하지 않도록, lsof 출력의 경로/프로세스를 반드시 확인하고 단계적으로 조치하세요.