feat(volumes): pluggable distributed storage backend for high-performance cross-sandbox file sharing
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Problem
The current persistent volume implementation backs every volume with a plain host directory and serves it to sandboxes over NFS v3 (packages/orchestrator/pkg/nfsproxy/). This works for sequential large-file access, but has hard limits for real workloads:
| Workload | Current behaviour |
|---|---|
| Large-file sequential read/write | Acceptable, but adds NFS round-trip latency |
| Many small files (node_modules, Python packages, build trees) | Poor — every stat/open/readdir is a separate NFS RPC |
| High-concurrency writes from multiple sandboxes | No kernel-side cache; lock contention on the NFS server goroutine |
| Sandboxes on different orchestrator nodes sharing a volume | Not possible — volume lives on a single node's local filesystem |
The last point is the most critical: as a fleet scales to multiple nodes, sandboxes scheduled on different nodes cannot share persistent volumes at all.
Proposed solution
Introduce a pluggable storage backend interface for persistent volumes, so the NFS chroot layer can be backed by any filesystem that satisfies a simple contract, while the default (local directory) is kept as-is.
Interface sketch
// packages/orchestrator/pkg/volumes/backend/backend.go
type Backend interface {
// CreateVolume provisions storage for the given volume ID.
CreateVolume(ctx context.Context, teamID, volumeID uuid.UUID) error
// DeleteVolume removes all data for the volume.
DeleteVolume(ctx context.Context, teamID, volumeID uuid.UUID) error
// RootPath returns the host-side absolute path that the NFS chroot
// (or future virtiofs passthrough) should expose to sandboxes.
// The path must be accessible on every orchestrator node.
RootPath(teamID, volumeID uuid.UUID) string
}
The existing volumes/volume_create.go + chrooted/builder.go path already calls os.MkdirAll(fullPath) — swapping fullPath to come from a Backend.RootPath() call is the only change needed in the hot path.
Target backends
JuiceFS (preferred first target)
- POSIX-compliant FUSE filesystem backed by object storage (S3, GCS, R2) + Redis/TiKV for metadata
- Single binary, no cluster to operate — easy to self-host
- Metadata latency for small files is ~1 ms (Redis) vs ~10 ms+ (NFS RPC over loopback)
- Mount once on the host at
/mnt/juicefs/<team>/<vol>; sandbox chroot sees it as a normal directory - Handles millions of small files well (Python envs, npm packages, build caches)
Ceph (CephFS)
- Kernel or FUSE client; fits teams already running Ceph
- Better for large-file, high-throughput workloads (ML checkpoints, video assets)
- Requires a Ceph cluster, so higher operational overhead
Generic mountpoint backend
- Simplest: operator pre-mounts any filesystem at a configured root (NFS, SMB, Lustre, Weka, etc.) and points the backend at it
- Zero code beyond reading the configured path
Configuration
# orchestrator environment / config
volumes:
backend: juicefs # "local" | "juicefs" | "cephfs" | "mountpoint"
juicefs:
meta_url: "redis://redis:6379/1"
storage: s3
bucket: s3://my-bucket/e2b-volumes
access_key: ...
secret_key: ...
mountpoint:
root: /mnt/shared-volumes # pre-mounted by operator
Why this matters
- Cross-node volume sharing: sandboxes on any node can read/write the same volume — essential for stateful multi-step workflows (agent pipelines, CI builds, data processing)
- Small-file performance: JuiceFS with a Redis metadata store is 5–20× faster than NFS for workloads dominated by
stat/openon many small files - Durability: object-storage backends survive orchestrator node failure; local-directory volumes do not
- No API change: the
POST /volumes+VolumeMountsAPI surface stays identical; the backend is an operator concern
Scope
- Define
Backendinterface inpackages/orchestrator/pkg/volumes/backend/ - Refactor
volumes/volume_create.goandchrooted/builder.goto consumeBackend.RootPath()instead of constructing paths directly - Implement
LocalBackend(wraps currentos.MkdirAllbehaviour, default) - Implement
MountpointBackend(operator-managed pre-mount, zero deps) - Document JuiceFS and CephFS as supported external backends with example configs
- Integration test: two sandboxes on the same node share a volume through a
MountpointBackendbacked by a tmpfs
Out of scope for this issue: virtiofs transport improvement, cross-team volume sharing.
References
- Current NFS proxy:
packages/orchestrator/pkg/nfsproxy/ - Chroot builder:
packages/orchestrator/pkg/chrooted/builder.go - Volume create:
packages/orchestrator/pkg/volumes/volume_create.go - Config model:
packages/orchestrator/pkg/cfg/model.go(PersistentVolumeMounts) - JuiceFS architecture
- CephFS
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading packages/orchestrator/pkg/volumes/volume_create.go, packages/orchestrator/pkg/chrooted/builder.go, packages/orchestrator/pkg/cfg/model.go, and the current NFS proxy under packages/orchestrator/pkg/nfsproxy/. Define the backend boundary and verify how volume paths and configuration flow through the existing code. Done means LocalBackend and MountpointBackend work, external backend configuration is documented, and the two-sandbox tmpfs integration test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, distributed-systems, infrastructure
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100