ファイルシステム — 名前と実体を分離した設計
한국어 원문으로 표시합니다.
한 줄 요약
유닉스 파일시스템은 파일의 실체(아이노드)와 이름(디렉터리 항목)을 분리했고, 그 결정 하나가 하드 링크, 열린 파일 삭제, 원자적 교체 같은 동작을 전부 설명한다.
왜 이게 필요했나
파일에는 두 종류의 정보가 있다. 내용과 메타데이터(크기, 권한, 소유자, 수정 시각, 데이터가 어느 블록에 있는지)다. 여기에 이름도 있다. 셋을 한 덩어리로 묶으면 단순하지만, 같은 파일을 두 이름으로 부르거나 이름을 바꾸는 일이 비싸진다.
유닉스의 선택은 메타데이터와 내용은 아이노드에 두고, 이름은 디렉터리에 둔다는 것이었다. 디렉터리는 특별한 파일이며 그 내용은 "이름 → 아이노드 번호" 쌍의 목록이다.
어떻게 동작하나
아이노드에는 파일 크기, 권한, 타임스탬프, 링크 수, 그리고 데이터 블록을 가리키는 포인터가 들어간다. 포인터 구조가 재미있다. 앞쪽 몇 개는 데이터 블록을 직접 가리키고, 그다음은 포인터들의 블록을 가리키는 단일 간접, 그다음은 이중, 삼중 간접이다. 작은 파일은 직접 포인터만으로 끝나 빠르고, 큰 파일도 표현할 수 있다. 계층이 깊어질수록 접근에 필요한 블록 읽기가 늘어나는 대가를 치른다.
이 구조에서 자연스럽게 따라오는 결과들이 있다.
하드 링크는 같은 아이노드를 가리키는 이름을 하나 더 만드는 일이다. 원본과 사본의 구분이 없고 둘은 완전히 대등하다. 아이노드의 링크 수가 하나 늘 뿐이다.
파일 삭제는 사실 이름을 지우는 일이다(그래서 시스템 콜 이름이 unlink 다). 링크 수가 0 이 되고 그 파일을 열어 둔 프로세스도 없을 때 비로소 블록이 회수된다. 그래서 로그 파일을 지웠는데 디스크 여유 공간이 늘지 않는 상황이 벌어진다. 프로세스가 아직 그 파일을 열고 있기 때문이며, 해당 프로세스를 재시작하거나 파일 디스크립터를 닫아야 공간이 돌아온다.
이름 바꾸기(rename) 는 같은 파일시스템 안에서는 디렉터리 항목만 고치므로 원자적이다. 설정 파일을 안전하게 교체하는 표준 방법이 여기서 나온다. 임시 파일에 새 내용을 쓰고, fsync 로 디스크에 내린 다음, rename 으로 갈아 끼운다. 읽는 쪽은 항상 옛 내용이나 새 내용 중 하나를 보고, 반쯤 쓰인 파일은 절대 보지 않는다.
저널링은 크래시 대비 장치다. 메타데이터 변경을 실제 위치에 쓰기 전에 저널에 먼저 기록해 두면, 중간에 전원이 나가도 재부팅 시 저널을 재생해 일관성을 복구할 수 있다. ext4 의 기본 모드는 메타데이터만 저널링하는 ordered 다. 데이터까지 저널링하면 안전하지만 모든 쓰기가 두 번 일어난다.
현장에서 만나는 모습
df 는 여유가 있다는데 쓰기가 실패하는 경우가 있다. 두 가지 중 하나다. 아이노드가 고갈됐거나(df -i 로 확인), 지운 파일을 누가 아직 열고 있어 공간이 회수되지 않았거나다. 후자는 lsof +L1 로 링크 수가 0 인 열린 파일을 찾아낼 수 있다. 로그 로테이션을 잘못 설정해 옛 파일을 지웠는데 애플리케이션이 계속 그 디스크립터에 쓰고 있는 상황이 전형적이다.
쓰기가 정말 디스크에 닿았는가
앞에서 설정 파일을 안전하게 교체하는 절차에 fsync 가 끼어 있었다. 이 한 단계가 왜 필요한지는 쓰기가 지나는 층을 알아야 이해된다.
프로그램이 write 를 호출하면 데이터는 커널의 페이지 캐시로 들어가고, 그 순간 호출은 성공으로 돌아온다. 아직 디스크에는 아무것도 안 갔다. 커널이 나중에 알아서 내려보내는데, 그 사이에 전원이 나가면 그 데이터는 없던 것이 된다. fsync 는 "지금 내려보내고 끝날 때까지 기다려라" 는 요청이다.
여기서 사람들이 자주 놓치는 것이 하나 더 있다. 파일 내용을 fsync 해도 그 파일의 이름이 안전해지는 것은 아니다. 새 파일을 만들고 쓰고 fsync 한 뒤 전원이 나가면, 내용은 남았는데 디렉터리 항목이 없어서 그 파일에 닿을 방법이 사라질 수 있다. 그래서 새로 만든 파일이나 rename 뒤에는 그 디렉터리도 함께 fsync 해야 한다. 데이터베이스와 로그 시스템이 하는 일이 정확히 이것이다.
그리고 층은 커널에서 끝나지 않는다. 디스크 자체에도 쓰기 캐시가 있어서, 장치가 "썼다" 고 답해도 아직 휘발성 버퍼에 있을 수 있다. 제대로 된 저장 장치는 캐시 비우기 명령을 지원하고 파일시스템이 그것을 보내지만, 싼 장치 중에는 그 명령을 받고 아무것도 하지 않으면서 성공이라고 답하는 것들이 있다. 그런 장치 위에서는 어떤 소프트웨어도 크래시 안전성을 보장할 수 없다.
실무의 결론은 단순하다. 잃으면 안 되는 데이터에는 fsync 를 쓰고, 그 대가로 느려지는 것을 받아들인다. 반대로 언제든 다시 만들 수 있는 캐시나 임시 파일에는 쓰지 않는다. 두 종류를 구분하지 않고 전부 fsync 하면 성능이 무너지고, 전부 생략하면 어느 날 전원이 나갔을 때 무엇이 사라졌는지도 모르게 된다.
이어지는 실습에서 할 것
여기 적힌 것을 전부 손으로 만들어 본다. 아이노드로 같은 파일인지 가려내고, 이름을 뗀 파일을 그대로 읽어 내고, 지운 채 공간을 붙잡고 있는 프로세스를 찾아내는 스크립트를 만든다. 설정 파일을 갈아 끼우는 스크립트도 만드는데, 채점기가 그 파일을 열어 둔 채로 여러분의 교체를 시켜서 읽는 쪽이 반쯤 쓰인 파일을 보는지 확인한다.