LabHub
배우기 러닝패스 코스

Linux Fundamentals

Names, inodes and Links

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

리눅스에서 파일 이름은 실체가 아니라 실체를 가리키는 하나의 참조일 뿐이다. 이 문장 하나에서 오늘 배울 것 전부가 따라 나온다.

Flow map: 디렉터리에서 이름을 하나 떼어내는(unlink) 명령 · 이름이 하나도 남아 있지 않고 · 열어 둔 프로세스도 하나도 없을 때 · df 와 du 가 어긋나는 순간이 바로 이 상황의 신호다.

왜 이게 필요했나

운영 중인 서버에서 로그가 디스크를 다 먹었다. rm으로 큰 로그 파일을 지웠는데 df의 여유 공간이 1바이트도 늘지 않는다. 다들 한 번씩 겪고, 한 번씩 당황한다.

여기서 "rm 이 안 먹혔다"고 결론 내리면 그날 밤이 길어진다. 실제로 일어난 일은 이렇다. rm은 파일을 지우는 명령이 아니라 디렉터리에서 이름을 하나 떼어내는(unlink) 명령이다. 데이터 블록이 반환되려면 두 조건이 동시에 성립해야 한다.

  1. 그 실체를 가리키는 이름이 하나도 남아 있지 않고
  2. 그 실체를 열어 둔 프로세스도 하나도 없을 때

로그를 쓰던 애플리케이션이 아직 그 파일을 열고 있으면 두 번째 조건이 깨진다. 이름은 사라졌지만 데이터는 살아 있고, du는 경로를 걸어다니며 세기 때문에 이름 없는 그 파일을 영원히 찾지 못한다. df 와 du 가 어긋나는 순간이 바로 이 상황의 신호다.

어떻게 동작하나

세 가지를 구분해야 한다.

개념 무엇인가 이름을 담고 있나
inode 파일의 실체. 크기·권한·소유자·시각·링크 수·데이터 블록 위치 담고 있지 않다
디렉터리 엔트리 (이름, inode 번호) 짝 이름은 여기에만 있다
파일 디스크립터 프로세스가 연 파일을 가리키는 번호 담고 있지 않다

디렉터리는 결국 이 짝의 목록일 뿐이다. 그래서 다음이 전부 자연스럽게 설명된다.

디렉터리는 이름과 inode 번호의 짝 목록이다. app.log 와 app.log.1 은 같은 inode 12 를 가리키는 하드 링크라 링크 수가 2 이고, latest.log 는 경로 문자열만 담은 별개의 inode 13 이다. 프로세스의 fd 7 은 이름을 거치지 않고 inode 12 를 직접 붙잡으므로, 이름을 전부 떼어내도 그 fd 가 살아 있는 한 데이터 블록은 반환되지 않는다

현장에서 만나는 모습

첫째, 지운 로그 복구. 프로세스가 아직 열고 있다면 /proc/PID/fd 아래에 그 파일로 가는 심링크가 살아 있다. ls -l /proc/1234/fd | grep deleted로 찾아 cp /proc/1234/fd/7 /backup/recovered.log를 하면 내용을 통째로 건질 수 있다. 여기서 가장 중요한 건 순서다 - 프로세스를 재기동하는 순간 복구 기회도 함께 사라진다. 이 상황에서 첫 행동은 재기동이 아니라 복사여야 한다.

둘째, 세대 백업의 용량 폭발. rsync의 --link-dest는 바뀌지 않은 파일을 하드 링크로 연결해 세대 백업을 싸게 만든다. 그런데 그 백업 디렉터리를 하드 링크를 모르는 도구로 복사하면 용량이 몇 배로 튄다. rsync -aH처럼 링크를 보존하는 옵션을 명시해야 하고, -H-a에 포함되지 않는다.

셋째, inode 고갈. ext4는 포맷 시점에 inode 개수를 고정하고 나중에 늘릴 수 없다. 세션 파일이나 메일 큐가 작은 파일 수백만 개를 만들면 블록은 남아도 inode가 먼저 떨어져 No space left on device가 난다. 그래서 용량 알람은 df -hdf -i 둘 다 걸어야 한다.

로그가 디스크를 먹지 않게 하는 구조

앞의 사고를 겪은 뒤에 할 일은 그 파일을 복구하는 것에서 끝나지 않는다. 같은 일이 다시 생기지 않게 만드는 것이 본론이고, 여기에는 이름과 실체가 분리되어 있다는 성질이 그대로 쓰인다.

로그 회전에는 두 가지 방식이 있고, 앞의 사고는 그중 하나를 잘못 쓸 때 생긴다.

옮기고 신호를 보낸다. 파일 이름을 바꿔 두고(같은 파일시스템이므로 즉시 끝난다) 애플리케이션에 "로그 파일을 다시 열어라" 는 신호를 보낸다. 애플리케이션이 새 파일을 열면 옛 파일의 마지막 참조가 사라지고 공간이 회수된다. 신호를 보내지 않거나 애플리케이션이 그 신호를 처리하지 않으면 옛 파일에 계속 쓰게 되고, 그것이 앞에서 본 "지웠는데 안 줄어드는" 상황이다.

복사하고 비운다. 내용을 다른 파일로 복사한 뒤 원본을 길이 0으로 자른다. 파일 디스크립터가 그대로 유지되므로 애플리케이션을 건드릴 필요가 없다. 대신 복사와 자르기 사이에 쓰인 로그가 사라질 수 있고, 큰 파일이면 그 순간 디스크 사용량이 잠시 두 배가 된다.

어느 쪽이 맞는지는 애플리케이션이 정한다. 신호를 처리하는 프로그램이면 앞의 방식이 낫고, 처리하지 않으면 뒤의 방식뿐이다. 회전 설정을 쓸 때 이 판단을 안 하고 기본값으로 두는 것이 사고의 출발점이다.

그리고 요즘 컨테이너 환경에서는 애플리케이션이 아예 파일에 쓰지 않는 쪽이 더 흔하다. 표준 출력으로만 내보내고 회전과 보존은 런타임과 수집기가 맡는 구조다. 이렇게 하면 회전 신호 문제가 통째로 사라지지만, 런타임의 회전 설정을 확인하지 않으면 노드 디스크가 로그로 차는 같은 사고가 층만 바뀌어 다시 일어난다. 어느 구조든 누가 지우기로 되어 있는지를 아는 것이 핵심이다.

다음 실습에서 할 것

/root/work 아래에 작업 공간을 만들고, 하드 링크와 심볼릭 링크를 직접 걸어 inode 번호와 링크 수가 어떻게 달라지는지 눈으로 확인한다. 마지막에는 로그 디렉터리를 통째로 아카이브로 묶어 본다.