Skip to content
Published on

파일 디스크립터와 inode 완전 가이드: df와 du가 다를 때 무슨 일이 벌어지는가

공유하기
Authors

들어가며

운영을 하다 보면 상식에 어긋나 보이는 상황을 만납니다. 로그를 지웠는데 df의 사용량이 그대로입니다. 용량이 30퍼센트 남았는데 파일을 만들 수 없다고 합니다. 파일 이름을 바꿨을 뿐인데 애플리케이션이 계속 옛 파일에 씁니다. ulimit -n을 올렸는데 여전히 "Too many open files"가 납니다.

네 가지 모두 같은 두 개념을 모르면 설명되지 않습니다. inode와 파일 디스크립터입니다. 그리고 이 둘을 알면 네 가지가 전부 당연한 결과로 보입니다.

이 글은 파일시스템 이론서가 아니라 운영자를 위한 레퍼런스입니다. 개념은 필요한 만큼만 설명하고, 나머지는 전부 "이 상황에서 무엇을 확인하고 무엇을 실행하는가"에 씁니다.

기준은 리눅스 커널 5.x 이상, ext4와 XFS입니다. 파일시스템 종류에 따라 동작이 다른 부분은 그때그때 표기합니다. 특히 inode 관련 특성은 ext4와 XFS가 크게 다르며, 이 차이가 운영 중에 실제로 문제가 되는 지점이기도 합니다.

읽는 순서는 개념에서 증상으로 갑니다. 앞의 두 절에서 inode와 디렉터리 엔트리의 관계를 잡고, 그다음부터는 현장에서 만나는 상황을 하나씩 다룹니다. 급한 상황이라면 마지막의 진단 순서 요약표부터 보고 필요한 절로 올라오는 방식도 괜찮습니다.


1. 이름과 실체 — inode가 무엇인가

리눅스에서 파일은 두 부분으로 나뉩니다.

  • inode: 파일의 실체입니다. 크기, 권한, 소유자, 시각, 링크 수, 그리고 데이터 블록의 위치를 담습니다. 파일 이름은 여기 들어 있지 않습니다.
  • 디렉터리 엔트리: 이름과 inode 번호를 짝지은 항목입니다. 디렉터리는 결국 이런 짝의 목록입니다.

이 분리가 리눅스 파일 동작의 거의 모든 특이점을 설명합니다.

ls -li /var/log/messages
stat /var/log/messages

ls -istat은 inode 번호와 링크 수를 보여 줍니다. stat 출력에서 Links가 링크 수입니다.

파일을 지운다는 것은 정확히는 디렉터리 엔트리를 지우는 것입니다. 그 결과 링크 수가 1 줄어듭니다. 링크 수가 0이 되고 그 파일을 열어 둔 프로세스도 없을 때 비로소 커널이 데이터 블록을 반환합니다. 이 두 조건이 핵심입니다.

하드 링크와 심볼릭 링크의 차이도 여기서 나옵니다.

echo hello > original.txt
ln original.txt hardlink.txt
ln -s original.txt symlink.txt
ls -li original.txt hardlink.txt symlink.txt
  • 하드 링크는 같은 inode를 가리키는 또 하나의 이름입니다. inode 번호가 같고 링크 수가 2가 됩니다. 원본을 지워도 하드 링크로 접근할 수 있습니다.
  • 심볼릭 링크는 경로 문자열을 담은 별개의 파일입니다. inode 번호가 다르고, 원본을 지우면 깨집니다.

하드 링크에는 제약이 있습니다. 같은 파일시스템 안에서만 만들 수 있고, 디렉터리에는 걸 수 없습니다. inode 번호가 파일시스템 안에서만 유일하기 때문입니다. 이것이 rsync의 --link-dest 기반 세대 백업이 같은 파일시스템에서만 효율적인 이유이기도 합니다.

여기서 파생되는 결과가 하나 더 있습니다. 파일에 대한 쓰기 권한과 삭제 권한은 별개라는 것입니다. 파일을 지우는 행위는 디렉터리 엔트리를 지우는 것이므로 필요한 것은 그 파일이 아니라 그 디렉터리에 대한 쓰기 권한입니다. 읽기 전용 파일이라도 디렉터리에 쓸 수 있으면 지울 수 있습니다. 공유 디렉터리에서 남의 파일이 지워지는 사고가 여기서 나옵니다.

