Published on

Linux 디스크 100%인데 큰 파일이 안 보일 때 lsof로 추적

Authors

서버 운영 중 가장 당황스러운 순간 중 하나가 df -h에서는 디스크가 100%로 꽉 찼는데, 정작 du로 뒤져봐도 큰 파일이 안 보이는 상황입니다. 로그도 지워봤고, 캐시도 정리했는데 여전히 No space left on device가 터진다면 **"삭제된 파일을 프로세스가 계속 열고 있는 상태"**를 의심해야 합니다.

리눅스/유닉스 파일 시스템은 파일을 삭제(unlink)해도, 그 파일을 열고 있는 프로세스의 파일 디스크립터(fd)가 남아 있으면 실제 데이터 블록이 해제되지 않습니다. 즉, 디스크는 계속 차 있는데 디렉터리 트리에서는 파일이 사라져 du로는 잡히지 않는 “유령 공간”이 생깁니다. 이때 가장 강력한 도구가 lsof입니다.

증상: df는 가득, du는 정상

대표적인 패턴은 아래와 같습니다.

  • df -h 또는 df -i에서 사용량이 비정상적으로 높음
  • du -xh / | sort -h 등으로 찾아도 합계가 맞지 않음
  • 서비스(nginx, app, db, java 등)가 로그를 계속 쓰거나 rotate 이후에도 공간이 안 돌아옴

먼저 “정말 파일 데이터 사용량 문제인지”를 확인합니다.

# 파일시스템 사용량(블록)
df -hT

# inode 고갈 여부도 체크(파일이 너무 많아도 장애가 납니다)
df -ih

# du는 디렉터리 트리 기준이라, 삭제된(open) 파일은 안 잡힙니다.
du -xh /var 2>/dev/null | sort -h | tail -n 20
  • df -ih에서 inode가 100%면 이 글의 주제(lsof)와는 다른 케이스입니다(작은 파일 폭증).
  • 블록 사용량만 높은데 du 합이 안 맞으면, 삭제된 파일을 잡고 있는 프로세스 가능성이 큽니다.

원리: 삭제된 파일이 왜 디스크를 계속 먹나?

리눅스에서 파일은 크게 두 가지로 연결됩니다.

  • 디렉터리 엔트리(경로 이름)
  • inode(실제 데이터와 메타데이터)

rm은 디렉터리 엔트리를 제거(unlink)할 뿐이고, inode의 링크 카운트가 0이 되어도 열려 있는 fd가 하나라도 남아 있으면 커널은 inode와 데이터 블록을 유지합니다. 프로세스가 파일을 닫는 순간에야 블록이 해제됩니다.

이 문제가 자주 발생하는 상황:

  • 로그 파일을 수동으로 rm 했는데 데몬이 계속 그 fd로 로그를 씀
  • logrotate 설정이 copytruncate/create/postrotate 신호 처리와 맞지 않음
  • 애플리케이션이 임시파일을 만들고 삭제했지만 핸들을 닫지 않음(버그)

핵심 해결: lsof로 (deleted) 열린 파일 찾기

1) (deleted) 파일만 필터링

lsof는 “어떤 프로세스가 어떤 파일을 열고 있는지” 보여줍니다. 삭제된 파일은 보통 경로 뒤에 (deleted)로 표시됩니다.

# 삭제되었지만 열린 파일 찾기
sudo lsof +L1

# 또는 (deleted) 문자열로 필터링(환경에 따라 표시가 다를 수 있음)
sudo lsof | grep '(deleted)'
  • +L1link count < 1(즉, 경로에서 끊긴 파일)인 항목을 보여줍니다.
  • 출력에서 특히 SIZE/OFF(크기)와 COMMAND, PID, FD를 봅니다.

예시 출력(개념):

COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF   NODE NAME
java     2314 app   12w   REG  253,0  2147483648 12345 /var/log/app.log (deleted)

여기서 중요한 건:

  • PID=2314 프로세스가
  • FD=12w(쓰기 모드)로
  • 2GB짜리 삭제된 로그를 붙잡고 있음

2) 어떤 마운트/디렉터리에서 발생했는지 좁히기

특정 파일시스템(/var, /data)만 꽉 찼다면 해당 마운트에서 열린 파일만 보도록 제한하면 빠릅니다.

# 특정 마운트포인트 기준으로 열린 파일 검색
sudo lsof +D /var/log 2>/dev/null | grep deleted

# 파일시스템 단위로 보고 싶다면(장치명 확인 후)
df -hT
# 예: /dev/nvme0n1p2 가 /var 에 마운트
sudo lsof /var | grep deleted

+D는 디렉터리 트리를 스캔하므로 큰 디렉터리에서는 느릴 수 있습니다. 급할 때는 +L1로 전체를 보고, grep/awk로 정렬하는 편이 실전에서 빠릅니다.

3) 큰 순서로 정렬해서 “범인”부터 잡기

# SIZE/OFF(7번째 컬럼)가 숫자로 나오지 않는 케이스도 있어 약간의 튜닝이 필요할 수 있습니다.
# 일반적인 REG 파일 위주로 크기 정렬 예시:
sudo lsof +L1 | awk '$5=="REG" {print $7, $1, $2, $4, $9}' | sort -n | tail -n 20

환경마다 컬럼이 달라질 수 있으니, 먼저 원본 lsof +L1를 확인하고 awk 필드를 조정하세요.

공간 회수 방법: 프로세스를 “닫게” 만들어야 한다

