사전 NFS
프로토콜

NFS

gabury1고친 사람 github-actions[bot]

NFS 는 다른 컴퓨터의 디스크에 있는 파일을 내 컴퓨터의 파일처럼 열고 읽고 쓰게 해 주는 프로토콜입니다. 서버가 디렉터리 하나를 내놓으면 클라이언트가 그것을 자기 디렉터리 트리에 붙입니다. 그 뒤로 프로그램은 평소처럼 파일을 다룹니다. 실제 읽기와 쓰기는 네트워크 너머 서버에서 일어납니다.

쉽고 빠른 이해

NFS 는 서버 한 대의 폴더를 여러 컴퓨터가 함께 쓰게 해 줍니다. 웹 서버 세 대가 사용자가 올린 사진을 한 폴더에서 같이 읽는 식입니다.

이게 없으면 파일을 기계마다 복사해야 합니다. 복사본끼리는 금방 어긋납니다. NFS 를 쓰면 원본은 서버에 하나만 있습니다. 프로그램 코드도 고칠 필요가 없습니다.

어떻게 도는가:

  1. 서버가 내놓을 폴더를 정합니다
  2. 클라이언트가 그 폴더를 자기 경로 하나에 붙입니다
  3. 그 경로에서 파일을 읽고 쓰면 운영체제가 요청을 서버로 보내고 답을 받아 옵니다

대가도 있습니다. 파일을 다룰 때마다 네트워크를 오가므로 내 디스크보다 오래 걸립니다. 그 시간을 줄이려고 클라이언트가 내용을 잠시 들고 있어서, 다른 기계가 방금 고친 내용이 바로 안 보일 수 있습니다. 서버가 멈추면 그 폴더를 쓰던 프로그램도 함께 멈출 수 있습니다.

상세

NFS(Network File System, 네트워크 파일 시스템)는 한 컴퓨터가 다른 컴퓨터의 파일을 네트워크 너머에서 읽고 쓰게 해 주는 프로토콜입니다. 이 절은 디렉터리를 붙이는 일부터 시작합니다. 그다음 파일 하나를 읽을 때 오가는 요청을 따라갑니다. 이어서 서버가 무엇을 기억하는지, 여러 클라이언트가 한 파일을 볼 때 어떤 규칙이 있는지, 서버가 멈추면 무슨 일이 생기는지를 봅니다.

책상 서랍 하나를 옆 건물 창고와 이어 둔 것과 같습니다. 서랍을 열면 창고에 있는 물건이 보입니다. 서랍에 넣은 물건은 창고에 쌓입니다. 서랍을 쓰는 사람은 창고까지 걸어가지 않습니다.

디렉터리를 내놓고 붙이는 일

NFS 에는 두 역할이 있습니다. 파일을 가진 쪽이 서버입니다. 그 파일을 빌려 쓰는 쪽이 클라이언트입니다.

서버는 자기 디스크의 디렉터리 가운데 어느 것을 어느 클라이언트에게 내놓을지 정합니다. 이 일을 내보내기(export) 라고 부릅니다. 내보낸 디렉터리 아래의 파일만 클라이언트에게 보입니다.

유닉스 계열 운영체제는 모든 파일을 루트(/)에서 뻗어 나가는 트리 하나로 보여 줍니다. 클라이언트는 받은 디렉터리를 자기 트리의 한 경로에 붙입니다.

이 일이 마운트입니다. 붙인 경로는 마운트 지점이라고 합니다. 마운트는 트리의 가지 하나를 다른 저장소로 바꿔 끼우는 일입니다.

아래는 클라이언트에서 마운트한 뒤 파일을 읽는 모습입니다. 명령에 나오는 이름은 셋입니다.

  • files — 서버의 이름
  • /srv/share — 서버가 내보낸 디렉터리
  • /mnt/share — 클라이언트의 마운트 지점
터미널
mount -t nfs files:/srv/share /mnt/share
ls /mnt/share          # a.txt  b.txt
cat /mnt/share/a.txt   # hello