이 문제의 표준 해법이 sticky 비트입니다.

ls -ld /tmp
sudo chmod 1777 /srv/shared

sticky 비트가 걸린 디렉터리에서는 파일 소유자와 디렉터리 소유자, 그리고 root만 파일을 지울 수 있습니다. /tmp가 이 방식으로 보호됩니다. 여러 사용자가 함께 쓰는 디렉터리를 만들 때는 이 비트를 함께 고려하세요.


2. df와 du가 다른 이유

df는 파일시스템의 슈퍼블록이 관리하는 사용량을, du는 경로를 순회하며 만난 파일의 크기를 더합니다. 둘이 다르면 이유는 대개 셋 중 하나입니다.

이유 1 — 삭제되었지만 열려 있는 파일. 가장 흔합니다. 디렉터리 엔트리가 없으니 du는 못 찾지만, 프로세스가 열고 있으니 블록은 반환되지 않습니다.

sudo lsof -nP +L1
sudo lsof -nP +L1 | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -nr | head

+L1은 문서에 따르면 링크 수가 1보다 작은 열린 파일, 즉 이미 unlink된 파일을 나열합니다.

이유 2 — 마운트에 가려진 파일. 어떤 디렉터리에 파일을 넣은 뒤 그 위에 다른 파일시스템을 마운트하면, 아래 파일은 보이지 않지만 공간은 차지합니다.

sudo mkdir -p /mnt/check
sudo mount --bind / /mnt/check
sudo du -x -sh /mnt/check/var/log
sudo umount /mnt/check

바인드 마운트로 루트를 다시 마운트하면 가려진 파일이 보입니다. 확인 후 반드시 언마운트하세요.

이유 3 — 예약 블록. ext4는 기본적으로 일정 비율을 root 전용으로 예약합니다. df의 사용 가능 용량이 총량에서 사용량을 뺀 값보다 작은 이유입니다.

sudo tune2fs -l /dev/sda1 | grep -i 'reserved'

주의: 예약 비율을 줄이는 tune2fs -m 명령이 인터넷에 흔히 소개되지만, 예약 블록은 단편화 방지와 root의 긴급 작업 공간 확보라는 목적이 있습니다. 데이터 전용 볼륨이 아니라면 함부로 0으로 만들지 마세요.


3. 삭제했는데 공간이 안 돌아올 때

가장 자주 겪는 상황이므로 절차를 정리합니다.

1단계 — 확인.

df -h /var
sudo du -x -sh /var
sudo lsof -nP +L1 | head -20

2단계 — 어느 프로세스인지 특정.

sudo lsof -nP +L1 | awk 'NR>1 {print $2}' | sort -u | while read -r P; do
  printf '%s\t%s\n' "$P" "$(ps -o comm= -p "$P")"
done

3단계 — 해제. 순서대로 시도합니다.

# 3-1. 재오픈 신호 (가장 안전)
sudo systemctl reload rsyslog
sudo kill -USR1 "$(cat /run/nginx.pid)"

# 3-2. 파일 디스크립터를 직접 비우기 (프로세스는 유지)
sudo truncate -s 0 /proc/1234/fd/7

# 3-3. 서비스 재기동 (마지막 수단)
sudo systemctl restart myapp

파괴적 명령 경고: truncate -s 0 /proc/PID/fd/N은 그 디스크립터가 가리키는 파일의 내용을 즉시 비웁니다. 잘못된 디스크립터 번호를 지정하면 살아 있는 다른 파일을 날립니다. 실행 전에 반드시 대상 링크를 확인하세요.

sudo ls -l /proc/1234/fd/7

반대로 이 성질을 이용해 삭제된 파일을 복구할 수도 있습니다. 프로세스가 아직 열고 있다면 내용이 살아 있습니다.

sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log

삭제된 파일의 심볼릭 링크에는 경로 뒤에 삭제되었음을 나타내는 표시가 붙습니다. 이 상태에서 cp로 복사하면 내용을 되살릴 수 있습니다. 로그 파일을 실수로 지웠을 때 가장 먼저 시도할 방법입니다.


4. inode 고갈 — 용량이 남아도 파일을 못 만들 때

df -i
df -ih

IUse%가 100퍼센트면 블록이 남아 있어도 새 파일을 만들 수 없습니다. No space left on device 오류가 나는데 df -h는 여유롭다면 이것입니다.

