vnfs 0.0.9

Protocol-neutral vectorized filesystem API for NFS, SMB, and local files
Documentation

vnfs

A vectorized NFSv4.1 client library written in Rust.

NFSv4 supports COMPOUND requests: one RPC can carry an ordered sequence of file operations. A conventional POSIX-style loop hides that capability behind one-file-at-a-time calls, so latency grows with the number of files. vnfs instead exposes a vectorized VecFs API (the idea from the FAST'17 paper vNFS: Maximizing NFS Performance with Compounds and Vectorized I/O) as a Rust crate. The NFS backend packs each vector into compounds up to the server's negotiated operation and message-size limits, then returns results in input order.

Backends

The crate is backend-agnostic through the VecFs trait:

  • NfsVecFs — an NFSv4.1 implementation on top of libntirpc, batching many operations (lookups, readdirs, getattrs, opens, reads, ...) into few large compounds.
  • SmbVecFs — an SMB2/3 implementation for Samba and other modern SMB servers.
  • DummyVecFs — a std::fs-backed implementation so the same API also works on non-NFS filesystems (and is handy for tests).

NFS and the dummy backend are enabled by default. The SMB backend is opt-in because it uses the async smb2 client internally, so its synchronous VecFs facade owns a Tokio runtime. Enable it explicitly with the smb feature:

vnfs = { version = "0.0.9", features = ["smb"] }

Example

Read the first 4 KiB of 32 independent files as one vectorized operation:

use vnfs::{NfsVecFs, ReadOp, VecFs, VfOffset};

fn main() -> vnfs::VfResult<()> {
    let mut fs = NfsVecFs::connect("nfs.example.com")?;
    let paths: Vec<String> = (0..32)
        .map(|shard| format!("/dataset/shard-{shard:02}.json"))
        .collect();
    let reads: Vec<ReadOp> = paths
        .iter()
        .map(|path| ReadOp::from_path(path, VfOffset::At(0), 4096))
        .collect();

    // One API call gives the backend the whole batch. NfsVecFs emits as few
    // NFSv4 COMPOUND RPCs as the negotiated server limits allow.
    let results = fs.readv(&reads)?;

    for (path, result) in paths.iter().zip(results) {
        println!("{path}: {} bytes (eof={})", result.data.len(), result.eof);
    }
    Ok(())
}

With scalar calls, reading 32 paths requires a succession of path lookup, open, read, and close exchanges for each file. readv exposes all 32 reads at once, allowing NfsVecFs to place many independent operation chains into each NFSv4 COMPOUND. The number of latency-bearing RPC round trips therefore scales with the number of compound chunks instead of directly with the number of files. The exact packing depends on the server's ca_maxoperations, request size, and response-size limits; oversized vectors are split automatically.

The same model applies to openv, writev, getattrsv, listdirv, renamev, removev, and the other vector methods. This is especially useful for metadata-heavy workloads and for many small, independent I/O operations, where network latency dominates transfer time.

License

Licensed under either of

  • Apache License, Version 2.0
  • MIT license

at your option.