마운트한 뒤의 두 줄은 내 디스크의 파일을 다룰 때와 똑같습니다. ls 와 cat 은 자기가 읽는 파일이 다른 기계에 있다는 것을 모릅니다. 애플리케이션 코드도 마찬가지입니다. 파일 경로만 /mnt/share 아래로 두면 코드를 한 줄도 안 고치고 서버의 파일을 씁니다.

이것이 NFS 가 있는 이유입니다. 웹 서버 여러 대가 같은 파일을 봐야 할 때 파일을 기계마다 복사하면 복사본끼리 금방 어긋납니다. NFS 를 쓰면 원본은 NFS 서버에 하나만 둡니다. 웹 서버들은 저마다 클라이언트가 되어 그것을 함께 봅니다.

요청이 서버까지 가는 길

이 소절은 cat 이 파일을 읽을 때 클라이언트 안에서 요청이 어느 계층을 지나 서버에 닿는지를 봅니다.

프로그램이 파일을 열거나 읽으면 시스템 콜을 부릅니다. 시스템 콜은 프로그램이 운영체제에 일을 부탁하는 입구입니다. 그 부탁은 운영체제의 핵심부인 커널이 받습니다.

커널 안에는 가상 파일 시스템이라는 계층이 있습니다. 이 계층은 경로를 보고, 그 파일을 어느 파일 시스템이 맡는지 골라 요청을 넘깁니다. 덕분에 프로그램은 파일이 내 디스크에 있든 서버에 있든 같은 호출을 씁니다.

NFS 클라이언트는 클라이언트 기계의 커널 안에서 NFS 요청을 만드는 부분입니다. 마운트 지점 아래의 경로라면 요청은 이 NFS 클라이언트로 갑니다. NFS 클라이언트는 요청을 네트워크 메시지로 바꿔 서버에 보냅니다.

서버 기계에서 그 메시지를 받는 부분이 NFS 서버입니다. NFS 서버는 요청을 자기 로컬 파일 시스템에서 실행한 뒤 결과를 돌려보냅니다.

클라이언트와 서버 사이의 메시지는 RPC(Remote Procedure Call, 원격 프로시저 호출) 꼴입니다. RPC 는 다른 기계에 있는 함수를 부르듯 요청 하나를 보내고 답 하나를 받는 방식입니다. NFS 는 「이 이름을 찾아라」·「여기부터 읽어라」 같은 파일 작업 하나하나를 서버 쪽 함수로 정해 둡니다. 클라이언트는 그 함수를 부릅니다.

flowchart TD
    subgraph C["클라이언트"]
        A["프로그램 · 파일 읽기"] --> K["커널 · 가상 파일 시스템"]
        K --> N["NFS 클라이언트"]
    end
    subgraph S["서버"]
        D["NFS 서버"] --> F["서버의 로컬 파일 시스템"]
        F --> X["디스크"]
    end
    N -->|"RPC 요청"| D

그림에서 네트워크를 건너는 화살표는 하나뿐입니다. 프로그램부터 NFS 클라이언트까지는 전부 클라이언트 기계 안에서 일어납니다. 파일을 실제로 읽는 일은 서버의 파일 시스템이 합니다.

파일 핸들

서버는 클라이언트에게 파일을 경로로 가리키지 않고 파일 핸들로 가리킵니다. 파일 핸들은 서버가 파일마다 발급하는 식별 값입니다. 클라이언트는 이 값의 속을 모릅니다. 받아 두었다가 그 파일에 요청을 보낼 때 함께 실어 보낼 뿐입니다.

핸들에는 서버가 그 파일을 바로 찾아갈 수 있는 정보가 담깁니다. 경로는 요청마다 디렉터리를 한 칸씩 따라 내려가야 풀리지만, 핸들은 한 번에 파일에 닿습니다.

그래서 파일을 처음 열 때는 경로를 핸들로 바꾸는 일부터 합니다. 클라이언트는 마운트할 때 받은 최상위 디렉터리의 핸들에서 출발해, 경로의 이름을 한 칸씩 서버에 묻습니다. 한 칸을 물을 때마다 그 이름의 핸들이 돌아옵니다.