범인을 찾습니다. 파일 개수가 많은 디렉터리를 찾는 문제입니다.

sudo find /var -xdev -type f 2>/dev/null | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head -20

-xdev는 다른 파일시스템으로 넘어가지 않게 합니다. 이미 부하가 높은 상태라면 이 스캔 자체가 부담이 되므로 범위를 좁혀 실행하세요.

전형적인 원인은 다음과 같습니다.

  • PHP 세션 파일이나 애플리케이션 캐시가 정리되지 않고 쌓임
  • 메일 큐가 밀림
  • 임시 파일을 만들고 지우지 않는 배치
  • 컨테이너 이미지 레이어의 다수 소파일

ext4에서는 inode 개수가 파일시스템 생성 시점에 고정됩니다. 나중에 늘릴 수 없으므로, 고갈되면 파일을 지우거나 파일시스템을 다시 만드는 수밖에 없습니다. 생성 시 밀도를 조정할 수 있습니다.

sudo mkfs.ext4 -i 8192 /dev/sdb1
sudo mkfs.ext4 -N 10000000 /dev/sdb1

-i는 inode 하나당 바이트 수(작을수록 inode가 많아짐), -N은 inode 개수 직접 지정입니다.

XFS는 다릅니다. inode를 동적으로 할당하므로 사실상 고갈 문제가 거의 없습니다. 다만 옛 커널이나 특정 설정에서 inode가 특정 영역에만 할당되는 제한이 있을 수 있으므로, 대량의 소파일을 다루는 볼륨이라면 XFS를 고려하는 편이 안전합니다. 정확한 옵션과 제약은 사용 중인 배포판의 mkfs 관련 man 페이지에서 확인하세요.

파괴적 명령 경고: mkfs는 대상 장치의 모든 데이터를 지웁니다. 장치 이름을 한 글자 잘못 쓰면 운영 볼륨이 사라집니다. 실행 전에 반드시 확인하세요.

lsblk -f
sudo blkid /dev/sdb1
findmnt /dev/sdb1

5. 파일 디스크립터 — 프로세스가 무엇을 열고 있는가

파일 디스크립터는 프로세스가 연 파일을 가리키는 정수입니다. 0, 1, 2는 각각 표준 입력, 표준 출력, 표준 오류로 관례가 잡혀 있습니다.

ls -l /proc/1234/fd
ls -l /proc/1234/fd | wc -l
sudo lsof -p 1234
sudo lsof -p 1234 | wc -l

/proc/PID/fd의 각 항목은 문서에 따르면 실제 파일을 가리키는 심볼릭 링크입니다. 소켓이나 파이프는 파일 경로 대신 종류와 inode 번호 형태로 표시됩니다.

lsof의 FD 열에는 숫자 외에 다음 값이 나옵니다.

  • cwd: 현재 작업 디렉터리
  • rtd: 루트 디렉터리
  • txt: 실행 파일 텍스트
  • mem: 메모리 매핑된 파일

숫자 뒤의 문자는 접근 모드입니다. r은 읽기, w는 쓰기, u는 읽기·쓰기입니다.

유용한 조회 조합입니다.

sudo lsof -nP -iTCP -sTCP:LISTEN
sudo lsof -nP -i :5432
sudo lsof -u appuser
sudo lsof +D /var/lib/myapp
sudo lsof -c nginx -a -u www-data
sudo lsof -t -c nginx
  • -a는 조건들을 AND로 결합합니다. 이것이 없으면 조건들이 OR로 처리되어 예상보다 훨씬 많은 결과가 나옵니다.
  • -t는 PID만 출력하므로 다른 명령에 넘기기 좋습니다.
  • +D는 지정한 디렉터리 아래 전체를 재귀 탐색합니다. 느리므로 범위를 좁혀 쓰세요.

6. 한도 — Too many open files 해결하기

이 오류의 원인은 네 층에 걸쳐 있습니다. 한 층만 고치면 해결되지 않는 경우가 많습니다.

층 1 — 프로세스별 소프트/하드 한도.

ulimit -Sn
ulimit -Hn
cat /proc/1234/limits | grep 'open files'

/proc/PID/limits를 보는 것이 확실합니다. 셸에서 ulimit을 바꿔도 이미 떠 있는 프로세스에는 반영되지 않기 때문입니다.

