프롤로그 - "디스크 꽉 찼다"는데 용량은 남아있다?
No space left on device. 이 에러 메시지를 처음 마주쳤을 때, df -h로 디스크 사용률을 확인하니 47%였다. 용량은 절반밖에 안 찼는데 왜 "디스크 꽉 참"인 거지? 구글링 시작. "disk full but space available linux"라고 쳤더니 스택오버플로우 첫 답변이 떴다. "Check your inodes. Run df -i."
inode? 처음 들어본다. 파일시스템을 배울 때 "파일은 데이터 블록에 저장된다"까지만 배웠지, inode가 뭔지는 몰랐다. df -i 쳐보니 IUsed가 100%. 뭔지도 모르는 inode가 고갈된 상태였다.
로그 파일이 많아지는 환경에서는 이런 상황이 실제로 발생한다. 로깅 라이브러리가 1KB씩 파일을 쪼개서 저장하는 설정이라면, 몇 달 방치 시 inode를 모두 소진할 수 있다. 그날 밤, inode를 찾아 헤매다 깨달았다. 파일 이름은 껍데기에 불과하다는 사실을. 리눅스에서 진짜 주인공은 inode 번호다.
고민 - 파일이름이 진짜가 아니라고?
처음엔 당연하게 생각했다. hello.txt라는 파일이 있으면, 리눅스가 "hello.txt"라는 이름으로 디스크에 데이터를 저장하겠지. 그런데 아니었다.
리눅스는 파일 이름을 거의 신경 쓰지 않는다. 대신 inode number라는 숫자로 파일을 관리한다. 파일명은 사람을 위한 별명일 뿐, 운영체제가 진짜로 관리하는 건 숫자다.
이게 무슨 말인지 확인해보자. ls -i 명령어를 치면 파일 앞에 숫자가 붙어 나온다:
$ ls -i
12345678 hello.txt
12345679 world.txt
12345680 README.md
저 숫자가 바로 inode number다. 리눅스 입장에선 hello.txt가 아니라 12345678번 파일이 중요하다. 마치 사람 이름보다 주민등록번호를 더 신뢰하는 것처럼.
그럼 파일 이름은 어디에 저장되는가? 디렉토리에 저장된다. 디렉토리는 파일명 : inode번호 매핑을 담고 있는 일종의 전화번호부다. 디렉토리 자체도 파일이다. 다만 내용이 "텍스트"가 아니라 "이름-숫자 쌍의 리스트"일 뿐.
처음 이걸 알았을 때 혼란스러웠다. 파일 이름과 파일 실체가 분리돼있다는 게 직관적이지 않았다. 근데 곰곰이 생각해보니 이 구조 덕분에 hard link가 가능하다는 걸 깨달았다.
깨달음 - inode는 파일의 신분증이다
inode를 이해하는 가장 쉬운 비유는 주민등록증이다.
- 파일명: 이름 (김철수, 이영희)
- inode: 주민등록번호 (850101-1234567)
- 데이터 블록: 집 주소 (서울시 강남구...)
주민등록번호(inode)는 변하지 않지만, 이름(파일명)은 개명할 수 있다. 주민등록증(inode)에는 집 주소(데이터 블록 위치)가 적혀있다. 주민등록번호 하나에 여러 별명(hard link)을 붙일 수 있다.
이제 inode에 뭐가 들어있는지 보자. stat 명령어를 치면 inode 내용이 쫙 나온다:
$ stat hello.txt
File: hello.txt
Size: 1024 Blocks: 8 IO Block: 4096 regular file
Device: 802h/2050d Inode: 12345678 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
Access: 2025-03-15 10:30:00.000000000 +0900
Modify: 2025-03-15 10:25:00.000000000 +0900
Change: 2025-03-15 10:25:00.000000000 +0900
Birth: -
inode에 들어있는 정보:
- 파일 크기 (Size: 1024 bytes)
- 권한 (Permission: 0644 =
rw-r--r--) - 소유자 (Owner: UID 1000, GID 1000)
- 타임스탬프 (Access/Modify/Change time)
- 데이터 블록 포인터 (Blocks: 8) - "진짜 내용은 디스크 7번, 8번 블록에 있어"
- Hard link 개수 (Links: 1)
유일하게 없는 것: 파일 이름.
파일 이름은 디렉토리가 가지고 있다. 디렉토리는 아래처럼 생긴 테이블이다:
파일명 inode번호
hello.txt 12345678
world.txt 12345679
README.md 12345680
hello.txt를 goodbye.txt로 mv 명령어로 바꾸면? 디렉토리 테이블만 업데이트된다. inode는 그대로다. 데이터 블록도 그대로다. 이름만 바뀐다. 리네임은 O(1) 연산이다. 파일을 복사하지 않기 때문이다.
이제 hard link가 이해된다. ln hello.txt alias.txt 하면:
$ ls -i
12345678 hello.txt
12345678 alias.txt
같은 inode 번호다. 디렉토리에 엔트리 하나만 추가됐을 뿐, 데이터는 복사되지 않았다. 두 이름이 같은 주민등록번호를 가리킨다. stat으로 보면 Links: 2가 된다. hard link는 "같은 inode를 가리키는 또 다른 이름"이다.
inode 구조와 블록 포인터 한 걸음 더
inode가 진짜 파워풀한 건 데이터 블록 포인터 때문이다. 파일 크기가 작으면 direct pointer만 쓰지만, 큰 파일은 indirect pointer를 쓴다.
ext4 파일시스템 기준으로:
- Direct block pointers (12개): 작은 파일. 직접 데이터 블록 주소 저장.
- Indirect pointer (1개): 블록 하나를 "포인터 리스트"로 쓴다. 그 리스트가 실제 데이터 블록을 가리킴.
- Double indirect pointer (1개): 포인터의 포인터. 2단계 간접 참조.
- Triple indirect pointer (1개): 3단계 간접 참조.
이 구조 덕분에 작은 파일은 빠르게 접근하고, 큰 파일도 효율적으로 저장할 수 있다.
비유하면 이렇다:
- Direct pointer: "사과는 냉장고 2칸에 있어" (직접 위치)
- Indirect pointer: "사과 위치는 메모장에 적혀있어. 메모장은 책상 서랍에 있어" (1단계 간접)
- Double indirect pointer: "메모장 위치가 적힌 노트가 있어. 그 노트는 책장에 있어" (2단계 간접)
큰 파일일수록 탐색 비용이 증가한다. 하지만 대부분의 파일은 작기 때문에 direct pointer로 충분하다. 이게 바로 최적화다.
적용 - inode 고갈 문제 해결
앞서 얘기한 inode 고갈 상황을 재현해보면 df -i를 쳤을 때 이렇게 보인다:
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 1000000 1000000 0 100% /
inode가 100% 찼다. 용량은 47%밖에 안 찼는데 왜? 대량의 파일을 다루는 환경에서는 로그 디렉토리에 작은 파일이 수백만 개 쌓이는 일이 생긴다. 로깅 라이브러리가 1KB씩 파일을 쪼개서 저장하는 설정이라면, 몇 달간 방치되면서 inode를 모두 소진할 수 있다.
파일을 삭제하면 inode가 해제된다. 작은 파일 수백만 개를 삭제하고 나니 다시 정상화됐다:
$ find /var/log/app -type f -name "*.log" -delete
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 1000000 300000 700000 30% /
교훈:
- 디스크 모니터링할 땐
df -h와df -i둘 다 봐야 한다. - 작은 파일 대량 생성은 inode 고갈 리스크.
- 로그 파일은 rotate하거나 aggregation하자.
inode 개수는 파일시스템 생성 시 결정된다. mkfs.ext4 할 때 -i 옵션으로 "bytes-per-inode" 비율을 정할 수 있다. 기본값은 16KB당 inode 하나. 작은 파일이 많을 걸 예상하면 8KB당 inode 하나로 늘릴 수 있다.
비교 - NTFS의 MFT
Windows의 NTFS는 inode 대신 MFT (Master File Table)를 쓴다. 개념은 비슷하다. 각 파일이 MFT 엔트리를 가지고, 거기에 메타데이터와 데이터 블록 포인터가 저장된다.
차이점:
- inode: 고정 크기, 파일당 하나씩 미리 할당.
- MFT: 동적 크기, 필요할 때마다 확장 가능.
NTFS는 inode 고갈 문제가 덜하다. MFT가 디스크 용량 허용 범위 내에서 계속 커질 수 있기 때문. 하지만 MFT 단편화가 발생하면 성능 저하가 온다.
Unix/Linux의 inode 방식은 예측 가능하고 단순하다. 파일시스템 생성 시 inode 개수가 정해지니까 "최대 몇 개 파일까지 만들 수 있는지" 명확하다. 운영하기 편하다.
Inode의 물리적 구조 (Ext4 vs XFS) 뜯어보기
파일시스템마다 inode 구조가 다릅니다.
- Ext4: inode 크기가 고정됨 (기본 256 bytes). 이 안에 권한, 소유자, 타임스탬프, 그리고 데이터 블록 포인터가 빡빡하게 들어갑니다. 확장 속성(xattr)을 많이 쓰면 공간이 부족해져서 별도 블록을 할당해야 하므로 느려질 수 있습니다.
- XFS: 동적 inode 할당을 지원합니다. Ext4처럼 전체 개수가 고정되지 않고, 필요하면 데이터 블록을 inode 청크로 변환해서 쓸 수 있습니다. 그래서 "inode 고갈" 문제에서 비교적 자유롭습니다. (하지만 무한대는 아님)
대용량 스토리지 서버를 구축한다면 XFS가, 일반적인 OS 부팅용 디스크는 Ext4가 추천되는 이유 중 하나입니다.
8.9. 실제 에피소드 - "파일 지웠는데 용량이 안 줄어요!"
가장 흔한 이슈입니다. 로그 파일을 rm 했는데 df -h 용량이 그대로인 경우.
- 원인: 어떤 프로세스(예: Apache, Java)가 그 파일을 Open 하고 있기 때문입니다.
- 메커니즘:
rm은 디렉토리 엔트리만 지웁니다. inode의Link Count를 1 줄입니다. 하지만 프로세스가 잡고 있으면Reference Count가 남아있어서 inode와 데이터 블록은 삭제되지 않습니다. - 해결: 프로세스를 재시작(
systemctl restart apache2)하거나,/proc/{pid}/fd를 찾아서 kill 해야 합니다. - 팁: 로그를 비울 때는
rm하지 말고echo "" > access.log하세요. (파일 내용은 비워지고 inode는 유지됨)
8.97. 성능 튜닝 - noatime과 relatime
리눅스는 파일을 "읽기(Read)"만 해도 inode의 atime (Access Time)을 갱신합니다.
즉, 읽기 작업이 쓰기 작업(Metadata Write)을 유발합니다. 웹 서버처럼 읽기가 많은 환경에서는 이게 치명적인 성능 저하를 가져옵니다.
그래서 실제로는 /etc/fstab에 noatime 또는 relatime 옵션을 켜서 마운트합니다.
- noatime:
atime을 아예 갱신하지 않음. (가장 빠름) - relatime:
mtime(수정 시간)이 바뀌었을 때만atime을 갱신함. (호환성과 성능의 타협점, 요즘 리눅스 기본값)
8.99. 더 깊이: VFS (Virtual File System)
ln -s target link로 만드는 심볼릭 링크(Soft Link)는 하드 링크와 다릅니다. 새 inode를 가집니다.
그럼 "원본 파일의 경로"는 어디에 저장될까요?
-
Fast Symlink: 경로가 60바이트보다 짧으면, 데이터 블록을 따로 쓰지 않고 inode 내부의 포인터 저장 공간에 경로 문자열을 직접 저장합니다. 디스크 I/O가 줄어서 엄청 빠릅니다.
-
Slow Symlink: 경로가 길면, 일반 파일처럼 데이터 블록을 할당해서 거기에 경로를 적습니다.
8.995. 자주 묻는 질문 (FAQ)
Q: inode가 꽉 차면 어떻게 늘리나요?
A: 불행히도 Ext4 같은 파일시스템은 포맷할 때(mkfs.ext4) inode 개수가 정해집니다. 이미 운영 중이라면 디스크를 포맷하고 다시 마운트해야 합니다. (끔찍하죠?) 그래서 처음 구축할 때 df -i를 고려해서 넉넉하게 잡는 게 중요합니다. (XFS는 동적 할당이라 좀 낫습니다.)
Q: 디렉토리도 inode를 먹나요? A: 네, 디렉토리도 파일의 일종입니다. 폴더 하나 만들 때마다 inode 하나가 소모됩니다. 그래서 무수히 많은 빈 폴더를 만드는 것도 inode 고갈의 원인이 됩니다.
Q: 하드 링크는 inode를 먹나요?
A: 아니요! 하드 링크는 기존 inode를 가리키는 "이름표"만 하나 더 붙이는 겁니다. inode는 그대로고 Link Count만 1 증가합니다.
8.99. 더 깊이: VFS (Virtual File System)
리눅스에서는 ext4, xfs, ntfs 등 서로 다른 파일시스템을 동시에 마운트해서 쓸 수 있습니다. 어떻게 가능할까요?
커널 중간에 VFS (가상 파일 시스템) 계층이 있기 때문입니다.
cp a.txt b.txt를 실행하면, cp 프로그램은 ext4인지 뭔지 모릅니다. 그냥 VFS의 open(), read(), write() 시스템 콜을 호출할 뿐이죠.
VFS가 알아서 "아, 이건 ext4 드라이버한테 넘겨야지", "이건 네트워크 파일시스템(NFS)이네" 하고 라우팅해 줍니다.
"Everything is a file"이라는 유닉스 철학을 지탱하는 기술입니다.
마치며
inode를 알기 전엔 "파일 이름 = 파일"이라고 생각했다. 지금은 안다. 파일 이름은 디렉토리가 관리하는 별명일 뿐, 진짜 정체성은 inode 번호라는 걸.
이 지식이 실제로 유용했던 순간:
- hard link를 써서 같은 파일을 여러 경로에서 접근 (복사 없이)
- inode 고갈로 인한 디스크 에러 디버깅
mv가 왜 빠른지 (inode는 안 바뀌고 디렉토리 엔트리만 수정)rm했는데 용량이 안 줄어드는 이유 (다른 프로세스가 파일 디스크립터 열고 있으면 inode는 살아있음)
파일시스템은 보이지 않는 곳에서 묵묵히 일한다. inode는 그 중심에 있는 설계다. ls -i와 stat 명령어 한 번씩 쳐보시길. 눈에 보이던 파일명 뒤에 숨겨진 숫자가 보이기 시작한다.