NFS 요청은 파일 작업 이름을 달고 있습니다. 자주 오가는 것만 추리면 다음과 같습니다. 오른쪽 칸은 같은 일을 내 디스크에서 할 때 쓰는 호출이나 명령입니다.

요청 하는 일 내 디스크에서 비슷한 것
LOOKUP 디렉터리 핸들과 이름 하나를 받아 그 이름의 핸들을 돌려준다 경로를 한 칸 따라가기
GETATTR 파일의 크기 · 수정 시각 · 권한을 돌려준다 stat
READ 핸들 · 시작 위치 · 길이를 받아 그만큼 읽어 준다 read
WRITE 핸들 · 시작 위치 · 데이터를 받아 쓴다 write
CREATE · REMOVE 파일을 만들고 지운다 open · unlink
READDIR 디렉터리 안의 이름 목록을 돌려준다 ls

표에서 GETATTR 이 돌려주는 크기·수정 시각·권한 같은 정보를 파일의 속성이라고 부릅니다. 속성은 파일 내용이 아니라 파일에 대한 정보입니다.

READ 와 WRITE 는 시작 위치를 요청에 직접 싣습니다. 내 디스크의 read 는 이와 다릅니다. 커널이 기억하는 현재 위치에서 읽고, 읽은 만큼 그 위치를 뒤로 옮깁니다. 이 차이가 다음 소절의 주제입니다.

아래는 /mnt/share/docs/a.txt 를 열고 앞부분을 읽을 때 오가는 요청입니다.

sequenceDiagram
    participant P as 프로그램
    participant C as NFS 클라이언트
    participant S as NFS 서버
    P->>C: docs/a.txt 열기
    C->>S: LOOKUP · 최상위 핸들 · docs
    S-->>C: docs 의 핸들
    C->>S: LOOKUP · docs 의 핸들 · a.txt
    S-->>C: a.txt 의 핸들
    C-->>P: 열렸다
    P->>C: 앞에서 4096 바이트 읽기
    C->>S: READ · a.txt 의 핸들 · 위치 0 · 길이 4096
    S-->>C: 데이터
    C-->>P: 데이터

경로가 두 칸이라 LOOKUP 이 두 번 나갔습니다. 그다음 READ 는 핸들과 위치, 길이를 한 번에 싣고 갑니다. 한 번 받은 핸들은 클라이언트가 기억해 두므로, 같은 파일을 다시 열 때는 LOOKUP 을 건너뛸 수 있습니다.

요청만 보고 답하는 서버

NFS 버전 2 와 3 의 서버는 어느 클라이언트가 어느 파일을 열어 두었는지 기록하지 않습니다. 이런 성질을 무상태라고 합니다. 요청 하나를 처리하는 데 필요한 정보가 전부 그 요청 안에 들어 있어서, 서버는 앞선 요청을 몰라도 답할 수 있습니다.

그래서 이 버전들에는 「파일을 열었다」·「닫았다」를 서버에 알리는 요청이 따로 없습니다. 앞의 그림에서 파일을 연 일은 LOOKUP 으로 핸들을 받아 둔 것이 전부였습니다.

이렇게 만든 까닭은 서버가 죽었다 살아날 때를 단순하게 하려는 것입니다. 서버가 다시 켜져도 잃어버릴 상태가 없습니다. 클라이언트는 답이 안 오면 같은 요청을 다시 보냅니다. 살아난 서버는 그 요청을 처음 받은 것처럼 처리합니다.

대신 같은 요청을 두 번 받아도 결과가 같아야 합니다. 이런 성질을 멱등성이라고 합니다. 위치를 싣고 가는 READ 와 WRITE 는 두 번 실행해도 같은 곳에서 같은 바이트를 읽고 씁니다. 위치를 요청마다 싣는 이유가 이것입니다.

REMOVE 는 다릅니다. 첫 요청에 파일은 지워졌지만 그 답이 오는 길에 사라졌다고 합시다. 클라이언트가 다시 보낸 요청은 「그런 파일 없음」 오류를 받습니다.

