A Rust CLI and REST API tool for benchmarking NFS performance.
Status: WORKING (with patched gVisor)
NFS read/write operations work on gVisor with patches 0.1 and 0.2 applied to the runsc binary.
| Patch | Purpose | File |
|---|---|---|
| 0.1 | Skip disable_file_handle_sharing for NFS |
runsc/cmd/gofer.go |
| 0.2 | Skip fchown on NFS when EPERM (root_squash) |
runsc/fsgofer/lisafs.go |
Due to NFS root_squash, all files created from gVisor containers are owned by nobody:nogroup (uid 65534). This is expected:
- gVisor gofer process runs as root
- NFS server squashes root to nobody
- Files are created with nobody ownership
- Applications can still read/write files (permissions are 644/755)
Implications:
chown()calls silently succeed but files remain owned by nobody- Files created from non-root users will have different ownership
- Write access depends on file permissions (default 644 allows owner-only writes)
| Environment | Runtime | Storage | Purpose |
|---|---|---|---|
| Env 1 | gVisor (patched) | NFS | gVisor container with NFS mounted |
| Env 2 | gVisor (patched) | Ephemeral | gVisor container baseline |
| Env 3 | runc | NFS | Standard container with NFS |
| Env 4 | runc | Ephemeral | Standard container baseline |
# build
make build-release
# run REST API server
make serve
# or with docker
make docker-build
make docker-runThen open http://localhost:8080/ to see system status and API documentation.
- Sequential I/O: Measures throughput for sequential read/write operations
- Random I/O: Measures IOPS and latency for random access patterns (4KB blocks)
- Concurrent I/O: Tests multi-threaded file operations with configurable concurrency
- Metadata Operations: Benchmarks file create/delete, directory operations, and stat calls
- Mixed Workload: 70% read / 30% write mixed operations
- Append Operations: Continuous append to growing files
- Read-Only Mode: Skip write benchmarks for environments where writes are blocked (unpatched gVisor)
- Prometheus Metrics: Built-in HTTP server for metric scraping with gauge reset on benchmark completion
- JSON Reports: Structured output for analysis
- Environment Detection: Auto-detects gVisor vs runc runtime
# build from source
make build-release
# or with docker
make docker-buildStart the server:
nfsb serve --port 8080| Method | Path | Description |
|---|---|---|
| GET | / |
System status, jobs, mounts, and API docs |
| GET | /health |
Health check |
| GET | /metrics |
Prometheus metrics |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/benchmarks/run |
Start a benchmark job |
| GET | /api/v1/benchmarks/:id/status |
Get job status |
| GET | /api/v1/benchmarks/:id/results |
Get job results |
| DELETE | /api/v1/benchmarks/:id |
Delete a job |
| GET | /api/v1/jobs |
List all jobs |
| GET | /api/v1/info?path=<path> |
Get environment info |
| DELETE | /api/v1/cleanup?path=<path> |
Remove a test directory |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/mounts |
Mount a filesystem |
| GET | /api/v1/mounts |
List all mounts |
| DELETE | /api/v1/mounts?target=<path> |
Unmount a filesystem |
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/log-writer/write |
Write log entries (simulates nginx/app logs) |
| POST | /api/v1/log-analyzer/analyze |
Analyze logs and write reports |
| POST | /api/v1/file-uploader/upload |
Upload file (simulates Laravel-style uploads) |
| GET | /api/v1/file-uploader/list |
List uploaded files |
| DELETE | /api/v1/file-uploader/delete |
Delete uploaded file |
| POST | /api/v1/report-generator/generate |
Generate periodic reports |
| POST | /api/v1/report-aggregator/aggregate |
Aggregate reports from multiple sources |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/ownership/check?path=<path> |
Check file ownership |
| GET | /api/v1/ownership/tree?path=<path> |
List files with ownership info |
| GET | /api/v1/exec?cmd=<cmd>&cwd=<path> |
Execute shell command (for debugging) |
# 1. mount nfs share
curl -X POST http://localhost:8080/api/v1/mounts \
-H "Content-Type: application/json" \
-d '{"source": "10.0.0.1:/export", "target": "/mnt/nfs"}'
# 2. start benchmark on nfs
curl -X POST http://localhost:8080/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{"path": "/mnt/nfs", "benchmark": "all", "iterations": 50}'
# response: {"job_id": "uuid-here", "status": "pending", ...}
# 3. check status
curl http://localhost:8080/api/v1/benchmarks/<job_id>/status
# 4. get results when completed
curl http://localhost:8080/api/v1/benchmarks/<job_id>/results# run on nfs
curl -X POST http://localhost:8080/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{"path": "/mnt/nfs", "benchmark": "sequential"}'
# run on ephemeral (local) storage
curl -X POST http://localhost:8080/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{"path": "/data", "benchmark": "sequential"}'
# compare results from both job IDs# remove test files after benchmarks
curl -X DELETE "http://localhost:8080/api/v1/cleanup?path=/mnt/nfs/nfsb-bench"Only directories under /tmp/, /mnt/, /data/, or /workspace/ can be cleaned up (safety restriction).
# run a command in the container
curl "http://localhost:8080/api/v1/exec?cmd=ls%20-la&cwd=/mnt/nfs"
# check disk usage
curl "http://localhost:8080/api/v1/exec?cmd=df%20-h"
# view /proc/mounts
curl "http://localhost:8080/api/v1/exec?cmd=cat%20/proc/mounts"# check ownership of a specific file
curl "http://localhost:8080/api/v1/ownership/check?path=/mnt/nfs/test.txt"
# list all files with ownership info
curl "http://localhost:8080/api/v1/ownership/tree?path=/mnt/nfs"{
"path": "/mnt/nfs",
"test_dir": "nfsb-bench",
"cleanup": true,
"benchmark": "all",
"sizes": ["small", "medium", "large"],
"iterations": 100,
"concurrency": [1, 4, 8, 16],
"prometheus_port": 9090,
"no_warmup": false,
"read_only": false,
"preserve_test_files": false,
"runtime": "gvisor",
"storage_type": "nfs",
"run_id": "test-001"
}| Field | Default | Description |
|---|---|---|
path |
required | Path to benchmark directory |
test_dir |
null |
Create subdirectory within path (e.g., "nfsb-bench" creates /mnt/nfs/nfsb-bench) |
cleanup |
false |
Remove test directory after benchmarks complete |
benchmark |
all |
sequential, random, concurrent, metadata, mixed, append, all |
sizes |
["small","medium","large"] |
small (4KB), medium (1MB), large (100MB) |
iterations |
100 |
Number of iterations per test |
concurrency |
[1,4,8,16] |
Concurrency levels for concurrent tests |
prometheus_port |
9090 |
Port for metrics (0 to disable) |
no_warmup |
false |
Skip warmup phase |
read_only |
false |
Skip all write benchmarks (only needed for unpatched gVisor + NFS) |
preserve_test_files |
false |
Keep test files after benchmarks (for subsequent read-only tests) |
runtime |
auto-detected | Override runtime label: gvisor, runc, native |
storage_type |
auto-detected | Override storage type label: nfs, ephemeral, block |
run_id |
null |
Identifier for grouping results in Prometheus metrics |
nfsb run --path /mnt/nfs --output results.jsonnfsb run --path /mnt/nfs --benchmark sequential
nfsb run --path /mnt/nfs --benchmark random
nfsb run --path /mnt/nfs --benchmark concurrent
nfsb run --path /mnt/nfs --benchmark metadata
nfsb run --path /mnt/nfs --benchmark mixed
nfsb run --path /mnt/nfs --benchmark append# Test only small and medium files
nfsb run --path /mnt/nfs --sizes small,medium
# File sizes:
# small: 4KB
# medium: 1MB
# large: 100MBTest files are generated dynamically at runtime and cleaned up after each benchmark. No test files are stored in the repository.
| Size | Bytes | Example Filename |
|---|---|---|
| small | 4 KB | nfsb_seq_write_small.dat |
| medium | 1 MB | nfsb_seq_write_medium.dat |
| large | 100 MB | nfsb_seq_write_large.dat |
The benchmark flow:
- Random data is generated in memory
- Data is written to the target path (e.g.,
/mnt/nfs/nfsb_seq_write_small.dat) - Benchmark operations run (read/write iterations)
- Test file is deleted
This ensures:
- Fresh random data for each test (prevents filesystem caching tricks)
- Tests run on the actual target filesystem (NFS or ephemeral)
- No leftover files after benchmarks complete
nfsb run --path /mnt/nfs --iterations 200When writes are blocked (unpatched gVisor with NFS due to gVisor #11383), use read-only mode:
# Step 1: Create test files on a writable environment (e.g., runc or patched gVisor)
curl -X POST http://<writable-app>/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{
"path": "/mnt/nfs",
"test_dir": "nfsb-bench",
"preserve_test_files": true
}'
# Step 2: Run read-only benchmarks on unpatched gVisor environment
curl -X POST http://<gvisor-app>/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{
"path": "/mnt/nfs/nfsb-bench",
"read_only": true,
"benchmark": "all"
}'In read-only mode:
- All write benchmarks are skipped
- Warmup phase is skipped (warmup writes to test filesystem)
- Test files must already exist (created by a previous writable benchmark run)
Note: With patched gVisor (patches 0.1 + 0.2), read-only mode is not needed.
nfsb run --path /mnt/nfs --concurrency 1,2,4,8,16,32nfsb run --path /mnt/nfs --format jsonnfsb info --path /mnt/nfs
nfsb info --format jsonThe tool exposes metrics on port 9090 by default:
# Start with Prometheus metrics
nfsb run --path /mnt/nfs --prometheus-port 9090
# Disable Prometheus
nfsb run --path /mnt/nfs --prometheus-port 0Available metrics:
nfsb_bytes_written_total- Total bytes writtennfsb_bytes_read_total- Total bytes readnfsb_operations_total- Total operations performednfsb_throughput_mbps- Current throughput in MB/snfsb_iops- Current IOPSnfsb_operation_duration_seconds- Benchmark duration histogramnfsb_latency_seconds- I/O latency histogram
Start the monitoring stack:
docker-compose up -dAccess:
- Grafana: http://localhost:3000 (admin/admin)
- Prometheus: http://localhost:9091
{
"version": "0.1.0",
"timestamp": "2025-01-01T00:00:00Z",
"environment": {
"runtime": "gvisor",
"storage_type": "nfs",
"mount_point": "/mnt/nfs",
"filesystem": "nfs4"
},
"results": {
"sequential": [
{
"name": "sequential_write",
"size": "medium",
"iterations": 100,
"throughput_mbps": 150.5,
"latency_stats": {
"p50": 0.0012,
"p95": 0.0034,
"p99": 0.0081
}
}
]
}
}Run benchmarks in both environments and compare:
# In gVisor container
nfsb run --path /mnt/nfs --output gvisor-results.json
# In runc container
nfsb run --path /mnt/nfs --output runc-results.json# NFS mount
nfsb run --path /mnt/nfs --output nfs-results.json
# Ephemeral storage
nfsb run --path /tmp --output ephemeral-results.jsonnfsb run --path /mnt/nfs \
--benchmark concurrent \
--concurrency 1,2,4,8,16,32,64 \
--output concurrency-results.jsonThe tool automatically detects:
- Runtime: gVisor, runc, or native (bare metal)
- Storage Type: NFS or ephemeral
- System Info: CPU cores, memory
Detection methods:
/proc/versionfor gVisor signature/proc/1/cgroupfor container detection/proc/mountsfor filesystem type
| Benchmark | File | Entry Function |
|---|---|---|
| Sequential | src/benchmarks/sequential.rs |
run_sequential() |
| Random | src/benchmarks/random.rs |
run_random() |
| Concurrent | src/benchmarks/concurrent.rs |
run_concurrent() |
| Metadata | src/benchmarks/metadata.rs |
run_metadata() |
| Mixed | src/benchmarks/mixed.rs |
run_mixed() |
| Append | src/benchmarks/append.rs |
run_append() |
| Module | Description |
|---|---|
src/api/ |
REST API handlers and types |
src/benchmarks/ |
Benchmark implementations |
src/metrics/ |
Prometheus metrics collection |
src/report/ |
JSON report generation |
src/storage/ |
Environment and storage detection |
cargo testcargo check
cargo clippycargo fmtTesting NFS performance across different runtime environments to measure gVisor's overhead on NFS operations.
| Test | gVisor (patched) + NFS | gVisor + Ephemeral | runc + NFS | runc + Ephemeral |
|---|---|---|---|---|
| Sequential Read | ✓ | ✓ | ✓ | ✓ |
| Sequential Write | ✓ | ✓ | ✓ | ✓ |
| Random Read | ✓ | ✓ | ✓ | ✓ |
| Random Write | ✓ | ✓ | ✓ | ✓ |
| Concurrent I/O | ✓ | ✓ | ✓ | ✓ |
| Metadata Ops | ✓ | ✓ | ✓ | ✓ |
| Mixed Workload | ✓ | ✓ | ✓ | ✓ |
| Append | ✓ | ✓ | ✓ | ✓ |
Note: With patched gVisor (patches 0.1 + 0.2), all operations work. Unpatched gVisor blocks NFS writes due to gVisor #11383.
# run locally
make serve
# or with docker
make docker-runcurl http://localhost:8080/This returns current jobs, mounts, and API documentation.
# test NFS performance (if NFS is mounted)
curl -X POST http://localhost:8080/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{
"path": "/mnt/nfs",
"benchmark": "all",
"sizes": ["small", "medium", "large"],
"iterations": 100
}'
# save the job_id from response
JOB_ID="<job-id-from-response>"
# test local storage for comparison
curl -X POST http://localhost:8080/api/v1/benchmarks/run \
-H "Content-Type: application/json" \
-d '{
"path": "/tmp",
"benchmark": "all",
"sizes": ["small", "medium", "large"],
"iterations": 100
}'curl http://localhost:8080/api/v1/benchmarks/$JOB_ID/statuscurl http://localhost:8080/api/v1/benchmarks/$JOB_ID/results | jq .Key metrics to compare:
- Throughput (MB/s): Higher is better
- IOPS: Higher is better for random I/O
- Latency p50/p95/p99: Lower is better
| Scenario | Expected Impact |
|---|---|
| gVisor overhead | 10-50% slower than runc for file I/O |
| NFS vs Ephemeral | NFS typically 2-10x slower due to network |
| gVisor + NFS | Combined overhead - key metric for go/no-go |
- NFS read/write throughput > 50 MB/s
- Random IOPS > 1000
- p99 latency < 100ms
- No significant degradation under concurrent load
# verify NFS server connectivity
ping <nfs-ip>
# check if NFS mount point exists
ls -la /mnt/nfs
# check mount options
cat /proc/mounts | grep nfs# check job status
curl http://localhost:8080/api/v1/benchmarks/$JOB_ID/status
# check server logsIf running unpatched gVisor:
- NFS writes are blocked due to gVisor #11383
- Use
read_only: truemode or apply patches 0.1 + 0.2
If running patched gVisor:
- Check NFS export permissions on the server
- Verify the NFS share allows writes
This is expected behavior with patched gVisor due to NFS root_squash:
- gVisor runs as root
- NFS server squashes root to nobody (uid 65534)
- Files are created with nobody:nogroup ownership
- Applications can still read/write (permissions are 644/755)
# build docker image
make docker-build
# run container
make docker-run
# or manually
docker build -t nfsb .
docker run -p 8080:8080 nfsbdocker run -p 8080:8080 \
-v /mnt/nfs:/mnt/nfs \
--cap-add SYS_ADMIN \
nfsb- gVisor #11383 -
disable_file_handle_sharingNFS write issue - gVisor #575 - NFS root_squash fchown EPERM issue