층 2 — 로그인 세션 한도. /etc/security/limits.conf 또는 /etc/security/limits.d/ 아래 파일입니다.

appuser soft nofile 65535
appuser hard nofile 65535

이 설정은 PAM을 거치는 로그인 세션에만 적용됩니다. systemd가 띄우는 서비스에는 적용되지 않습니다. 이 사실을 모르면 "설정했는데 왜 안 되지"에서 오래 헤맵니다.

층 3 — systemd 유닛 설정.

[Service]
LimitNOFILE=65535

서비스로 실행되는 프로세스의 한도는 여기서 정해집니다. 적용은 데몬 재적재와 서비스 재기동이 모두 필요합니다.

sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'

층 4 — 시스템 전체 한도.

cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
sysctl fs.file-max

file-nr은 세 값을 보여 줍니다. 할당된 디스크립터 수, 사용되지 않는 할당분, 최대치입니다. 첫 값이 세 번째에 근접하면 시스템 전체 한도에 도달한 것입니다. 현대 시스템에서 file-max 기본값은 충분히 크므로 여기서 걸리는 일은 드뭅니다.

inotify 감시 한도도 같은 계열의 문제를 만듭니다. 파일 감시를 많이 쓰는 개발 도구나 로그 수집기에서 자주 걸립니다.

cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances

값을 영구적으로 바꾸려면 sysctl 설정 파일에 넣습니다. 커널 파라미터 조정 전반은 리눅스 커널 파라미터 튜닝 가이드를 참고하세요.


7. 진단 순서 요약

증상별로 무엇부터 확인할지 정리합니다.

증상첫 명령다음 확인
지웠는데 용량이 안 줄어듦lsof +L1해당 프로세스 재오픈 신호
용량 남는데 파일 생성 실패df -i소파일 다수 디렉터리 탐색
dfdu가 크게 다름lsof +L1바인드 마운트로 가려진 파일 확인
Too many open files/proc/PID/limitssystemd LimitNOFILE 확인
이름을 바꿨는데 옛 파일에 계속 씀ls -l /proc/PID/fd재오픈 신호 또는 재기동
로그 파일을 실수로 삭제ls -l /proc/PID/fdcp /proc/PID/fd/N으로 복구
백업 용량이 예상보다 큼find -links +1하드 링크 보존 옵션 확인

마지막 줄을 조금 더 설명하면, 하드 링크가 많은 디렉터리를 하드 링크를 모르는 도구로 복사하면 각 링크가 별개 파일로 복사되어 용량이 몇 배가 됩니다.

find /backup -type f -links +1 | head
du -sh --count-links /backup/daily.0
rsync -aH /backup/ /backup2/

rsync-H 옵션이 하드 링크를 보존합니다. tar도 기본적으로 하드 링크를 인식하지만, 아카이브에 두 파일이 모두 포함되어야 관계가 유지된다는 점을 기억하세요.

파일시스템 자체의 손상이 의심되면 검사를 돌립니다.

sudo umount /dev/sdb1
sudo fsck -n /dev/sdb1
sudo xfs_repair -n /dev/sdb1

파괴적 명령 경고: fsckxfs_repair는 마운트된 파일시스템에 실행하면 손상을 일으킬 수 있습니다. 반드시 언마운트 후 실행하고, -n 옵션으로 먼저 읽기 전용 점검을 수행하세요. -n은 아무것도 고치지 않고 문제만 보고합니다.


퀴즈: 실력을 확인해 보세요

퀴즈 1: 10GB 로그 파일을 지웠는데 df의 사용량이 그대로입니다. 무슨 일이 벌어진 것인가요?

정답: 파일을 연 프로세스가 살아 있어 inode가 해제되지 않았습니다

설명: 파일 삭제는 디렉터리 엔트리를 지울 뿐입니다. 링크 수가 0이 되어도 열린 디스크립터가 남아 있으면 커널은 블록을 반환하지 않습니다.

sudo lsof -nP +L1 | head
sudo systemctl reload rsyslog

가장 안전한 해결은 프로세스에 재오픈을 알리는 것입니다. 그것이 불가능하면 디스크립터를 직접 비우는 방법도 있지만 대상 확인이 필수입니다.