이런 일을 줄이려고 서버는 최근에 처리한 요청과 그 답을 잠시 기억해 두기도 합니다. 같은 요청이 다시 오면 앞의 답을 돌려줍니다.

이 기억은 무상태와 부딪치지 않습니다. 요청에 답하는 데 꼭 필요한 정보가 아니라 다시 보낸 요청을 걸러 내는 보조 캐시라서, 서버가 다시 켜질 때 잃어도 됩니다. 잃으면 앞의 「그런 파일 없음」 같은 오류가 가끔 다시 생길 뿐입니다.

무상태의 대가는 파일 잠금입니다. 잠금은 「누가 이 파일을 쥐고 있다」는 상태라서 무상태 서버가 맡을 수 없습니다. 그래서 버전 3 까지는 잠금을 NFS 바깥의 별도 프로토콜이 맡았습니다.

마운트를 처리하는 일도 별도 프로토콜이 맡았습니다. 이 프로토콜들은 저마다 따로 서비스로 돌았습니다. 서비스마다 포트 번호도 따로였습니다. 번호가 고정되지 않은 서비스도 있어서, 서버 앞에 방화벽을 두면 어느 포트를 열어야 할지 정하기가 어려웠습니다.

버전 4 에서 서버가 기억하게 된 것

버전 4 는 방향을 바꿨습니다. 서버가 상태를 기억합니다. 클라이언트는 파일을 열고 닫을 때 OPEN 과 CLOSE 를 보냅니다. 잠금도 NFS 요청 안으로 들어왔습니다.

서버가 상태를 기억하면 새 문제가 생깁니다. 클라이언트가 파일을 잠근 채 꺼져 버리면 그 잠금이 영영 안 풀립니다.

버전 4 는 이것을 리스로 풉니다. 리스는 서버가 클라이언트에게 상태를 정해진 시간 동안만 맡겨 두는 약속입니다. 클라이언트가 그 시간 안에 연락해 리스를 갱신하지 않으면, 서버는 그 클라이언트가 쥐고 있던 잠금을 풀어 버립니다.

버전 4 는 요청 여러 개를 한 메시지에 묶어 보낼 수도 있습니다. 이 묶음을 COMPOUND 요청이라고 부릅니다. 앞의 그림에서 LOOKUP 두 번과 READ 한 번은 따로 왕복했습니다. 버전 4 에서는 이 셋을 한 번의 왕복에 담을 수 있습니다.

요청 하나가 서버에 갔다가 답이 돌아오기까지 걸리는 시간을 왕복 시간이라고 합니다. 서버가 멀리 있을수록 왕복 시간이 길어지므로, 요청을 묶어 왕복 횟수를 줄이는 효과도 그만큼 커집니다.

두 버전의 차이를 한데 모으면 다음과 같습니다. 마지막 줄의 사용자 확인은 아래 「누가 요청했는지 믿는 방식」 소절에서 풉니다.

버전 3 버전 4
서버가 기억하는 것 없다 열린 파일 · 잠금 · 리스
잠금 NFS 바깥의 별도 프로토콜 NFS 요청 안
쓰는 포트 서비스마다 따로 2049 번 하나
한 메시지에 담는 작업 하나 여럿(COMPOUND)
사용자 확인 클라이언트가 보낸 사용자 번호를 믿는 방식이 흔하다 암호로 신원을 확인하는 인증을 반드시 지원한다

포트가 하나로 줄어든 덕분에 방화벽에서는 2049 번만 열면 됩니다.

캐시와 닫고 열기 일관성

네트워크를 오가는 일은 내 디스크를 읽는 것보다 훨씬 오래 걸립니다. 그래서 NFS 클라이언트는 읽은 데이터와 파일 속성을 자기 메모리에 캐시해 둡니다. 쓰기도 곧바로 보내지 않고 모아 두었다가 한꺼번에 보냅니다.

문제는 여러 클라이언트가 한 파일을 볼 때입니다. 클라이언트 A 가 고친 내용이 아직 A 의 메모리에만 있으면, 클라이언트 B 는 서버에서 옛 내용을 읽습니다. B 가 가진 캐시가 낡았어도 B 는 그것을 모릅니다.