삭제된 열린 파일의 공간을 돌려받는 방법은 본질적으로 하나입니다.

  • 그 파일을 잡고 있는 프로세스가 fd를 닫게 만들기

방법 A) 서비스 재시작(가장 확실)

대부분의 데몬/애플리케이션은 재시작하면 fd가 정리됩니다.

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

# 컨테이너라면
kubectl rollout restart deploy/your-app
# 또는 해당 Pod 재시작

운영 환경에서는 재시작이 곧 장애/영향일 수 있으니, 트래픽 분산/롤링 재시작 전략을 고려해야 합니다. 특히 메모리 압박까지 동반되는 경우 OOM으로 이어질 수 있어, 관련해서는 리눅스 OOM Killer로 프로세스 죽음 진단·방지도 함께 참고하면 좋습니다.

방법 B) 프로세스에 로그 재오픈 시그널 보내기(무중단에 가깝게)

nginx, rsyslog 같은 일부 데몬은 로그 파일을 다시 열도록 신호를 받을 수 있습니다.

# nginx: 로그 재오픈
sudo nginx -s reopen
# 또는
sudo kill -USR1 $(cat /run/nginx.pid)

# rsyslog: 재시작/재로딩 정책은 배포판마다 다름
sudo systemctl restart rsyslog

애플리케이션이 자체적으로 SIGHUP/USR1을 처리하도록 구현된 경우도 있습니다.

방법 C) (주의) /proc/<pid>/fd로 fd를 직접 truncate

재시작이 어렵고, 해당 fd가 명확하며, 영향도를 이해하고 있을 때만 쓰는 응급처치입니다. 열린 fd는 /proc/<pid>/fd/<n>로 노출됩니다.

# 예: PID 2314, FD 12w
sudo ls -l /proc/2314/fd/12

# 파일 내용을 0으로 만들어 공간 회수(매우 주의)
sudo truncate -s 0 /proc/2314/fd/12

# 또는 셸 리다이렉션
: | sudo tee /proc/2314/fd/12 > /dev/null

주의사항:

  • 잘못된 fd를 건드리면 애플리케이션이 비정상 동작할 수 있습니다.
  • 로그 파일이라면 대개 괜찮지만, 데이터 파일/DB 파일에 하면 치명적입니다.
  • 가능하면 서비스 재시작/로그 재오픈이 정석입니다.

재발 방지: logrotate와 애플리케이션 로그 전략 점검

삭제된 열린 파일 문제의 80%는 로그 로테이션/삭제 정책에서 옵니다.

1) logrotate 설정에서 postrotate 신호 처리

nginx 예시(개념):

/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    sharedscripts
    postrotate
        [ -s /run/nginx.pid ] && kill -USR1 `cat /run/nginx.pid`
    endscript
}

핵심은 로테이트 후에 데몬이 새 파일로 다시 열도록 유도하는 것입니다.

2) copytruncate는 만능이 아니다

copytruncate는 기존 파일을 복사한 뒤 원본을 truncate하는 방식이라, 데몬 재시작 없이도 공간을 회수할 수 있습니다. 하지만 단점도 큽니다.

  • 로테이트 순간에 로그 유실/중복 가능
  • 대용량 파일이면 I/O 부담

가능하면 애플리케이션/데몬이 재오픈 시그널을 지원하도록 운영하는 편이 안정적입니다.

3) 컨테이너 환경에서는 stdout 로깅과 로그 드라이버 점검

Kubernetes에서는 파일 로그 대신 stdout/stderr로 내보내고, 노드에서 로그 수집(Fluent Bit 등)하는 패턴이 일반적입니다. 하지만 노드의 /var/log/containers가 비정상적으로 커지거나, 로그 수집기가 파일 핸들을 붙잡는 경우도 있습니다. 클러스터 운영 이슈가 겹치면 장애 형태가 복합적으로 나타나므로, 예를 들어 인그레스 레벨에서 비정상 종료가 늘어 로그가 폭증하는 케이스라면 EKS NGINX Ingress 499 폭주 원인과 해결 같은 글도 함께 보면 원인-결과를 더 빨리 연결할 수 있습니다.

실전 체크리스트(명령어 흐름)

아래 순서대로 실행하면 대부분의 케이스를 빠르게 해결할 수 있습니다.

# 1) 파일시스템 사용량 확인
sudo df -hT

# 2) du로도 큰 파일이 안 보이는지 확인
sudo du -xh /var 2>/dev/null | sort -h | tail -n 20

# 3) 삭제되었지만 열려 있는 파일 찾기
sudo lsof +L1

# 4) 큰 항목 위주로 범인 PID/FD 확인
sudo lsof +L1 | grep deleted

# 5) 가능한 조치 선택
# (A) 서비스 재시작
# sudo systemctl restart <service>
# (B) 로그 재오픈(nginx)
# sudo kill -USR1 <nginx-pid>
# (C) 응급: fd truncate
# sudo truncate -s 0 /proc/<pid>/fd/<fd>

# 6) 회수 확인
sudo df -hT

마무리

dfdu가 서로 다른 숫자를 보여줄 때는 대부분 “경로에서 사라졌지만 커널이 아직 붙잡고 있는 데이터”가 원인입니다. lsof +L1(deleted) 열린 파일을 찾고, 프로세스를 재시작하거나 로그를 재오픈시키는 것이 정석적인 해결입니다. 응급 상황에서만 /proc/<pid>/fd를 이용한 truncate를 고려하고, 이후에는 logrotate/로깅 정책을 손봐 재발을 막는 것이 운영 비용을 크게 줄입니다.