sudo ls -l /proc/1234/fd/7
sudo truncate -s 0 /proc/1234/fd/7
퀴즈 2: df -h는 여유 40퍼센트인데 파일 생성이 실패합니다. 확인 명령과 대응은?

정답: df -i로 inode 사용률을 확인합니다

설명: 블록과 inode는 별개 자원입니다. 작은 파일이 수백만 개면 용량이 남아도 inode가 먼저 고갈됩니다.

df -i
sudo find /var -xdev -type f | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head

ext4는 inode 개수가 생성 시점에 고정되므로 나중에 늘릴 수 없습니다. 즉시 대응은 불필요한 소파일 정리이고, 근본 대응은 XFS 사용 검토나 inode 밀도를 높인 재생성입니다.

퀴즈 3: systemd 서비스에 limits.conf로 nofile을 65535로 설정했는데 반영되지 않습니다. 왜일까요?

정답: limits.conf는 PAM 로그인 세션에만 적용되고 systemd 서비스에는 적용되지 않습니다

설명: 서비스의 한도는 유닛 파일에서 정합니다.

[Service]
LimitNOFILE=65535

적용 후 실제 값을 확인해야 합니다.

sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'

ulimit -n을 셸에서 확인하는 것은 그 셸의 값일 뿐 서비스의 값이 아닙니다.

퀴즈 4: 애플리케이션이 쓰던 로그를 실수로 지웠습니다. 프로세스는 아직 살아 있습니다. 복구 가능할까요?

정답: 가능합니다. 열린 디스크립터를 통해 내용을 복사하면 됩니다

설명: 프로세스가 파일을 열고 있는 한 데이터 블록은 살아 있습니다.

sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log

주의할 점은 프로세스를 재기동하는 순간 복구 기회가 사라진다는 것입니다. 그래서 이 상황에서 첫 행동은 재기동이 아니라 복사여야 합니다.

퀴즈 5: 하드 링크와 심볼릭 링크의 실무적 차이를 하나만 든다면?

정답: 하드 링크는 원본을 지워도 데이터가 유지되고, 심볼릭 링크는 깨집니다

설명: 하드 링크는 같은 inode를 가리키는 또 하나의 이름입니다. 링크 수가 0이 되어야 데이터가 해제되므로, 이름 하나를 지워도 다른 이름으로 접근할 수 있습니다.

ls -li original.txt hardlink.txt symlink.txt
stat original.txt | grep Links

제약도 함께 기억하세요. 하드 링크는 같은 파일시스템 안에서만 만들 수 있고 디렉터리에는 걸 수 없습니다. 반면 심볼릭 링크는 파일시스템 경계를 넘고 디렉터리도 가리킬 수 있지만, 대상이 사라지면 깨집니다.

퀴즈 6: 세대 백업 디렉터리를 다른 서버로 복사했더니 용량이 다섯 배가 되었습니다. 원인은?

정답: 하드 링크를 보존하지 않아 각 링크가 별개 파일로 복사되었습니다

설명: --link-dest 기반 세대 백업은 변경되지 않은 파일을 하드 링크로 공유합니다. 복사 도구가 이 관계를 모르면 모든 세대의 모든 파일을 실제로 복사합니다.

find /backup -type f -links +1 | head
rsync -aH /backup/ /backup2/

-H(--hard-links)가 하드 링크를 보존합니다. -a에는 포함되지 않으므로 반드시 별도로 지정해야 합니다.


마치며

이 글의 내용을 한 문장으로 줄이면 이렇습니다. 리눅스에서 파일 이름은 실체가 아니라 실체를 가리키는 하나의 참조일 뿐입니다.

이 한 문장에서 나머지가 따라 나옵니다. 이름을 지워도 다른 참조가 남아 있으면 데이터는 살아 있습니다. 그래서 지운 파일의 용량이 안 줄어들고, 그래서 지운 파일을 복구할 수 있고, 그래서 이름을 바꿔도 프로세스는 옛 파일에 계속 씁니다.

운영 체크리스트로 옮기면 세 줄입니다. 용량 문제에서는 df -hdf -i를 함께 봅니다. 지웠는데 안 줄면 lsof +L1을 봅니다. 한도 문제는 셸이 아니라 /proc/PID/limits에서 확인합니다. 이 세 줄이 이 글의 실전 요약입니다.


참고 자료


이어서 읽기