NFS 클라이언트는 이것을 닫고 열기 일관성(close-to-open consistency) 이라는 규칙으로 다룹니다. 파일을 닫을 때는 모아 둔 쓰기를 전부 서버로 보냅니다. 파일을 열 때는 서버에 속성을 물어, 수정 시각이 캐시와 다르면 캐시를 버립니다.

sequenceDiagram
    participant A as 클라이언트 A
    participant S as NFS 서버
    participant B as 클라이언트 B
    Note over A: 파일을 열고 쓴다 · 쓰기가 메모리에 모인다
    A->>S: 닫을 때 모아 둔 쓰기를 WRITE 로 보낸다
    B->>S: 열 때 GETATTR 로 속성을 묻는다
    S-->>B: 수정 시각이 바뀌었다
    Note over B: 캐시를 버린다
    B->>S: READ
    S-->>B: A 가 쓴 새 내용

A 가 닫은 뒤에 B 가 열면 B 는 A 가 쓴 내용을 봅니다. 규칙이 약속하는 것은 거기까지입니다. 둘이 파일을 동시에 열어 둔 채로 쓰고 읽으면, 서로의 변경이 언제 보일지는 정해져 있지 않습니다.

내 디스크에서는 한 프로세스가 쓴 내용을 다른 프로세스가 바로 읽습니다. 같은 프로그램을 NFS 위로 옮기면 이 전제가 깨질 수 있습니다. 여러 기계가 한 로그 파일에 동시에 덧붙여 쓰는 경우가 대표적입니다.

서버가 답하지 않을 때

서버가 꺼지거나 네트워크가 끊기면 NFS 요청에 답이 안 옵니다. 이때 클라이언트가 어떻게 할지는 마운트할 때 고릅니다. 기다리는 한도가 타임아웃입니다. 다시 보내는 일은 재시도입니다.

마운트 방식 답이 안 오면 얻는 것 잃는 것
hard 서버가 돌아올 때까지 끝없이 다시 보낸다 쓰던 데이터가 사라지지 않는다 그 파일을 쓰던 프로그램이 멈춘 채 기다린다
soft 몇 번 다시 보내다 포기하고 오류를 돌려준다 프로그램이 멈추지 않는다 프로그램이 오류를 안 챙기면 쓰기가 조용히 사라진다

hard 로 마운트한 디렉터리에서 서버가 사라지면, 그 디렉터리에 ls 를 친 셸까지 멈춥니다. 대신 서버가 돌아오면 멈췄던 프로그램이 이어서 돕니다.

soft 는 프로그램을 살리는 대신 오류를 프로그램에 넘깁니다. 쓰기 오류를 확인하지 않는 프로그램은 데이터가 안 써졌다는 것을 모른 채 지나갑니다.

실패가 하나 더 있습니다. 클라이언트가 핸들을 쥐고 있는 동안 다른 클라이언트가 그 파일을 지우면, 핸들이 가리키던 파일이 서버에서 사라집니다. 그 핸들로 요청을 보내면 서버는 오래된 파일 핸들(stale file handle) 오류를 돌려줍니다.

무상태 서버라서 생기는 일입니다. 서버는 누가 그 파일을 열어 두었는지 모르므로, 지우는 요청이 오면 그대로 지웁니다.

누가 요청했는지 믿는 방식

유닉스 계열에서 사용자와 그룹은 이름이 아니라 번호로 구분합니다. 사용자 번호가 UID(User ID, 사용자 식별 번호)입니다. 그룹 번호는 GID(Group ID, 그룹 식별 번호)입니다.

버전 3 까지 흔히 쓰는 방식에서 클라이언트는 요청마다 이 두 번호를 싣습니다. 서버는 그 번호를 믿고 파일 권한을 따집니다. 따로 신원을 확인하지 않습니다.

