운영체제 아주 쉬운 세가지 이야기 18주차(Sun 사의 네트워크 파일 시스템(NFS)

2026. 1. 6. 18:47개념 공부

Sun NFS(v2): “서버가 죽어도 다시 돌아오는” 분산 파일 시스템의 고전

분산 파일 시스템은 “여러 클라이언트가 한(또는 몇) 대의 서버 디스크를 네트워크로 공유”하는 구조입니다. 로컬 디스크를 쓰면 되는데 왜 굳이 이렇게 하냐고요? 핵심은 공유중앙 관리입니다. 어느 PC에서 파일을 수정해도 다른 PC에서 같은 파일을 같은 모습으로 볼 수 있고, 백업/관리도 서버 쪽에서 집중적으로 하면 됩니다.


1) 기본 구조: 클라이언트 파일시스템 + 파일 서버

중요한 포인트는 “앱 입장에서는 로컬 파일 시스템이랑 똑같아 보이게” 만드는 겁니다. 즉, 앱은 평소처럼 open/read/write/close를 호출하는데, 클라이언트 측 파일 시스템이 뒤에서 네트워크 메시지로 바꿔 서버와 통신합니다.

[App]
  |
  v  (POSIX system call)
[Client-side File System]
  |
  v  (protocol message)
[Network]
  |
  v
[File Server] ----> [Disks/RAID]

정리하면, 분산 파일 시스템은 결국 “클라 쪽 번역기” + “서버 쪽 처리기” 두 덩어리의 합작품입니다.


2) NFSv2가 가장 중요하게 본 목표: “서버 크래시 복구를 단순하고 빠르게”

NFSv2는 “서버가 죽으면 전체 사용자가 멈춘다”는 현실을 정면으로 봅니다. 그래서 설계 목표를 성능보다도 크래시 복구 단순화에 강하게 둡니다.

그리고 그 목표를 달성하는 핵심 키워드가 바로 Stateless(무상태) 입니다.


3) Stateless(무상태) 프로토콜: 서버는 “클라이언트 상태”를 기억하지 않는다

주니어가 여기서 가장 많이 헷갈리는 포인트가 이거예요.

  • Stateful: 서버가 “이 클라이언트가 어떤 파일을 열었고(fd), 현재 오프셋이 어디고…” 같은 상태를 기억
  • Stateless(NFSv2): 서버는 그런 걸 기억하지 않음. 요청 메시지 하나에 필요한 정보를 다 넣어서 보냄

왜 이게 중요하냐면, 서버가 크래시로 메모리 상태를 잃어도 다시 떠서 요청을 받기만 하면 되기 때문입니다. 클라이언트는 “응답이 안 오면 재전송”하면 되고, 서버는 요청만 보고 처리하면 됩니다.


4) 무상태를 가능하게 하는 핵심: File Handle(파일 핸들)

NFS에서 “파일 핸들”은 파일/디렉터리를 유일하게 식별하는 ID입니다. 대충 “서버 쪽에서 이 파일이 뭔지 다시 찾아갈 수 있는 주소표”라고 생각하면 됩니다.

파일 핸들의 구성(개념)

  • Volume ID: 어떤 파일시스템(export)인지
  • Inode number: 그 안에서 어떤 파일인지
  • Generation number: inode 재사용 시 과거 핸들이 새 파일을 잘못 가리키지 않게 방지

이 핸들이 있으니까, 서버는 “클라이언트가 열어둔 fd 같은 상태”를 기억할 필요가 없습니다. 클라이언트가 매번 “이 파일 핸들 + 이 오프셋”을 함께 보내면 되거든요.


5) NFSv2 프로토콜 감 잡기 (자주 쓰는 것만)

NFS는 POSIX API(open/read/write...) 자체를 네트워크로 보내는 게 아니라, 그걸 더 작은 RPC들로 “번역”합니다.

  • LOOKUP: (디렉터리 핸들 + 이름) → (대상 파일 핸들 + 속성)
  • READ: (파일 핸들 + offset + count) → (data + 속성)
  • WRITE: (파일 핸들 + offset + count + data) → (속성/성공여부)
  • GETATTR: (파일 핸들) → (속성; 특히 mtime이 중요)

포인트는 READ/WRITE가 반드시 offset을 포함한다는 겁니다. 이게 나중에 “재시도(retry)”를 안전하게 만드는 기반이 됩니다.


6) “open/read”가 실제로는 어떻게 동작할까? (클라가 상태를 들고 있는 구조)

앱은 read(fd, ...) 할 때 오프셋을 말하지 않죠. 그럼 누가 오프셋을 기억하냐? 클라이언트가 기억합니다.

앱: fd = open("/foo")
클라FS: LOOKUP("/", "foo") -> fileHandle(foo) 받음
클라FS: fd 테이블에 (fd -> fileHandle(foo), offset=0) 저장

앱: read(fd, buf, MAX)
클라FS: fd로 fileHandle/offset 찾고
       READ(fileHandle, offset=0, count=MAX) 전송
       성공하면 offset += bytesRead

여기서 중요한 결론: 서버는 “fd”도 모르고 “현재 offset”도 모른다. 서버가 아는 건 오직 “파일 핸들 + 오프셋 + 길이”뿐.


7) 크래시/네트워크 장애를 단순하게 처리하는 비법: Retry + Idempotency

