NFS 심화 (Network File System)
Linux 커널 NFS 심화: NFSv3/NFSv4/NFSv4.1/pNFS, RPC/XDR, 서버(knfsd)/클라이언트, 위임, 잠금, 캐싱, Kerberos 인증 종합 가이드.
NFS 개요
NFS(Network File System)는 Sun Microsystems가 1984년에 개발한 분산 파일시스템 프로토콜로, 네트워크를 통해 원격 파일시스템을 마치 로컬 파일시스템처럼 접근할 수 있게 합니다. 리눅스 커널은 NFS 서버(knfsd)와 클라이언트를 모두 커널 내부에 구현하여 높은 성능을 제공합니다.
NFS 버전 발전
| 버전 | 연도 | RFC | 핵심 특징 | 상태 모델 |
|---|---|---|---|---|
| NFSv2 | 1989 | RFC 1094 | 최초 공식 표준, 32비트 파일 오프셋, UDP 전용 | Stateless |
| NFSv3 | 1995 | RFC 1813 | 64비트 오프셋, 비동기 쓰기(WRITE UNSTABLE), TCP 지원, readdirplus | Stateless |
| NFSv4.0 | 2003 | RFC 7530 | Stateful, compound ops, delegation, ACL, 방화벽 친화(단일 포트 2049) | Stateful |
| NFSv4.1 | 2010 | RFC 8881 | 세션, pNFS(병렬 NFS), 트렁킹, 디렉토리 위임 | Stateful |
| NFSv4.2 | 2016 | RFC 7862 | 서버 사이드 복사, 희소(sparse) 파일, 레이블 NFS(SELinux) | Stateful |
NFS 프로토콜 - RPC/XDR
NFS는 ONC RPC(Open Network Computing Remote Procedure Call) 위에 구축됩니다. 클라이언트가 원격 프로시저를 호출하면, 인자와 반환값은 XDR(eXternal Data Representation) 형식으로 직렬화되어 네트워크를 통해 전달됩니다.
ONC RPC 구조
리눅스 커널의 SUNRPC 구현은 net/sunrpc/ 디렉토리에 위치합니다. 핵심 구조체는 다음과 같습니다.
/* net/sunrpc/clnt.c - RPC 클라이언트 핵심 구조체 */
struct rpc_clnt {
struct rpc_xprt __rcu *cl_xprt; /* 전송 계층 */
const struct rpc_procinfo *cl_procinfo; /* 프로시저 정보 테이블 */
u32 cl_prog; /* RPC 프로그램 번호 */
u32 cl_vers; /* RPC 프로그램 버전 */
u32 cl_maxproc; /* 최대 프로시저 수 */
struct rpc_auth *cl_auth; /* 인증 핸들 */
struct rpc_stat *cl_stats; /* RPC 통계 */
struct rpc_iostats *cl_metrics; /* I/O 성능 메트릭 */
/* ... */
};
XDR 인코딩
XDR은 플랫폼 독립적 데이터 직렬화 형식입니다. 모든 데이터는 4바이트 경계로 정렬되며, 빅 엔디안(big-endian)을 사용합니다.
/* XDR 기본 타입 인코딩/디코딩 */
static inline __be32 *xdr_encode_hyper(__be32 *p, __u64 val)
{
put_unaligned_be64(val, p);
return p + 2; /* 64비트 = 2 x 32비트 워드 */
}
static inline __be32 *xdr_encode_opaque_fixed(__be32 *p,
const void *ptr, unsigned int nbytes)
{
memcpy(p, ptr, nbytes);
if (nbytes & 3) /* 4바이트 패딩 */
memset((char *)p + nbytes, 0, 4 - (nbytes & 3));
return p + XDR_QUADLEN(nbytes);
}
portmap / rpcbind
NFSv2/v3에서 클라이언트는 여러 포트를 사용합니다. rpcbind(포트 111)를 통해 NFS(프로그램 번호 100003), mountd(100005), NLM(100021), NSM(100024) 서비스의 포트를 조회합니다.
| RPC 프로그램 | 번호 | 기능 | NFSv4 필요 여부 |
|---|---|---|---|
| portmapper/rpcbind | 100000 | RPC 포트 매핑 | 불필요 |
| nfs | 100003 | NFS 프로토콜 | 포트 2049 고정 |
| mountd | 100005 | 마운트 프로토콜 | 불필요 (pseudo-fs) |
| nlockmgr (NLM) | 100021 | 파일 잠금 | 불필요 (프로토콜 내장) |
| status (NSM) | 100024 | 상태 모니터링 | 불필요 |
sunrpc 전송 계층 (xprt)
SUNRPC는 전송 계층을 추상화하여 TCP, UDP, RDMA를 동일한 인터페이스로 사용합니다. rpc_xprt 구조체가 전송 독립적 인터페이스를 제공하며, 각 전송 방식은 rpc_xprt_ops 콜백을 구현합니다.
/* include/linux/sunrpc/xprt.h - 전송 계층 추상화 */
struct rpc_xprt {
const struct rpc_xprt_ops *ops; /* 전송 방식별 콜백 */
const struct rpc_timeout *timeout; /* 타임아웃 정책 */
struct rpc_wait_queue sending; /* 송신 대기 큐 */
struct rpc_wait_queue pending; /* 응답 대기 큐 */
struct rpc_wait_queue backlog; /* 슬롯 대기 큐 */
unsigned int max_reqs; /* 최대 동시 요청 (슬롯 수) */
unsigned int min_reqs; /* 최소 동시 요청 */
unsigned int num_reqs; /* 현재 활성 요청 */
unsigned long bind_timeout;
unsigned long reestablish_timeout;
struct sockaddr_storage addr; /* 서버 주소 */
size_t addrlen;
int prot; /* IPPROTO_TCP/UDP */
};
/* 전송 방식별 연산 테이블 */
struct rpc_xprt_ops {
void (*set_buffer_size)(struct rpc_xprt *, size_t, size_t);
int (*reserve_xprt)(struct rpc_xprt *, struct rpc_task *);
void (*release_xprt)(struct rpc_xprt *, struct rpc_task *);
void (*connect)(struct rpc_xprt *, struct rpc_task *);
int (*send_request)(struct rpc_rqst *);
void (*close)(struct rpc_xprt *);
void (*destroy)(struct rpc_xprt *);
void (*inject_disconnect)(struct rpc_xprt *);
};
| 전송 모듈 | 소스 | 프로토콜 | 특징 |
|---|---|---|---|
| xprtsock | net/sunrpc/xprtsock.c | TCP/UDP | 기본 전송, 소켓 기반 |
| xprtrdma | net/sunrpc/xprtrdma/ | RDMA | 제로카피, 커널 바이패스 |
| xprtmultipath | net/sunrpc/xprtmultipath.c | 다중 경로 | nconnect, 트렁킹용 다중 xprt 관리 |
RPC 태스크 스케줄링
각 NFS RPC 호출은 rpc_task로 표현되며, 유한 상태 머신(FSM)으로 실행됩니다. RPC 태스크는 비동기적으로 처리되어 여러 요청을 동시에 진행할 수 있습니다.
/* include/linux/sunrpc/sched.h - RPC 태스크 */
struct rpc_task {
struct rpc_clnt *tk_client; /* RPC 클라이언트 */
struct rpc_xprt *tk_xprt; /* 전송 핸들 */
struct rpc_rqst *tk_rqstp; /* 요청/응답 버퍼 */
/* 상태 머신 콜백 */
const struct rpc_call_ops *tk_ops;
void *tk_calldata; /* 콜백 인자 */
rpc_action tk_action; /* 현재 상태 함수 */
unsigned short tk_flags; /* RPC_TASK_* 플래그 */
unsigned short tk_timeouts; /* 재시도 횟수 */
int tk_status; /* 결과 상태 코드 */
};
/* RPC 태스크 상태 머신 흐름:
* call_start → call_reserve → call_refreshauth → call_allocate
* → call_bind → call_connect → call_transmit → call_status
* → call_decode → 완료 또는 재시도 */
NFSv3 프로토콜
NFSv3 프로시저
NFSv3는 22개의 프로시저를 정의합니다. 각 프로시저는 독립적인 RPC 호출이며, 서버는 요청 간 상태를 유지하지 않습니다(stateless).
/* NFSv3 주요 프로시저 (RFC 1813) */
enum nfs3_procedure {
NFS3PROC_NULL = 0, /* ping */
NFS3PROC_GETATTR = 1, /* 파일 속성 조회 */
NFS3PROC_SETATTR = 2, /* 파일 속성 설정 */
NFS3PROC_LOOKUP = 3, /* 이름으로 파일핸들 조회 */
NFS3PROC_ACCESS = 4, /* 접근 권한 확인 */
NFS3PROC_READLINK = 5, /* 심볼릭 링크 읽기 */
NFS3PROC_READ = 6, /* 파일 데이터 읽기 */
NFS3PROC_WRITE = 7, /* 파일 데이터 쓰기 */
NFS3PROC_CREATE = 8, /* 파일 생성 */
NFS3PROC_MKDIR = 9, /* 디렉토리 생성 */
NFS3PROC_REMOVE = 12, /* 파일 삭제 */
NFS3PROC_READDIR = 16, /* 디렉토리 목록 읽기 */
NFS3PROC_READDIRPLUS= 17, /* 디렉토리 + 속성 읽기 */
NFS3PROC_FSSTAT = 18, /* 파일시스템 통계 */
NFS3PROC_FSINFO = 19, /* 파일시스템 정보 */
NFS3PROC_COMMIT = 21, /* 비동기 쓰기 커밋 */
};
마운트 프로토콜
NFSv3 클라이언트는 mountd 데몬에 MOUNT 요청을 보내 초기 파일핸들(root filehandle)을 얻습니다. 이후 모든 NFS 요청은 이 파일핸들 기반으로 동작합니다.
# NFSv3 마운트 예시
mount -t nfs -o vers=3,tcp,rsize=65536,wsize=65536 \
server:/export/data /mnt/nfs
# rpcinfo로 NFS 관련 서비스 확인
rpcinfo -p server
# program vers proto port service
# 100000 4 tcp 111 portmapper
# 100003 3 tcp 2049 nfs
# 100005 3 tcp 20048 mountd
# 100021 4 tcp 44629 nlockmgr
NLM (Network Lock Manager)
NFSv3의 파일 잠금은 NLM(Network Lock Manager) 프로토콜로 처리됩니다. NLM은 stateful 프로토콜이므로 NSM(Network Status Monitor)과 함께 동작하여 서버/클라이언트 재시작 시 잠금을 복구합니다.
/* fs/lockd/svc.c - NLM 서비스 커널 구현 */
static const struct svc_version nlmsvc_version4 = {
.vs_vers = 4,
.vs_nproc = 24,
.vs_proc = nlmsvc_procedures4,
.vs_dispatch = nlmsvc_dispatch,
.vs_xdrsize = NLMSVC_XDRSIZE,
};
/* NLM 잠금 요청 처리 */
static __be32
nlmsvc_proc_lock(struct svc_rqst *rqstp)
{
struct nlm_args *argp = rqstp->rq_argp;
struct nlm_host *host;
struct nlm_file *file;
/* 파일 핸들 → 잠금 설정 */
return cast_status(nlmsvc_lock(rqstp, file,
host, &argp->lock, argp->block,
&argp->cookie, argp->reclaim));
}
NFSv4 프로토콜
Compound Operations
NFSv4의 가장 큰 프로토콜 변화는 compound 연산입니다. 여러 개의 작은 연산(operation)을 하나의 RPC 요청에 묶어 보낼 수 있어 네트워크 왕복 횟수를 크게 줄입니다.
/* NFSv4 compound 연산 예: 파일 읽기 */
/* 하나의 RPC 요청에 4개 연산을 묶음 */
COMPOUND {
SEQUENCE(session, slot, seq) /* v4.1: 세션 시퀀싱 */
PUTFH(parent_filehandle) /* 현재 파일핸들 설정 */
LOOKUP("filename") /* 이름 → 파일핸들 */
GETATTR(size, mtime, mode) /* 속성 조회 */
READ(offset=0, count=65536) /* 데이터 읽기 */
}
/* NFSv3였다면 MOUNT + LOOKUP + GETATTR + READ = 4번 왕복 */
커널에서 compound 연산 처리는 fs/nfsd/nfs4proc.c의 nfsd4_proc_compound()에서 수행됩니다.
/* fs/nfsd/nfs4proc.c */
static __be32
nfsd4_proc_compound(struct svc_rqst *rqstp)
{
struct nfsd4_compoundargs *args = rqstp->rq_argp;
struct nfsd4_compoundres *resp = rqstp->rq_resp;
struct nfsd4_op *op;
while (resp->opcnt < args->opcnt) {
op = &args->ops[resp->opcnt++];
/* 각 연산을 순차적으로 실행 */
op->status = nfsd4_enc_ops[op->opnum](rqstp, &xdr, op);
if (op->status)
break; /* 에러 시 나머지 연산 중단 */
}
return rpc_success;
}
OPEN/CLOSE 상태 관리
NFSv4에서 파일 열기는 OPEN 연산으로 수행되며, 서버에 상태(stateid)를 생성합니다. 이 stateid는 이후 READ/WRITE/LOCK 연산에 사용됩니다.
/* fs/nfsd/nfs4state.c - NFSv4 open 상태 구조체 */
struct nfs4_ol_stateid {
struct nfs4_stid st_stid;
struct list_head st_locks; /* 이 open에 연결된 잠금 목록 */
struct nfs4_openowner *st_openowner;
struct nfs4_file *st_stateowner;
unsigned char st_access_bmap; /* READ/WRITE 접근 비트맵 */
unsigned char st_deny_bmap; /* 거부 공유 비트맵 */
};
/* stateid: 128비트 = 4바이트 seqid + 12바이트 other */
typedef struct {
u32 si_generation; /* 시퀀스 번호 */
opaque_t si_opaque; /* 서버 고유 식별자 */
} stateid4;
위임 (Delegation)
NFSv4 위임(delegation)은 서버가 클라이언트에게 파일에 대한 특정 권한을 위임하는 메커니즘입니다. 클라이언트가 위임을 받으면, 서버에 매번 확인하지 않고 로컬에서 캐시된 데이터를 사용할 수 있습니다.
| 위임 유형 | 권한 | 설명 |
|---|---|---|
| OPEN_DELEGATE_READ | 읽기 | 다른 클라이언트가 쓰기를 시도하기 전까지 로컬 캐시 유효 |
| OPEN_DELEGATE_WRITE | 읽기+쓰기 | 독점적 쓰기 권한, 잠금 없이 쓰기 가능 |
/* fs/nfs/delegation.c - 클라이언트 측 위임 처리 */
struct nfs_delegation {
struct list_head super_list;
const struct cred *cred;
struct inode *inode;
nfs4_stateid stateid;
fmode_t type; /* READ or WRITE */
unsigned long flags;
loff_t maxsize;
__u64 change_attr;
struct rcu_head rcu;
};
/* 위임 반환: 서버가 CB_RECALL 콜백 시 호출 */
int nfs4_proc_delegreturn(struct inode *inode,
const struct cred *cred,
const nfs4_stateid *stateid, int issync)
{
/* DELEGRETURN compound 전송 */
struct nfs4_delegreturnargs args = {
.fhandle = NFS_FH(inode),
.stateid = stateid,
};
return nfs4_call_sync(clp->cl_rpcclient, &msg, &args, &res, 0);
}
NFSv4.1 / pNFS
세션 (Sessions)
NFSv4.1은 세션 개념을 도입하여 exactly-once 시맨틱(EOS)을 보장합니다. 각 세션은 여러 슬롯을 가진 채널로 구성되며, 클라이언트는 슬롯별로 시퀀스 번호를 관리합니다.
/* include/linux/nfs4.h - NFSv4.1 세션 구조 */
struct nfs4_session {
struct nfs4_sessionid sess_id; /* 16바이트 세션 ID */
u32 flags;
struct nfs4_channel_attrs fc_attrs; /* 전방 채널 속성 */
struct nfs4_channel_attrs bc_attrs; /* 후방 채널 (콜백) */
struct nfs4_slot_table fc_slot_table; /* 전방 슬롯 테이블 */
struct nfs4_slot_table bc_slot_table; /* 후방 슬롯 테이블 */
struct nfs_client *clp;
};
/* SEQUENCE 연산: 모든 compound 요청의 첫 번째 연산 */
/* 슬롯 + 시퀀스 번호로 재전송 감지 (exactly-once) */
struct nfs4_slot {
struct nfs4_slot_table *table;
u32 sl_seqid; /* 시퀀스 번호 */
u32 sl_slot; /* 슬롯 인덱스 */
unsigned long sl_flags; /* NFS4_SLOT_* */
struct nfs4_slot_seqid_failed *sl_lru;
};
pNFS (Parallel NFS)
pNFS는 NFSv4.1의 핵심 확장으로, 데이터 접근을 메타데이터 서버(MDS)와 데이터 서버(DS)로 분리하여 병렬 I/O를 가능하게 합니다. 클라이언트는 MDS에서 레이아웃(layout)을 받아 DS에 직접 데이터를 읽고 쓸 수 있습니다.
레이아웃 유형 (Layout Types)
| 레이아웃 | 커널 모듈 | 데이터 접근 | 설명 |
|---|---|---|---|
| Files Layout | nfs_layout_nfsv41_files | NFSv4.1 DS | NFS 프로토콜로 DS 접근, 가장 일반적 |
| Blocks Layout | blocklayoutdriver | SAN (iSCSI/FC) | 블록 디바이스 직접 접근 |
| Objects Layout | objlayoutdriver | OSD | 객체 스토리지 접근 (사용 감소) |
| FlexFiles Layout | nfs_layout_flexfiles | NFSv3/v4 DS | 미러링/스트라이핑, 유연한 DS 프로토콜 |
| SCSI Layout | blocklayoutdriver | SCSI PR | SCSI Persistent Reservation 기반 |
/* fs/nfs/pnfs.h - pNFS 레이아웃 드라이버 인터페이스 */
struct pnfs_layoutdriver_type {
struct list_head pnfs_tblid;
u32 id; /* LAYOUT4_* 유형 */
const char *name;
/* 레이아웃 획득/반환 */
struct pnfs_layout_segment *
(*alloc_lseg)(struct pnfs_layout_hdr *layoutid,
struct nfs4_layoutget_res *lgr, gfp_t gfp);
void (*free_lseg)(struct pnfs_layout_segment *lseg);
/* 데이터 I/O */
enum pnfs_try_status
(*read_pagelist)(struct nfs_pgio_header *);
enum pnfs_try_status
(*write_pagelist)(struct nfs_pgio_header *, int sync);
enum pnfs_try_status
(*commit_pagelist)(struct inode *,
struct list_head *, int, struct nfs_commit_info *);
};
NFSv4.2 프로토콜
NFSv4.2(RFC 7862)는 NFSv4.1의 마이너 확장으로, 서버 사이드 복사, 희소 파일, 레이블 NFS 등 현대적 스토리지 기능을 추가합니다. 리눅스 커널은 4.x 시리즈부터 점진적으로 NFSv4.2 기능을 구현했습니다.
서버 사이드 복사 (Server-Side Copy)
NFSv4.2의 가장 주목할 만한 기능은 서버 사이드 복사입니다. 클라이언트가 copy_file_range() 시스템 콜을 호출하면, 데이터가 클라이언트를 경유하지 않고 서버 내부(intra-server) 또는 서버 간(inter-server)에서 직접 복사됩니다.
/* fs/nfs/nfs42proc.c - NFSv4.2 서버 사이드 복사 */
static ssize_t _nfs42_proc_copy(
struct file *src, struct nfs_lock_context *src_lock,
struct file *dst, struct nfs_lock_context *dst_lock,
struct nfs42_copy_args *args,
struct nfs42_copy_res *res)
{
/* COPY compound 구성:
* SEQUENCE + PUTFH(src) + SAVEFH + PUTFH(dst) + COPY
*
* 동기(synchronous) 복사: 작은 파일
* 비동기(asynchronous) 복사: 큰 파일
* → CB_OFFLOAD 콜백으로 완료 통보 */
args->cp_src = 1; /* intra-server copy */
args->sync = nss->sync;
struct rpc_message msg = {
.rpc_proc = &nfs4_procedures[NFSPROC4_CLNT_COPY],
.rpc_argp = args,
.rpc_resp = res,
};
return nfs4_call_sync(server->client, &msg, &args->seq_args,
&res->seq_res, 0);
}
/* copy_file_range() → NFS COPY 연산 매핑 */
static ssize_t nfs4_copy_file_range(
struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
size_t count, unsigned int flags)
{
/* 같은 서버 → intra-server copy (COPY) */
/* 다른 서버 → inter-server copy (COPY_NOTIFY + COPY) */
if (!nfs42_ssc_enabled(server))
return -EOPNOTSUPP;
return nfs42_proc_copy(file_in, pos_in,
file_out, pos_out, count,
nss, nss_count, sync);
}
희소 파일 (Sparse File) 지원
NFSv4.2는 SEEK, ALLOCATE, DEALLOCATE 연산을 도입하여 희소 파일을 효율적으로 처리합니다. lseek(SEEK_HOLE/SEEK_DATA)가 서버에서 직접 처리되어 클라이언트가 빈 영역을 건너뛸 수 있습니다.
/* fs/nfs/nfs42proc.c - SEEK 연산 */
loff_t nfs42_proc_llseek(struct file *filep,
loff_t offset, int whence)
{
struct nfs42_seek_args args = {
.sa_fh = NFS_FH(inode),
.sa_offset = offset,
.sa_what = (whence == SEEK_HOLE) ?
NFS4_CONTENT_HOLE : NFS4_CONTENT_DATA,
};
struct nfs42_seek_res res;
/* COMPOUND: SEQUENCE + PUTFH + SEEK */
status = nfs4_call_sync(server->client, &msg,
&args.seq_args, &res.seq_res, 0);
if (res.sr_eof)
return -ENXIO;
return vfs_setpos(filep, res.sr_offset, inode->i_sb->s_maxbytes);
}
/* ALLOCATE: 지정 범위에 공간 사전 할당 (fallocate 연동) */
static int nfs42_proc_allocate(struct file *filep,
loff_t offset, loff_t len)
{
/* COMPOUND: SEQUENCE + PUTFH + ALLOCATE(offset, length) */
/* 서버가 지정 범위의 디스크 블록을 미리 할당 */
}
/* DEALLOCATE: 지정 범위의 블록 해제 (hole punching) */
static int nfs42_proc_deallocate(struct file *filep,
loff_t offset, loff_t len)
{
/* COMPOUND: SEQUENCE + PUTFH + DEALLOCATE(offset, length) */
/* fallocate(FALLOC_FL_PUNCH_HOLE) 매핑 */
}
레이블 NFS (Labeled NFS)
NFSv4.2의 레이블 NFS는 SELinux 등 MAC(Mandatory Access Control) 보안 레이블을 NFS를 통해 전파합니다. 서버와 클라이언트가 동일한 보안 정책을 공유하면, 원격 파일에도 일관된 보안 레이블이 적용됩니다.
# 레이블 NFS 설정
# 서버 exports (security_label 옵션)
/export *(rw,sync,no_subtree_check,security_label)
# 클라이언트 마운트
mount -t nfs -o vers=4.2,context="system_u:object_r:nfs_t:s0" \
server:/export /mnt/nfs
# 또는 서버 레이블 그대로 사용
mount -t nfs -o vers=4.2 server:/export /mnt/nfs
# 레이블 확인
ls -Z /mnt/nfs/
# -rw-r--r--. user group system_u:object_r:nfs_t:s0 file.txt
클론/리플링크 (CLONE)
NFSv4.2 CLONE 연산은 서버의 COW(Copy-on-Write) 파일시스템(Btrfs, XFS reflink)의 블록 공유를 NFS를 통해 노출합니다. cp --reflink이 NFS 마운트에서도 동작합니다.
/* fs/nfs/nfs42proc.c - CLONE 연산 */
static int nfs42_proc_clone(struct file *src_f,
struct file *dst_f, loff_t src_offset,
loff_t dst_offset, loff_t count)
{
/* COMPOUND: SEQUENCE + PUTFH(src) + SAVEFH +
* PUTFH(dst) + CLONE(src_offset, dst_offset, count)
*
* 서버 파일시스템이 reflink를 지원해야 함
* (Btrfs, XFS with reflink, OCFS2) */
struct nfs42_clone_args args = {
.src_fh = NFS_FH(src_inode),
.dst_fh = NFS_FH(dst_inode),
.src_offset = src_offset,
.dst_offset = dst_offset,
.count = count,
};
return nfs4_call_sync(server->client, &msg,
&args.seq_args, &res.seq_res, 0);
}
I/O 어드바이스 (IO_ADVISE)
NFSv4.2 IO_ADVISE 연산은 클라이언트의 posix_fadvise() 힌트를 서버에 전달하여 서버 측 캐시 전략을 최적화합니다.
| 어드바이스 | POSIX 매핑 | 서버 동작 |
|---|---|---|
| IO_ADVISE4_NORMAL | POSIX_FADV_NORMAL | 기본 캐시 정책 |
| IO_ADVISE4_SEQUENTIAL | POSIX_FADV_SEQUENTIAL | readahead 증가 |
| IO_ADVISE4_RANDOM | POSIX_FADV_RANDOM | readahead 비활성화 |
| IO_ADVISE4_WILLNEED | POSIX_FADV_WILLNEED | 데이터 선제 로드 |
| IO_ADVISE4_DONTNEED | POSIX_FADV_DONTNEED | 캐시에서 제거 |
| IO_ADVISE4_NOREUSE | POSIX_FADV_NOREUSE | 재사용 없음 힌트 |
NFSv4.2 기능 요약
| 기능 | 연산 | 시스템 콜 매핑 | 커널 버전 |
|---|---|---|---|
| 서버 사이드 복사 | COPY, OFFLOAD_CANCEL, CB_OFFLOAD | copy_file_range() | 4.11+ |
| 희소 파일 | SEEK, ALLOCATE, DEALLOCATE | lseek(SEEK_HOLE/DATA), fallocate() | 4.2+ |
| 레이블 NFS | GETATTR/SETATTR(security_label) | SELinux xattr | 3.14+ |
| 클론/리플링크 | CLONE | ioctl(FICLONE) | 4.5+ |
| I/O 어드바이스 | IO_ADVISE | posix_fadvise() | 4.5+ |
Linux NFS 서버 (knfsd)
knfsd 아키텍처
리눅스 NFS 서버(knfsd)는 커널 공간에서 동작합니다. fs/nfsd/ 디렉토리에 구현되어 있으며, 유저스페이스 nfs-utils 패키지가 설정과 관리를 담당합니다.
/* fs/nfsd/nfssvc.c - nfsd 커널 스레드 */
static int nfsd(void *vrqstp)
{
struct svc_rqst *rqstp = vrqstp;
for (;;) {
/* RPC 요청 대기 */
int err = svc_recv(rqstp, MAX_SCHEDULE_TIMEOUT);
if (err == -EAGAIN || err == -EINTR)
continue;
validate_process_creds();
/* RPC 요청 처리 (디스패치) */
svc_process(rqstp);
validate_process_creds();
}
svc_exit_thread(rqstp);
return 0;
}
NFS 파일핸들 (Filehandle)
NFS에서 모든 파일/디렉토리는 파일핸들(filehandle)로 식별됩니다. 파일핸들은 서버가 생성하는 불투명(opaque) 바이트 배열로, 클라이언트는 이를 해석하지 않고 서버에 그대로 전달합니다. 리눅스 knfsd는 파일핸들에 export ID, inode 번호, 생성 시간을 인코딩합니다.
/* include/linux/nfsd/nfsfh.h - NFS 파일핸들 구조 */
struct knfsd_fh {
unsigned int fh_size; /* 파일핸들 크기 (바이트) */
union {
struct {
__u8 fh_version; /* 파일핸들 버전 (1) */
__u8 fh_auth_type; /* 인증 유형 */
__u8 fh_fsid_type; /* fsid 인코딩 방식 */
__u8 fh_fileid_type; /* fileid 인코딩 방식 */
__u32 fh_fsid[]; /* 파일시스템 ID (export 식별) */
} fh_new;
__u8 fh_raw[NFS4_FHSIZE]; /* NFSv4: 최대 128바이트 */
} fh_base;
};
/* fs/nfsd/nfsfh.c - 파일핸들 → dentry 변환 */
__be32 fh_verify(struct svc_rqst *rqstp,
struct svc_fh *fhp, umode_t type, int access)
{
/* 1. fh_fsid → export 조회 */
/* 2. export의 ops->fh_to_dentry() 호출 */
/* 3. inode 번호 + generation 검증 */
/* 4. 접근 권한 확인 (export 옵션 + UNIX perms) */
/* Stale filehandle: inode가 삭제된 경우 */
if (IS_ERR(dentry))
return nfserr_stale; /* ESTALE → 클라이언트에 통보 */
}
/* NFSv3: 최대 64바이트, NFSv4: 최대 128바이트 */
#define NFS3_FHSIZE 64
#define NFS4_FHSIZE 128
no_subtree_check 옵션은 서브트리 이동 시 stale 발생을 줄여줍니다.
Duplicate Request Cache (DRC)
NFS 서버는 DRC(Duplicate Request Cache, 중복 요청 캐시)를 유지하여 동일한 RPC 요청이 재전송될 경우 연산을 반복 수행하지 않고 캐시된 응답을 반환합니다. 이는 비멱등(non-idempotent) 연산(CREATE, REMOVE, RENAME 등)의 안전성을 보장하는 핵심 메커니즘입니다.
/* fs/nfsd/nfscache.c - 중복 요청 캐시 */
struct nfsd_drc_bucket {
struct list_head lru;
spinlock_t lock;
};
struct svc_cacherep {
struct list_head c_lru;
unsigned char c_state; /* RC_UNUSED/INPROG/DONE */
unsigned char c_type; /* RC_NOCACHE/REPLSTAT/REPLBUFF */
struct svc_opaque_auth c_cred; /* 요청자 자격증명 */
__be32 c_xid; /* RPC XID (요청 식별) */
u32 c_prot; /* 프로토콜 (TCP/UDP) */
u32 c_proc; /* 프로시저 번호 */
u32 c_vers; /* 프로그램 버전 */
unsigned long c_timestamp;
union {
struct kvec u_vec; /* 캐시된 응답 데이터 */
__be32 u_status; /* 캐시된 상태 코드 */
} c_u;
};
/* 중복 요청 확인: XID + 소스 주소 + 프로시저로 매칭 */
int nfsd_cache_lookup(struct svc_rqst *rqstp)
{
/* XID 해시 → 버킷 탐색 → 일치 항목 검색
* - 일치: 캐시된 응답 반환 (RC_REPLSTAT/RC_REPLBUFF)
* - 미일치: RC_DOIT → 요청 처리 후 응답 캐시
* - 처리 중(RC_INPROG): 재전송 드롭 */
}
# DRC 관련 커널 파라미터
# DRC 항목 수 (기본 1024, 대규모 서버는 증가)
# /etc/nfs.conf
[nfsd]
drc-tcp-size=4096
# DRC 통계 확인
cat /proc/net/rpc/nfsd
# rc 항목: hits misses nocache
# → hits: DRC 히트 (중복 요청 캐시 응답)
# → misses: DRC 미스 (신규 요청)
# → nocache: 캐시 불필요 (멱등 연산)
exports 설정
/etc/exports 파일에서 NFS로 공유할 디렉토리와 접근 권한을 정의합니다. exportfs 명령으로 커널에 반영합니다.
# /etc/exports 설정 예시
# 기본 읽기/쓰기 공유
/export/data 192.168.1.0/24(rw,sync,no_subtree_check)
# 읽기 전용 + root squash (보안)
/export/public *(ro,root_squash,all_squash,anonuid=65534,anongid=65534)
# NFSv4용 pseudo filesystem root
/export *(fsid=0,ro,no_subtree_check)
# Kerberos 보안
/export/secure gss/krb5p(rw,sync,no_subtree_check)
# crossmnt: 하위 마운트 자동 traversal
/export 192.168.1.0/24(rw,sync,no_subtree_check,crossmnt)
| 옵션 | 설명 |
|---|---|
rw / ro | 읽기/쓰기 또는 읽기 전용 |
sync / async | 동기/비동기 쓰기 (sync 권장, async는 데이터 손실 위험) |
no_subtree_check | 서브트리 검사 비활성화 (성능 향상, 기본 권장) |
root_squash | root(UID 0) 접근을 anonymous 사용자로 매핑 |
all_squash | 모든 사용자를 anonymous로 매핑 |
fsid=0 | NFSv4 pseudo filesystem root 지정 |
crossmnt | 하위 파일시스템 자동 내보내기 |
sec=krb5/krb5i/krb5p | Kerberos 보안 수준 (인증/무결성/프라이버시) |
nfs-utils 데몬
# nfsd 서버 시작 (8개 커널 스레드)
systemctl start nfs-server
# 또는 수동
rpc.nfsd 8
# exports 변경 반영
exportfs -ra
# 현재 export 목록 확인
exportfs -v
# 커널 nfsd 스레드 수 확인/변경
cat /proc/fs/nfsd/threads
echo 16 > /proc/fs/nfsd/threads
# NFS 서버 통계
nfsstat -s
# NFS 클라이언트 통계
nfsstat -c
# NFSv4 지원 버전 확인
cat /proc/fs/nfsd/versions
# 출력 예: -2 +3 +4 +4.1 +4.2
/proc/net/rpc/nfsd의 th 행에서 스레드 사용률을 확인할 수 있습니다.
Linux NFS 클라이언트
클라이언트 아키텍처
NFS 클라이언트는 VFS 계층에 nfs 파일시스템 유형으로 등록됩니다. 커널 소스의 fs/nfs/ 디렉토리에 구현되어 있습니다.
/* fs/nfs/super.c - NFS 파일시스템 등록 */
struct file_system_type nfs_fs_type = {
.owner = THIS_MODULE,
.name = "nfs",
.init_fs_context = nfs_init_fs_context,
.parameters = nfs_fs_parameters,
.kill_sb = nfs_kill_super,
.fs_flags = FS_RENAME_DOES_D_MOVE | FS_BINARY_MOUNTDATA,
};
struct file_system_type nfs4_fs_type = {
.owner = THIS_MODULE,
.name = "nfs4",
.init_fs_context = nfs_init_fs_context,
.parameters = nfs_fs_parameters,
.kill_sb = nfs_kill_super,
.fs_flags = FS_RENAME_DOES_D_MOVE | FS_BINARY_MOUNTDATA,
};
nfs_client 구조체
/* include/linux/nfs_fs_sb.h */
struct nfs_client {
refcount_t cl_count;
atomic_t cl_mds_count;
int cl_cons_state; /* 연결 상태 */
unsigned long cl_res_expire; /* lease 만료 */
unsigned long cl_flags;
struct __kernel_sockaddr_storage cl_addr;
const char *cl_hostname; /* 서버 호스트명 */
struct list_head cl_share_link; /* 공유 전송 */
struct list_head cl_superblocks; /* superblock 목록 */
struct rpc_clnt *cl_rpcclient; /* RPC 클라이언트 */
const struct nfs_rpc_ops *rpc_ops; /* v3/v4 분기 */
int cl_proto; /* XPRT_TRANSPORT_* */
int cl_nfs_mod_state;
#if IS_ENABLED(CONFIG_NFS_V4)
u64 cl_clientid; /* NFSv4 client ID */
nfs4_verifier cl_confirm;
struct nfs4_session *cl_session; /* v4.1 세션 */
unsigned long cl_lease_time;
unsigned long cl_last_renewal;
#endif
};
마운트 옵션
# NFSv4.2 마운트 (권장 설정)
mount -t nfs -o vers=4.2,tcp,rsize=1048576,wsize=1048576,\
hard,timeo=600,retrans=2,nconnect=8 \
server:/export/data /mnt/nfs
# /etc/fstab 설정
server:/export/data /mnt/nfs nfs vers=4.2,hard,timeo=600,\
retrans=2,rsize=1048576,wsize=1048576,nconnect=8,_netdev 0 0
# autofs 자동 마운트
# /etc/auto.master
/mnt/auto /etc/auto.nfs --timeout=300
# /etc/auto.nfs
data -rw,vers=4.2,hard,nconnect=4 server:/export/data
| 마운트 옵션 | 기본값 | 설명 |
|---|---|---|
vers= | 협상 | NFS 프로토콜 버전 (3, 4, 4.1, 4.2) |
rsize= / wsize= | 협상 | 읽기/쓰기 전송 크기 (최대 1MB) |
hard / soft | hard | hard: 서버 복구까지 무한 재시도, soft: 타임아웃 후 에러 |
timeo= | 600 | RPC 타임아웃 (1/10초 단위) |
retrans= | 2 | 타임아웃 시 재전송 횟수 |
nconnect= | 1 | 서버당 TCP 연결 수 (최대 16, 병렬 성능 향상) |
proto= | tcp | 전송 프로토콜 (tcp, udp, rdma) |
sec= | sys | 보안 방식 (sys, krb5, krb5i, krb5p) |
_netdev | - | 네트워크 가용 후 마운트 (fstab용) |
hard를 사용하세요. soft는 서버 일시 장애 시 데이터 손상을 유발할 수 있습니다. soft+timeo 만료 시 프로세스에 EIO가 반환되어 쓰기 데이터가 유실됩니다.
NFSv4 ID 매핑 (idmapd)
NFSv4는 UID/GID 숫자 대신 user@domain 형식의 문자열 식별자를 사용합니다. 이를 통해 서로 다른 UID 체계를 가진 시스템 간에도 일관된 소유권 매핑이 가능합니다. rpc.idmapd 데몬 또는 커널의 NFSv4 idmapper가 문자열 ↔ UID/GID 변환을 수행합니다.
/* fs/nfsd/nfs4idmap.c - 서버 측 ID 매핑 */
struct ent {
struct cache_head h;
u32 id; /* UID 또는 GID */
char name[IDMAP_NAMESZ]; /* "user@domain" */
char authname[IDMAP_NAMESZ]; /* GSS 인증 이름 */
int type; /* IDMAP_TYPE_USER/GROUP */
};
/* 이름 → ID 변환 (GETATTR 응답 시) */
int nfsd_map_name_to_uid(
struct svc_rqst *rqstp,
const char *name, size_t namelen,
uid_t *id)
{
/* 1. 커널 키링에서 조회 (request_key) */
/* 2. 캐시 미스 시 유저스페이스 upcall (rpc.idmapd) */
/* 3. /etc/passwd, LDAP, NIS 등에서 매핑 확인 */
}
# NFSv4 ID 매핑 설정
# /etc/idmapd.conf - 서버/클라이언트 공통
[General]
Verbosity = 0
Domain = example.com # NFSv4 도메인 (양쪽 동일해야 함!)
[Mapping]
Nobody-User = nobody
Nobody-Group = nogroup
[Translation]
Method = nsswitch # nsswitch, umich_ldap, static */
# 커널 ID 매핑 키링 확인
cat /proc/net/rpc/nfs4.nametoid/content
cat /proc/net/rpc/nfs4.idtoname/content
# 커널 키링 기반 ID 매핑 (rpc.idmapd 대체, 최신 배포판 기본)
# /etc/request-key.conf에 id_resolver 설정
nfsidmap -c # 키링 캐시 클리어
nfsidmap -l # 키링 내용 조회
# 숫자 ID 직접 사용 (ID 매핑 우회)
# /etc/modprobe.d/nfs.conf
options nfs nfs4_disable_idmapping=1
options nfsd nfs4_disable_idmapping=1
/etc/idmapd.conf Domain 값이 다르면, 모든 파일의 소유자가 nobody:nogroup으로 표시됩니다. 도메인이 일치해야 user@domain 문자열이 올바르게 UID/GID로 변환됩니다. nfs4_disable_idmapping=1 설정 시 숫자 UID/GID를 직접 사용하여 이 문제를 우회할 수 있습니다.
NFSv4.1 클라이언트 ID 설정
NFSv4.1 클라이언트는 EXCHANGE_ID → CREATE_SESSION 과정을 통해 서버와 세션을 수립합니다. 클라이언트 ID는 클라이언트 재시작 시 이전 상태를 복구하는 데 핵심적 역할을 합니다.
/* fs/nfs/nfs4proc.c - EXCHANGE_ID: 클라이언트 ID 설정 */
static int _nfs4_proc_exchange_id(
struct nfs_client *clp,
const struct cred *cred,
u32 sp4_how)
{
struct nfs41_exchange_id_args args = {
.verifier = &verifier, /* 부팅 시 생성한 고유 verifier */
.client = clp,
/* co_ownerid: 클라이언트 식별 문자열
* "Linux NFSv4.1 /" */
};
/* 서버 응답:
* clientid: 64비트 클라이언트 ID
* sequence_id: 세션 생성용 시퀀스
* server_owner/scope: 트렁킹 판단용 */
return nfs4_call_sync(clp->cl_rpcclient, &msg,
&args.seq_args, &res.seq_res, 0);
}
/* CREATE_SESSION: 세션 수립 (EXCHANGE_ID 이후) */
static int _nfs4_proc_create_session(
struct nfs_client *clp,
const struct cred *cred)
{
/* 전방 채널: 클라이언트 → 서버 (NFS 연산)
* 후방 채널: 서버 → 클라이언트 (CB_RECALL 등 콜백)
*
* 채널별 속성:
* ca_maxrequests: 최대 동시 요청 (슬롯 수)
* ca_maxops: compound당 최대 연산 수
* ca_maxresp_sz: 최대 응답 크기 */
}
캐싱 메커니즘
속성 캐시 (Attribute Cache)
NFS 클라이언트는 파일 속성(크기, 수정 시간, 권한)을 로컬에 캐시하여 GETATTR RPC 호출을 줄입니다. 캐시 유효 시간은 acregmin/acregmax(일반 파일), acdirmin/acdirmax(디렉토리)로 제어합니다.
/* fs/nfs/inode.c - 속성 캐시 유효성 검사 */
static int nfs_attribute_cache_expired(struct inode *inode)
{
struct nfs_inode *nfsi = NFS_I(inode);
if (nfs_have_delegated_attributes(inode))
return 0; /* 위임이 있으면 캐시 항상 유효 */
return !time_in_range_open(jiffies,
nfsi->read_cache_jiffies,
nfsi->read_cache_jiffies + nfsi->attrtimeo);
}
/* 캐시 타임아웃은 적응적으로 증가:
* 파일이 변경되지 않으면 acregmin → acregmax까지 2배씩 증가
* 파일이 변경되면 acregmin으로 리셋 */
static void nfs_update_attrtimeo(struct nfs_inode *nfsi)
{
if (time_after(jiffies, nfsi->last_updated + nfsi->attrtimeo))
nfsi->attrtimeo = min(nfsi->attrtimeo << 1, nfsi->attrtimeo_max);
}
| 마운트 옵션 | 기본값(초) | 설명 |
|---|---|---|
acregmin | 3 | 일반 파일 속성 캐시 최소 시간 |
acregmax | 60 | 일반 파일 속성 캐시 최대 시간 |
acdirmin | 30 | 디렉토리 속성 캐시 최소 시간 |
acdirmax | 60 | 디렉토리 속성 캐시 최대 시간 |
actimeo= | - | 모든 속성 캐시 시간을 동일 값으로 설정 |
noac | - | 속성 캐시 완전 비활성화 (성능 저하 주의) |
데이터 캐시
NFS 클라이언트는 페이지 캐시를 사용하여 읽기/쓰기 데이터를 캐싱합니다. 캐시 일관성은 close-to-open(CTO) 시맨틱으로 보장합니다.
- close() 시: 수정된 데이터를 서버에 플러시하고, 속성을 서버와 동기화
- open() 시: 서버에 GETATTR을 보내 change attribute를 확인하고, 변경되었으면 캐시 무효화
/* fs/nfs/file.c - open 시 캐시 검증 */
static int nfs_file_open(struct inode *inode,
struct file *filp)
{
int res;
/* inode 속성 재검증 (CTO) */
res = nfs_check_flags(filp->f_flags);
if (res)
return res;
res = nfs_open(inode, filp);
return res;
}
/* fs/nfs/inode.c - close 시 데이터 플러시 */
static void nfs_file_clear_open_context(struct file *filp)
{
struct nfs_open_context *ctx = nfs_file_open_context(filp);
if (ctx) {
filp->private_data = NULL;
put_nfs_open_context_sync(ctx);
}
}
NFS 읽기/쓰기 I/O 경로
NFS 클라이언트의 데이터 I/O는 페이지 캐시를 통해 처리됩니다. 읽기는 readahead 알고리즘에 의해 사전 페칭되고, 쓰기는 writeback 메커니즘에 의해 비동기적으로 서버에 플러시됩니다.
/* fs/nfs/read.c - NFS 읽기 I/O */
static void nfs_readahead(struct readahead_control *ractl)
{
struct nfs_pageio_descriptor pgio;
/* readahead 윈도우 내 페이지들을 모아서 */
nfs_pageio_init_read(&pgio, ractl->mapping->host,
0, &nfs_async_read_completion_ops);
/* 연속 페이지를 하나의 READ RPC로 병합 */
readahead_expand(ractl, readahead_pos(ractl),
readahead_length(ractl));
nfs_pageio_complete_read(&pgio);
/* RPC 완료 시 콜백으로 페이지 unlock */
}
/* fs/nfs/write.c - NFS 쓰기 I/O */
static int nfs_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
struct nfs_pageio_descriptor pgio;
nfs_pageio_init_write(&pgio, mapping->host,
0, 0, &nfs_async_write_completion_ops);
/* dirty 페이지를 수집하여 WRITE RPC 전송
* wsize 크기로 분할하여 전송
*
* 안정성 모드:
* UNSTABLE → 서버 캐시만 (빠름, COMMIT 필요)
* FILE_SYNC → 서버 디스크 동기화 (느림, COMMIT 불필요) */
nfs_pageio_complete(&pgio);
return pgio.pg_error;
}
/* COMMIT: UNSTABLE 쓰기 후 서버 디스크 동기화 */
static int nfs_commit_inode(struct inode *inode, int how)
{
/* close() 또는 fsync() 시 호출
* COMPOUND: SEQUENCE + PUTFH + COMMIT(offset=0, count=0)
*
* 서버 verifier가 이전과 다르면
* → 서버 재시작 감지 → dirty 페이지 재전송 */
}
wsize=1048576(1MB)이면 최대 256개의 4K 페이지를 하나의 RPC로 전송할 수 있어 오버헤드가 크게 줄어듭니다. pNFS 환경에서는 여러 데이터 서버에 병렬 WRITE가 수행됩니다.
FS-Cache / CacheFiles
FS-Cache는 NFS 데이터를 로컬 디스크에 영구적으로 캐싱하는 커널 프레임워크입니다. 네트워크 대역폭 절약과 읽기 성능 향상에 효과적입니다.
# FS-Cache 설정 (CacheFiles 백엔드)
# 1. 캐시 디렉토리 생성
mkdir -p /var/cache/fscache
# 2. /etc/cachefilesd.conf
dir /var/cache/fscache
tag mycache
brun 10%
bcull 7%
bstop 3%
frun 10%
fcull 7%
fstop 3%
# 3. cachefilesd 시작
systemctl start cachefilesd
# 4. NFS 마운트 시 fsc 옵션 추가
mount -t nfs -o vers=4.2,fsc server:/export/data /mnt/nfs
# 캐시 상태 확인
cat /proc/fs/fscache/stats
잠금과 위임
바이트 범위 잠금 (Byte-Range Locking)
NFSv4에서 파일 잠금은 프로토콜에 내장되어 있습니다. POSIX 호환 fcntl() 잠금이 네트워크를 통해 서버에 전달됩니다.
/* fs/nfs/nfs4proc.c - NFSv4 LOCK 연산 */
static int nfs4_proc_lock(struct file *filp,
int cmd, struct file_lock *request)
{
struct nfs_open_context *ctx;
struct nfs4_state *state;
int status;
/* NFSv4 LOCK compound:
* SEQUENCE + LOCK(stateid, offset, length, type)
*
* 잠금 유형:
* READ_LT - 공유 잠금 (F_RDLCK)
* WRITE_LT - 배타 잠금 (F_WRLCK)
* READW_LT / WRITEW_LT - 대기형 잠금 (F_SETLKW) */
ctx = nfs_file_open_context(filp);
state = ctx->state;
if (request->fl_type == F_UNLCK)
status = nfs4_proc_unlck(state, cmd, request);
else
status = nfs4_proc_setlk(state, cmd, request);
return status;
}
위임 상세
위임은 잠금과 밀접한 관련이 있습니다. WRITE 위임을 가진 클라이언트는 서버에 LOCK 요청을 보내지 않고 로컬에서 잠금을 처리할 수 있습니다.
/* fs/nfs/delegation.c - 위임 기반 잠금 최적화 */
int nfs4_lock_delegation_recall(
struct file_lock *fl,
struct nfs4_state *state,
const nfs4_stateid *stateid)
{
struct nfs_inode *nfsi = NFS_I(state->inode);
struct nfs_delegation *delegation;
delegation = rcu_dereference(nfsi->delegation);
if (delegation == NULL)
return 0;
/* WRITE 위임이면 로컬 잠금으로 처리 */
if (delegation->type == FMODE_WRITE)
return locks_lock_file_wait(fl);
return 0;
}
상태 복구 (State Recovery)
NFSv4 서버가 재시작되면, 클라이언트는 grace period 동안 이전 상태(open, lock, delegation)를 복구해야 합니다. 리눅스 NFS 클라이언트는 nfs4_state_manager() 워크큐에서 이를 자동 처리합니다.
/* fs/nfs/nfs4state.c - NFSv4 상태 관리자 */
static void nfs4_state_manager(struct nfs_client *clp)
{
int status;
/* 상태 복구 순서 */
do {
/* 1. 클라이언트 ID 재설정 (EXCHANGE_ID) */
if (test_bit(NFS4CLNT_LEASE_EXPIRED, &clp->cl_state)) {
status = nfs4_reclaim_lease(clp);
if (status)
continue;
}
/* 2. 세션 복구 (CREATE_SESSION) */
if (test_bit(NFS4CLNT_SESSION_RESET, &clp->cl_state)) {
status = nfs4_reset_session(clp);
}
/* 3. open/lock 상태 복구 */
if (test_bit(NFS4CLNT_RECLAIM_REBOOT, &clp->cl_state)) {
status = nfs4_do_reclaim(clp,
clp->cl_mvops->reboot_recovery_ops);
}
/* 4. 위임 복구 */
nfs_delegation_reap_unclaimed(clp);
} while (test_bit(NFS4CLNT_MANAGER_RUNNING, &clp->cl_state));
}
/proc/fs/nfsd/nfsv4leasetime에서 lease 시간을 설정합니다.
보안
인증 방식
| 보안 방식 | 인증 | 무결성 | 암호화 | 설명 |
|---|---|---|---|---|
AUTH_SYS (sec=sys) | UID/GID | 없음 | 없음 | 기본값. 클라이언트가 보낸 UID를 신뢰 |
AUTH_NONE (sec=none) | 없음 | 없음 | 없음 | 인증 없음 (anonymous) |
RPCSEC_GSS/krb5 | Kerberos | 없음 | 없음 | Kerberos 인증만 |
RPCSEC_GSS/krb5i | Kerberos | HMAC | 없음 | 인증 + 메시지 무결성 |
RPCSEC_GSS/krb5p | Kerberos | HMAC | AES | 인증 + 무결성 + 데이터 암호화 |
Kerberos 설정
# Kerberos NFS 설정 (서버 측)
# 1. Kerberos keytab 생성 (KDC에서)
kadmin.local
addprinc -randkey nfs/server.example.com@EXAMPLE.COM
ktadd -k /etc/krb5.keytab nfs/server.example.com@EXAMPLE.COM
# 2. /etc/exports 보안 설정
/export/secure *(sec=krb5p,rw,sync,no_subtree_check)
# 3. rpc.gssd (클라이언트) / rpc.svcgssd (서버) 시작
systemctl enable --now rpc-gssd # 클라이언트
systemctl enable --now rpc-svcgssd # 서버 (선택적)
# 4. 마운트
mount -t nfs -o vers=4.2,sec=krb5p server:/export/secure /mnt/secure
/* net/sunrpc/auth_gss/auth_gss.c - RPCSEC_GSS 인증 */
static const struct rpc_authops authgss_ops = {
.owner = THIS_MODULE,
.au_flavor = RPC_AUTH_GSS,
.au_name = "RPCSEC_GSS",
.create = gss_create,
.destroy = gss_destroy,
.hash_cred = gss_hash_cred,
.lookup_cred = gss_lookup_cred,
.crcreate = gss_create_cred,
.info2flavor = gss_mech_info2flavor,
};
/* GSS 컨텍스트 구조: 세션 키, 시퀀스 번호, 알고리즘 */
struct gss_cl_ctx {
refcount_t count;
enum rpc_gss_svc gc_service; /* none/integrity/privacy */
struct gss_ctx *gc_gss_ctx; /* GSS 메커니즘 컨텍스트 */
struct xdr_netobj gc_wire_ctx; /* 와이어 상의 컨텍스트 핸들 */
atomic_t gc_seq; /* 시퀀스 번호 (재전송 방지) */
};
NFSv4 ACL
NFSv4는 Windows ACL과 유사한 풍부한 ACL 체계를 지원합니다. POSIX 모드 비트와 별개로, 세밀한 접근 제어가 가능합니다.
# NFSv4 ACL 설정 (nfs4-acl-tools 패키지)
# ACL 조회
nfs4_getfacl /mnt/nfs/file.txt
# 출력 예:
# A::OWNER@:rwatTnNcCoy
# A::GROUP@:rtncy
# A::EVERYONE@:rtncy
# 특정 사용자에게 읽기/쓰기 권한 부여
nfs4_setfacl -a "A::user@EXAMPLE.COM:rwatTnNcCoy" /mnt/nfs/file.txt
# ACL 형식: type:flags:principal:permissions
# type: A(Allow), D(Deny), U(aUdit), L(aLarm)
# flags: f(file-inherit), d(directory-inherit), g(group)
# principal: OWNER@, GROUP@, EVERYONE@, user@domain
NFS over RDMA (NFS/RDMA)
NFS/RDMA는 TCP 대신 RDMA(Remote Direct Memory Access) 전송을 사용하여 CPU 오버헤드를 최소화하고 극도로 낮은 지연 시간을 달성합니다. InfiniBand, RoCE(RDMA over Converged Ethernet), iWARP 네트워크에서 동작합니다.
RDMA 전송 아키텍처
/* net/sunrpc/xprtrdma/ - 클라이언트 RDMA 전송 */
/* xprtrdma: RPC/RDMA 클라이언트 전송 모듈 */
struct rpcrdma_xprt {
struct rpc_xprt rx_xprt;
struct rpcrdma_ia rx_ia; /* RDMA 어댑터 */
struct rpcrdma_ep rx_ep; /* RDMA 엔드포인트 */
struct rpcrdma_buffer rx_buf; /* 버퍼 풀 */
};
/* net/sunrpc/svcrdma/ - 서버 RDMA 전송 */
/* svcrdma: RPC/RDMA 서버 전송 모듈 */
struct svcxprt_rdma {
struct svc_xprt sc_xprt;
struct rdma_cm_id *sc_cm_id; /* CM 연결 ID */
struct ib_pd *sc_pd; /* Protection Domain */
struct ib_cq *sc_rq_cq; /* 수신 CQ */
struct ib_cq *sc_sq_cq; /* 송신 CQ */
u32 sc_max_requests; /* 최대 동시 요청 */
};
# NFS/RDMA 마운트
mount -t nfs -o vers=4.2,proto=rdma,port=20049 \
server:/export/data /mnt/nfs_rdma
# 서버 측: RDMA 전송 활성화
# /etc/nfs.conf
[nfsd]
rdma=y
rdma-port=20049
# 커널 모듈 로드
modprobe svcrdma # 서버
modprobe xprtrdma # 클라이언트
# RDMA 포트 활성화 (서버)
echo rdma 20049 > /proc/fs/nfsd/portlist
성능 튜닝
rsize/wsize 최적화
읽기/쓰기 전송 크기는 NFS 성능에 가장 큰 영향을 미치는 파라미터입니다. NFSv3는 최대 64KB, NFSv4는 최대 1MB까지 지원합니다.
# rsize/wsize 테스트
# 작은 크기 (비효율)
mount -t nfs -o vers=4.2,rsize=4096,wsize=4096 server:/data /mnt/test
dd if=/mnt/test/largefile of=/dev/null bs=1M count=1024
# 100 MB/s (4K RPC 요청 = 높은 오버헤드)
# 최적 크기 (기본 협상)
mount -t nfs -o vers=4.2,rsize=1048576,wsize=1048576 server:/data /mnt/test
dd if=/mnt/test/largefile of=/dev/null bs=1M count=1024
# 800+ MB/s (1M RPC 요청 = 낮은 오버헤드)
nconnect 병렬 연결
커널 5.3+에서 도입된 nconnect 옵션은 단일 NFS 마운트에서 여러 TCP 연결을 사용하여 대역폭을 극대화합니다. 단일 TCP 연결의 병목(헤드오브라인 블로킹, 혼잡 윈도우 제한)을 회피합니다.
# nconnect=8: 8개 TCP 연결로 병렬 I/O
mount -t nfs -o vers=4.2,nconnect=8 server:/data /mnt/nfs
# 연결 확인
ss -tn | grep :2049 | wc -l
# 출력: 8
# max_connect (v5.14+): 다중 서버 IP 트렁킹
mount -t nfs -o vers=4.1,max_connect=4 server:/data /mnt/nfs
async vs sync
/etc/exports에서 async 옵션은 서버가 데이터를 디스크에 쓰기 전에 클라이언트에 완료 응답을 보냅니다. 서버 장애 시 데이터 유실이 발생할 수 있으므로, 프로덕션 환경에서는 반드시 sync를 사용하세요.
/* fs/nfsd/vfs.c - sync vs async 쓰기 처리 */
static __be32
nfsd_vfs_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
struct nfsd_file *nf, loff_t offset,
struct kvec *vec, int vlen,
unsigned long *cnt, int stable,
__be32 *verf)
{
/* stable 모드:
* NFS_UNSTABLE (0) - 비동기 (서버 캐시만)
* NFS_DATA_SYNC (1) - 데이터 디스크 동기화
* NFS_FILE_SYNC (2) - 데이터 + 메타데이터 동기화 */
if (stable != NFS_UNSTABLE) {
flags |= RWF_SYNC; /* O_SYNC 쓰기 */
}
host_err = vfs_writev(nf->nf_file, vec, vlen, &offset, flags);
/* ... */
}
sunrpc 디버깅
# sunrpc 디버그 플래그 활성화
# 클라이언트 RPC 디버그
echo 0xFFFF > /proc/sys/sunrpc/rpc_debug
# NFS 클라이언트 디버그
echo 0xFFFF > /proc/sys/sunrpc/nfs_debug
# NFS 서버 디버그
echo 0xFFFF > /proc/sys/sunrpc/nfsd_debug
# rpcdebug 유틸리티 (세분화된 제어)
rpcdebug -m rpc -s all # 모든 RPC 디버그
rpcdebug -m nfs -s vfs fh # NFS VFS + 파일핸들 디버그
rpcdebug -m nfsd -s proc # nfsd 프로시저 디버그
rpcdebug -m rpc -c all # 디버그 해제
# dmesg에서 NFS 관련 로그 확인
dmesg | grep -i nfs
nfsstat 모니터링
# 서버 통계 (주요 항목)
nfsstat -s
# Server nfs v4:
# null getattr setattr lookup access readlink
# 0 152634 8432 45231 89012 1234
# read write create remove rename ...
# 234567 123456 5678 2345 678 ...
# 클라이언트 통계
nfsstat -c
# 마운트별 상세 통계
nfsstat -m
# 또는
cat /proc/self/mountstats
# 실시간 모니터링 (1초 간격)
nfsstat -l
# 주요 지표 해석:
# - retrans: 재전송 횟수 (높으면 네트워크 문제)
# - jitter: RPC 응답 시간 편차 (높으면 서버 부하)
# - backlog: 대기 큐 깊이 (높으면 nconnect 필요)
커널 파라미터 튜닝
# /etc/sysctl.conf - NFS 관련 커널 파라미터
# sunrpc 전송 버퍼 크기
sunrpc.tcp_slot_table_entries = 128 # 기본 2, 동시 RPC 요청 수
sunrpc.udp_slot_table_entries = 128
# TCP 버퍼 크기 (NFS 대역폭에 영향)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# NFS 서버 스레드 수 (높은 동시 접속 시)
# /etc/nfs.conf
[nfsd]
threads=64
grace-time=90
lease-time=90
# NFS 서버 TCP window 크기
# 적용
sysctl -p
성능 최적화 체크리스트
| 항목 | 권장값 | 영향 |
|---|---|---|
| NFS 버전 | v4.2 | compound ops, 서버 사이드 복사, 단일 포트 |
| rsize/wsize | 1048576 (1MB) | 대용량 전송 시 오버헤드 최소화 |
| nconnect | 4~16 | 병렬 TCP 연결로 대역폭 극대화 |
| nfsd threads | CPU 코어 x 2~4 | 서버 동시 처리 능력 |
| sunrpc slot table | 128+ | 동시 RPC 요청 수 |
| TCP buffer | 16MB+ | WAN/고지연 환경 처리량 |
| 전송 프로토콜 | TCP (LAN), RDMA (HPC) | 지연 시간, CPU 사용률 |
| FS-Cache | 활성화 (읽기 위주) | 반복 읽기 시 네트워크 절감 |
| noatime | 서버/클라이언트 모두 | 불필요한 SETATTR 감소 |
커널 소스 구조
Linux NFS 관련 코드의 디렉토리 구조를 정리합니다.
fs/nfs/ # NFS 클라이언트
├── client.c # nfs_client 관리
├── dir.c # 디렉토리 연산
├── file.c # 파일 연산 (open/read/write)
├── inode.c # inode 연산, 속성 캐시
├── super.c # superblock, 마운트
├── read.c / write.c # 데이터 I/O
├── delegation.c # 위임 처리
├── nfs3proc.c # NFSv3 프로시저
├── nfs4proc.c # NFSv4 프로시저
├── nfs4state.c # NFSv4 상태 관리/복구
├── nfs4xdr.c # NFSv4 XDR 인코딩/디코딩
├── pnfs.c # pNFS 코어
├── pnfs_nfs.c # pNFS-NFS 레이아웃
├── flexfilelayout/ # FlexFiles 레이아웃 드라이버
├── blocklayout/ # Blocks 레이아웃 드라이버
└── fscache.c # FS-Cache 통합
fs/nfsd/ # NFS 서버 (knfsd)
├── nfssvc.c # nfsd 서비스 스레드
├── nfs3proc.c # NFSv3 서버 프로시저
├── nfs4proc.c # NFSv4 서버 compound 처리
├── nfs4state.c # 서버 상태 관리 (open/lock/delegation)
├── nfs4xdr.c # NFSv4 서버 XDR
├── vfs.c # 로컬 VFS 연동
├── export.c # export 테이블 관리
└── nfs4callback.c # NFSv4 콜백 (CB_RECALL 등)
net/sunrpc/ # SUNRPC 프레임워크
├── clnt.c # RPC 클라이언트
├── svc.c # RPC 서버 (svc_process)
├── xprtsock.c # TCP/UDP 전송
├── xprtrdma/ # RDMA 전송 (클라이언트)
├── svcrdma/ # RDMA 전송 (서버)
├── auth_gss/ # RPCSEC_GSS (Kerberos)
├── xdr.c # XDR 인코딩/디코딩 라이브러리
└── cache.c # RPC 캐시 (export, auth)
커널 설정 (Kconfig)
# NFS 관련 주요 Kconfig 옵션
CONFIG_NFS_FS=m # NFS 클라이언트
CONFIG_NFS_V3=y # NFSv3 지원
CONFIG_NFS_V3_ACL=y # NFSv3 POSIX ACL
CONFIG_NFS_V4=y # NFSv4 지원
CONFIG_NFS_V4_1=y # NFSv4.1 (pNFS 필수)
CONFIG_NFS_V4_2=y # NFSv4.2
CONFIG_NFS_FSCACHE=y # FS-Cache 통합
CONFIG_NFS_USE_KERNEL_DNS=y # 커널 DNS 확인
CONFIG_NFSD=m # NFS 서버 (knfsd)
CONFIG_NFSD_V3=y # 서버 NFSv3
CONFIG_NFSD_V3_ACL=y # 서버 NFSv3 ACL
CONFIG_NFSD_V4=y # 서버 NFSv4
CONFIG_NFSD_PNFS=y # 서버 pNFS
CONFIG_NFSD_BLOCKLAYOUT=y # pNFS 블록 레이아웃
CONFIG_NFSD_SCSILAYOUT=y # pNFS SCSI 레이아웃
CONFIG_NFSD_FLEXFILELAYOUT=y # pNFS FlexFiles
CONFIG_SUNRPC=m # SUNRPC 프레임워크
CONFIG_SUNRPC_GSS=m # RPCSEC_GSS 인증
CONFIG_SUNRPC_XPRT_RDMA=m # RPC/RDMA 전송
CONFIG_PNFS_FILE_LAYOUT=m # pNFS 파일 레이아웃
CONFIG_PNFS_BLOCK=m # pNFS 블록 레이아웃
CONFIG_PNFS_FLEXFILE_LAYOUT=m # pNFS FlexFiles 레이아웃
NFS 실무 활용
고가용성 NFS (HA-NFS)
프로덕션 환경에서 NFS 서버 단일 장애점을 해소하기 위해 고가용성(HA) 구성이 필수적입니다. Linux에서는 Pacemaker/Corosync 클러스터 프레임워크와 공유 스토리지(DRBD, SAN)를 조합하여 active/passive NFS HA를 구현합니다.
# HA-NFS 구성 요소
# 1. 공유 스토리지: DRBD 또는 SAN LUN
# 2. 클러스터: Pacemaker + Corosync
# 3. 가상 IP (VIP): 클라이언트 접속용
# 4. NFSv4 grace 관리: v4recoverydir 공유
# Pacemaker NFS 리소스 설정 예시
pcs resource create nfs_vip ocf:heartbeat:IPaddr2 \
ip=192.168.1.100 cidr_netmask=24 \
op monitor interval=10s
pcs resource create nfs_share ocf:heartbeat:Filesystem \
device="/dev/drbd0" directory="/export" \
fstype="xfs" \
op monitor interval=20s
pcs resource create nfs_daemon ocf:heartbeat:nfsserver \
nfs_shared_infodir="/export/nfsinfo" \
nfs_ip=192.168.1.100 \
op monitor interval=30s
pcs resource create nfs_root exportfs \
clientspec="192.168.1.0/24" \
options="rw,sync,no_root_squash" \
directory="/export/data" \
fsid=0 \
op monitor interval=30s
# 리소스 순서/배치 제약 (반드시 순서대로 시작)
pcs constraint order nfs_share then nfs_daemon
pcs constraint order nfs_daemon then nfs_root
pcs constraint colocation add nfs_root with nfs_daemon
pcs constraint colocation add nfs_daemon with nfs_share
pcs constraint colocation add nfs_share with nfs_vip
/var/lib/nfs/v4recovery/ 디렉토리를 공유 스토리지에 두어 클라이언트 상태 복구(reclaim)가 가능하도록 합니다. nfs_ip 설정으로 VIP를 NFS에 바인딩하여 클라이언트가 grace period 내에 상태를 복구할 수 있습니다.
NFS와 컨테이너/Kubernetes
NFS는 Kubernetes의 PersistentVolume(PV) 백엔드로 널리 사용됩니다. 여러 Pod에서 동시 읽기/쓰기(ReadWriteMany)가 가능한 몇 안 되는 스토리지 유형입니다.
# Kubernetes NFS PersistentVolume 정의
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany # NFS는 RWX 지원
persistentVolumeReclaimPolicy: Retain
nfs:
server: nfs-server.example.com
path: /export/k8s-data
mountOptions:
- vers=4.2
- hard
- nconnect=4
- rsize=1048576
- wsize=1048576
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: nfs-pv
# NFS CSI 드라이버 (동적 프로비저닝)
# csi-driver-nfs: Kubernetes SIG Storage 프로젝트
# 서버에 PVC별 서브디렉토리 자동 생성/삭제
# Helm으로 설치
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
--namespace kube-system
# StorageClass 정의
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: nfs-server.example.com
share: /export/k8s-dynamic
mountPermissions: "0775"
mountOptions:
- vers=4.2
- hard
- nconnect=4
reclaimPolicy: Delete
volumeBindingMode: Immediate
docker volume create --driver local --opt type=nfs --opt o=addr=server,vers=4.2,hard --opt device=:/export/data nfs_vol
NFSv4.1 트렁킹 (Trunking)
NFSv4.1 트렁킹은 클라이언트가 서버의 여러 네트워크 인터페이스를 통해 단일 세션의 성능을 확장하는 메커니즘입니다. EXCHANGE_ID 응답의 server_owner가 동일하면 같은 서버로 인식하여 연결을 병합합니다.
| 트렁킹 유형 | 설명 | 요구사항 |
|---|---|---|
| clientid 트렁킹 | 동일 client ID로 여러 세션 → 각 세션이 독립 네트워크 경로 사용 | server_owner 일치 |
| 세션 트렁킹 | 하나의 세션에 여러 연결 바인딩 → 세션 내 요청이 여러 경로로 분산 | server_owner + server_scope 일치 |
# 트렁킹 설정: 서버의 여러 IP에 연결
# 첫 번째 마운트 (기본 연결)
mount -t nfs -o vers=4.1 server-ip1:/export /mnt/nfs
# 추가 연결 경로 바인딩 (max_connect, 커널 5.14+)
mount -t nfs -o vers=4.1,max_connect=4 server-ip2:/export /mnt/nfs2
# nconnect는 단일 IP에 여러 TCP 연결 (트렁킹과 별개)
mount -t nfs -o vers=4.1,nconnect=8 server:/export /mnt/nfs
# 트렁킹 상태 확인
cat /proc/self/mountstats | grep -A 20 "nfs4"
NFSv4 마이그레이션과 리퍼럴
NFSv4는 서버 간 파일시스템 마이그레이션과 리퍼럴(referral)을 프로토콜 수준에서 지원합니다. 클라이언트에게 투명하게 스토리지를 이동하거나 다른 서버로 리다이렉트할 수 있습니다.
| 메커니즘 | 에러 코드 | 용도 | 설명 |
|---|---|---|---|
| 마이그레이션 | NFS4ERR_MOVED | 서버 교체/로드밸런싱 | 파일시스템이 다른 서버로 이동됨. 클라이언트가 새 위치를 자동으로 따라감 |
| 리퍼럴 | NFS4ERR_MOVED | 분산 네임스페이스 | pseudo-fs의 특정 경로가 다른 서버를 가리킴. 클라이언트가 자동 리다이렉트 |
/* fs/nfs/nfs4state.c - 마이그레이션 감지 및 처리 */
static int nfs4_handle_migration(struct nfs_client *clp)
{
/* NFS4ERR_MOVED 수신 시:
* 1. fs_locations 속성 조회 (GETATTR)
* → 새 서버 주소 + 경로 획득
* 2. 새 서버에 EXCHANGE_ID + CREATE_SESSION
* 3. 기존 마운트를 새 서버로 전환
* 4. open/lock 상태 복구 (reclaim)
*
* 클라이언트 프로세스에게는 투명:
* 잠시 지연 후 정상 I/O 재개 */
}
/* fs/nfs/nfs4namespace.c - 리퍼럴 처리 */
static struct vfsmount *nfs4_submount(
struct nfs_server *server,
struct dentry *dentry,
struct nfs_fh *fh,
struct nfs_fattr *fattr)
{
/* 리퍼럴: pseudo-fs 교차점(junction)에서 자동 마운트
*
* 예: 서버 A의 /export/project가 서버 B를 가리킴
* → 클라이언트가 /mnt/nfs/project 접근 시
* → 자동으로 서버 B에 연결하여 서브마운트 */
struct nfs4_fs_locations *locations;
locations = nfs4_proc_fs_locations(server, fh, fattr);
return nfs_follow_referral(dentry, locations);
}
# NFSv4 리퍼럴 설정 (서버 측)
# 서버 A: pseudo-fs root 설정
# /etc/exports
/export *(fsid=0,ro,no_subtree_check,crossmnt)
/export/local *(rw,sync,no_subtree_check)
# 리퍼럴 지점 생성 (서버 B의 /data를 가리킴)
mkdir -p /export/remote
mount -t nfs4 -o vers=4.2 serverB:/data /export/remote
# 또는 nfs-utils의 junction 도구 사용
nfsref add /export/remote serverB:/data
# 클라이언트에서 투명하게 접근
mount -t nfs -o vers=4.2 serverA:/ /mnt/nfs
ls /mnt/nfs/local # → 서버 A
ls /mnt/nfs/remote # → 자동으로 서버 B로 리다이렉트
문제 해결
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
| mount: RPC: Program not registered | nfsd 미실행 | systemctl start nfs-server |
| mount: access denied | exports 설정 오류 | /etc/exports 확인 + exportfs -ra |
| Stale file handle | 서버 재export/재시작 | umount -f + 재마운트 |
| NFS: nfs4_reclaim_open_state | 서버 lease 만료 | 네트워크/서버 상태 확인 |
| nfs: server not responding | 네트워크 단절/서버 과부하 | 네트워크 확인, nfsd 스레드 증가 |
| Permission denied (UID mismatch) | UID/GID 불일치 | ID 매핑 또는 sec=krb5 사용 |
| 느린 ls (디렉토리 리스팅) | readdirplus 비활성화 | nordirplus 옵션 제거 확인 |
디버깅 명령어
# NFS 마운트 상세 정보
cat /proc/mounts | grep nfs
mount -l | grep nfs
# 마운트별 상세 통계 (RPC 지연, 재전송)
cat /proc/self/mountstats
# NFSv4 open 파일/위임 상태
cat /proc/fs/nfsd/clients/*/states
# RPC 전송 상태
cat /proc/net/rpc/nfs # 클라이언트
cat /proc/net/rpc/nfsd # 서버
# NFSv4 lease 시간 확인/설정
cat /proc/fs/nfsd/nfsv4leasetime
echo 90 > /proc/fs/nfsd/nfsv4leasetime
# 네트워크 패킷 캡처 (NFS 분석)
tcpdump -i eth0 -s 0 -w nfs.pcap port 2049
# Wireshark에서 nfs/rpc 프로토콜로 분석
# systemd 기반 NFS 서비스 상태
systemctl status nfs-server nfs-mountd rpc-statd rpcbind
tshark -r nfs.pcap -Y "nfs" -T fields -e frame.time -e nfs.procedure_v4 -e nfs.status 명령으로 프로시저별 응답 시간과 에러를 추출할 수 있습니다.
ftrace/tracepoint를 이용한 NFS 추적
커널의 NFS 서브시스템에는 다양한 tracepoint가 내장되어 있어 런타임 성능 분석과 디버깅에 활용할 수 있습니다.
# NFS 관련 tracepoint 목록 확인
cat /sys/kernel/debug/tracing/available_events | grep nfs
# nfs:nfs_readpage_done
# nfs:nfs_writeback_done
# nfs:nfs_commit_done
# nfs:nfs4_open_file
# nfs:nfs4_close
# nfs:nfs4_lock
# nfs:nfs4_delegreturn
# nfs:nfs4_state_mgr
# ...
# NFS READ 성능 추적
echo 1 > /sys/kernel/debug/tracing/events/nfs/nfs_readpage_done/enable
echo 1 > /sys/kernel/debug/tracing/events/nfs/nfs_initiate_read/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예:
# nfs-1234 [002] .... nfs_initiate_read: fileid=0x3f offset=0 count=131072
# nfs-1234 [002] .... nfs_readpage_done: fileid=0x3f offset=0 status=0
# SUNRPC 레벨 tracepoint (RPC 지연 분석)
echo 1 > /sys/kernel/debug/tracing/events/sunrpc/rpc_task_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/sunrpc/rpc_task_complete/enable
# bpftrace를 이용한 NFS 읽기 지연 히스토그램
bpftrace -e '
tracepoint:nfs:nfs_initiate_read {
@start[tid] = nsecs;
}
tracepoint:nfs:nfs_readpage_done /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
Stale Filehandle 심층 분석
ESTALE(Stale file handle) 에러는 NFS에서 가장 빈번하게 겪는 문제 중 하나입니다. 발생 원인과 해결법을 상세히 정리합니다.
| 원인 | 상세 | 해결 |
|---|---|---|
| 서버 파일 삭제 | 클라이언트가 캐시한 파일핸들의 inode가 서버에서 삭제됨 | 클라이언트에서 해당 파일 참조 제거 후 재접근 |
| 서버 re-export | exportfs -r 후 파일시스템 ID 변경 | fsid= 옵션으로 고정 fsid 사용 |
| 서버 파일시스템 재생성 | mkfs 후 동일 경로로 마운트 → inode 번호 불일치 | 클라이언트 umount + 재마운트 |
| subtree_check | 파일이 export 경로 내에서 이동됨 | no_subtree_check 사용 (기본 권장) |
| 서버 inode 재사용 | 삭제 후 같은 inode 번호에 새 파일 생성 | 파일핸들의 generation 필드로 감지 |
# Stale filehandle 발생 시 복구 절차
# 1. 영향 받는 프로세스 확인
lsof /mnt/nfs 2>&1 | grep -i stale
# 2. 강제 umount (프로세스가 남아있어도)
umount -f /mnt/nfs
# 또는 지연 umount (나중에 정리)
umount -l /mnt/nfs
# 3. 재마운트
mount -t nfs -o vers=4.2 server:/export /mnt/nfs
# 예방: exports에서 fsid 고정 (재export 시 안정성)
# /etc/exports
/export/data 192.168.1.0/24(rw,sync,no_subtree_check,fsid=100)
성능 저하 진단 체크리스트
# NFS 성능 저하 단계별 진단
# 1단계: 기본 네트워크 확인
ping -c 10 nfs-server
iperf3 -c nfs-server -t 10
# 대역폭이 예상보다 낮으면 네트워크 문제
# 2단계: NFS 마운트 옵션 확인
cat /proc/mounts | grep nfs
# rsize/wsize, nconnect, vers 확인
# 3단계: RPC 재전송/타임아웃 확인
nfsstat -rc
# retrans가 높으면 네트워크 불안정 또는 서버 과부하
# 4단계: 서버 스레드 사용률 확인
cat /proc/net/rpc/nfsd | grep th
# "th <10% 20% ... 100%>"
# 높은 비율이 90-100%에 몰리면 스레드 부족
# 5단계: 클라이언트 RPC backlog 확인
cat /proc/self/mountstats | grep -A 5 "backlog"
# backlog가 높으면 sunrpc.tcp_slot_table_entries 증가 필요
# 6단계: 서버 디스크 I/O 확인
iostat -x 1 5
# await가 높으면 서버 측 스토리지 병목
# 7단계: 속성 캐시 확인 (많은 GETATTR는 acregmin/max 조정)
nfsstat -c | grep -i getattr
# GETATTR 비율이 과도하면 actimeo 값 증가 고려