그래서 두 가지가 따라옵니다. 첫째, 클라이언트와 서버가 같은 사람에게 같은 번호를 줘야 합니다. 클라이언트에서 1000 번인 사람이 서버에서는 다른 사람의 번호일 수 있습니다.

둘째, 클라이언트의 관리자는 아무 번호로나 요청을 보낼 수 있습니다. 유닉스 계열의 관리자 계정은 root 입니다. 서버는 적어도 root 의 요청만은 권한 없는 사용자의 요청으로 바꿔 받습니다. 이것을 root squash라고 부릅니다.

버전 4 는 암호로 신원을 확인하는 인증을 반드시 지원하게 했습니다. 대표가 Kerberos입니다. Kerberos 에서는 믿을 수 있는 서버 하나가 사용자의 신원을 보증하는 티켓을 발급합니다. 파일 서버는 그 티켓을 보고 사용자를 확인합니다.

NFS 가 맞는 작업과 안 맞는 작업

앞의 소절들이 보인 성질을 쓰임새로 옮기면 다음과 같습니다.

상황 NFS 와의 궁합 까닭
웹 서버 여러 대가 같은 파일을 읽는다 맞는다 원본 하나를 여럿이 보고 코드를 안 고친다
사용자 홈 디렉터리를 여러 기계에서 함께 쓴다 맞는다 어느 기계에 들어가도 같은 파일이 보인다
여러 곳에서 한 파일에 동시에 쓴다 안 맞는다 캐시 탓에 서로의 변경이 늦게 보인다
작은 파일 수만 개를 훑는다 오래 걸린다 파일마다 LOOKUP · GETATTR 왕복이 붙는다
인터넷 너머 먼 서버 안 맞는다 왕복 시간이 길고, 연결이 끊기면 프로그램이 멈춘다
데이터베이스의 데이터 파일 확인이 먼저다 잠금과 쓰기 순서에 기대는 프로그램이라 NFS 위에서 잠금이 어떻게 도는지부터 봐야 한다

코드를 고칠 수 있고 파일 경로로 다룰 필요가 없다면 오브젝트 스토리지가 대안입니다. 파일 작업 대신 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 요청으로 데이터 덩어리를 통째로 올리고 받습니다.

윈도 환경에서 폴더를 나눠 쓸 때는 NFS 와 같은 일을 SMB(Server Message Block, 서버 메시지 블록)가 합니다.

관련 항목

NFS 가 올라타는 아래 계층의 프로토콜

RPC · XDR · TCP · UDP · 포트 번호 · portmapper

NFS 가 파일을 가리키고 붙이는 방식

파일 핸들 · 마운트 · 마운트 지점 · 가상 파일 시스템 · 디렉터리 · 아이노드

NFS 가 기대거나 지키는 성질

무상태 · 멱등성 · 닫고 열기 일관성 · 캐시 일관성 · 리스 · 파일 잠금

NFS 에서 자주 나는 장애와 그 설정

오래된 파일 핸들 · hard 마운트 · soft 마운트 · 타임아웃 · 재시도 · 언인터럽터블 슬립

NFS 요청의 사용자를 확인하는 장치

UID · GID · root squash · Kerberos · RPCSEC_GSS · 인증 · 권한

NFS 의 버전과 확장

NFSv2 · NFSv3 · NFSv4 · NFSv4.1 · pNFS

NFS 를 정의하는 표준 문서

RFC 1813 · RFC 7530 · RFC 8881 · RFC 5531

NFS 와 같은 일을 두고 겨루는 저장 방식

SMB · 오브젝트 스토리지 · Amazon S3 · 분산 파일 시스템 · CephFS · HDFS · iSCSI

NFS 를 구현하거나 NFS 로 붙는 제품

NFS-Ganesha · Amazon EFS · Azure Files · Filestore · NetApp ONTAP

NFS 를 볼륨으로 붙여 쓰는 컨테이너 환경

Kubernetes · 퍼시스턴트 볼륨 · Docker · 볼륨

NFS 가 속하는 상위 분류

네트워크 파일 시스템 · 파일 시스템 · 프로토콜 · 응용 계층 · 네트워크 · 스토리지

다른 이름: Network File System