분산 환경에서는 다음이 항상 일어납니다:

  • 요청이 유실됨
  • 서버가 다운됨
  • 서버는 처리했는데 응답이 유실됨

NFSv2 클라이언트의 기본 전략은 간단합니다. 타임아웃 나면 “같은 요청을 다시 보낸다”.

이게 가능한 이유가 Idempotency(멱등성)입니다. “같은 요청을 여러 번 해도, 결과가 한 번 한 것과 같아야” 안전하게 재시도할 수 있죠.

  • READ/LOOKUP: 당연히 멱등(읽기니까)
  • WRITE: 직관과 달리 멱등이 되게 설계됨. 왜냐면 offset+data가 요청에 들어 있어서 “같은 자리”에 “같은 내용”을 다시 쓰기 때문

시니어 관점에서 한 줄 조언: 분산 시스템에서 “재시도 가능한 API”는 그냥 편의가 아니라 생존 조건입니다.


8) 성능을 올리려면? 클라이언트 캐시. 근데… “캐시 일관성 지옥” 시작

네트워크는 로컬 메모리보다 느립니다. 그래서 NFS 클라이언트는 데이터/메타데이터 캐시를 합니다. 읽은 블록을 메모리에 저장해두면 다음 읽기는 네트워크 없이 끝나니까요. 또한 쓰기도 일단 클라이언트에 버퍼링하면 앱의 write() 지연이 줄어듭니다.

하지만 다중 클라이언트 캐시가 생기면 바로 문제가 터집니다:

  • Update visibility: C2가 파일을 바꿨는데 C3가 언제 최신을 보게 되나?
  • Stale cache: C1이 예전 버전을 캐시에 들고 있으면 계속 예전 걸 주면 어떡하나?

9) NFSv2의 “현실적 타협” 일관성: Close-to-open + GETATTR 검증(+속성 캐시)

(1) Close-to-open(Flush-on-close)

  • 한 클라이언트가 파일을 쓰고 close()할 때, dirty 데이터를 서버로 flush
  • 다른 클라이언트가 그 다음에 open하면 “대체로” 최신을 보게 됨

(2) Stale cache 방지: 사용 전에 GETATTR로 “바뀌었는지” 확인

  • 캐시된 블록을 쓰기 전에 서버에 GETATTR로 mtime 같은 속성을 확인
  • 서버 mtime이 더 최신이면 캐시 무효화하고 다시 가져옴

그런데 여기서 또 문제가 생깁니다. 모든 접근마다 GETATTR을 때리면 서버가 GETATTR 폭격을 맞아요. 그래서 Attribute cache(속성 캐시)를 둬서 “몇 초 동안은 그냥 믿고 쓰는” 방식으로 완화합니다.

정직하게 말하면, 이건 수학적으로 깔끔한 일관성 모델이라기보다 “현장에서 대충 잘 돌아가게 만든 엔지니어링 타협”에 가깝습니다. 가끔 “왜 최신이 안 보이지?” 같은 이상한 체감 버그가 나오는 이유도 이 지점입니다.


10) 서버 쪽 write buffering은 왜 조심해야 하나? (성공 응답은 ‘영구 저장’ 이후에)

여기 정말 중요합니다. NFSv2에서 클라이언트는 “응답이 없으면 재시도”합니다. 그런데 서버가 WRITE를 메모리에만 버퍼링해두고 디스크에 안 쓴 상태로 성공 응답을 주면, 그 직후 서버가 크래시했을 때 클라이언트는 “아 성공했네?”라고 믿고 다음으로 넘어가 버립니다. 그러면 파일에 중간 블록만 옛날 내용이 남는 식의 데이터 손상이 생길 수 있어요.

그래서 NFS 서버는 원칙적으로: WRITE 성공을 돌려주기 전에 stable storage(디스크/배터리백업 NVRAM 등)에 커밋해야 합니다.

이 원칙 때문에 “내구성(durability)”을 얻는 대신, 서버의 쓰기 성능이 병목이 되기 쉽고, 이를 해결하려고 배터리 백업 메모리 같은 아이디어가 등장합니다.


11) (덤) NFS가 남긴 유산: VFS/vnode

NFS가 널리 쓰이려면 OS 안에 “파일시스템 플러그인 구조”가 필요했습니다. Sun은 이를 위해 VFS / vnode 인터페이스를 도입했고, 현대 OS들도 비슷한 계층을 갖고 있습니다. 즉, NFS 그 자체보다도 “여러 파일시스템을 같은 OS에서 공존시키는 구조”가 큰 유산이 됐습니다.


마무리: 꼭 가져가야 할 체크리스트

  • Stateless: 서버는 클라 상태를 기억하지 않게 설계 → 크래시 복구 단순
  • File handle: “서버가 다시 찾아갈 수 있는 유일 식별자”
  • Retry: 네트워크/서버 실패는 기본값 → 타임아웃이면 재전송
  • Idempotency: 재시도를 가능하게 만드는 API 설계 조건
  • Client cache: 성능은 좋아지지만 일관성 문제가 생김 → close-to-open + getattr 검증(속성 캐시)
  • Server write durability: “성공 응답”은 stable storage 커밋 이후에

한 줄 결론: NFSv2는 ‘완벽한 일관성’보다 ‘장애에서 빨리 살아나는’ 시스템을 택한 설계이고, 그 선택의 결과가 stateless + idempotent + (약간은 이상한) 캐시 일관성입니다.