feat: use node-local overlayfs rootfs cache to eliminate per-restore untar (#228)#283
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
2b2efc3 to
970a309
Compare
|
I'd like to hold this one momentarily while we land #123 (one of our POC2 / alpha milestone requirements), as we need to make it work there too and there's already a lot in flight. I agree that we should do something roughly like this though, and appreciate the PR. |
|
Note: if you rebase on main we have uVM now (including e2e tests), also FYI #346. uVM is already mounting the image rootfs ro with a tmpfs upper to handle writes, similar to gVisor. I'm not sure we need the overlay on the atelet side vs just caching the unpacked images. We would need overlay if we started handling image layer sharing but that has other complexity. |
Implements issue agent-substrate#228: cache extracted rootfs per image digest on each node, and materialize per-actor bundles as overlayfs mounts instead of re-untarring on every restore. Changes: - New rootfscache package (cmd/atelet/internal/rootfscache) - New overlay.go with mount/unmount helpers - Modified prepareOCIDirectory for overlayfs integration - Updated resetActorDirs to unmount before cleanup - Added rootfs cache paths to ateompath - Unit tests for cache hit/miss, concurrent access, eviction
EnsureRootfs took an io.Reader that oci.go always populated via pullCache.Fetch before the call, so every resume pulled and extracted the image into the memory pull cache even when the on-disk rootfs cache already had it. Change the parameter to a lazy tar provider closure that EnsureRootfs invokes only on a cache miss; on a hit it returns the on-disk lowerDir without pulling or extracting anything.
d44ee35 to
db0e31b
Compare
atelet runs with all capabilities dropped and can no longer call mount(2), so it cannot perform the overlayfs rootfs mount itself. On a rootfs-cache hit atelet now writes a per-bundle "overlay-lower" marker recording the read-only lowerdir; the privileged ateom worker reads the marker and mounts the overlay just before `runsc create`, in ateom's mount namespace (shared by the runsc child, invisible to atelet). - ateompath: add ContainerRootfsDir/OverlayUpperDir/OverlayWorkDir/ OverlayLowerMarkerFile so both sides agree on paths by convention. - atelet: write the marker instead of mounting; drop unmountActorRootfs and the reset-time unmount loop (mounts live in ateom's ns). - ateom: mount overlays in Run/Restore (with failure-path unmount) and unmount in Checkpoint so a long-lived ateom does not leak mounts.
Two leak/idempotency gaps in the overlay rootfs lifecycle: - CheckpointWorkload unmounted only after the checkpoint calls, so an early-return checkpoint failure skipped the unmount and leaked the mount into a long-lived ateom. Move the unmount to a defer registered before the checkpoint so it runs on every exit path. - mountOverlayRootfsIfRequested now best-effort unmounts the target before mounting, mirroring setupActorNetwork's stale-network cleanup. This makes re-mount idempotent (no stacked overlays on actor reuse) and closes the window from any prior leaked mount at the same target.
LRU eviction could os.RemoveAll a lowerdir currently backing a live actor's overlayfs mount, which the kernel forbids modifying and would corrupt the running actor. atelet restarts independently of the actors whose overlays live in ateom's mount namespace, so an in-memory refcount is not restart-safe. Add a restart-safe, on-disk in-use signal: overlayLowerInUse scans the per-container overlay-lower bundle markers (written before ateom mounts, removed only at teardown) into the set of lowerdirs in use, injected into the cache via the new WithInUseFunc functional option. evictIfNeeded and EvictLRU skip pinned entries and, on provider error, skip the pass entirely (exceeding the disk budget is safe; deleting a live lowerdir is not).
Only the untar path needs to pre-create the identity bind target: runsc resolves the destination against the extracted image there. On the overlay path the pre-created dir is shadowed by ateom's later overlay mount, and runsc auto-creates the missing target into the upperdir anyway.
Two rootfs-cache improvements: - Trigger one eviction pass in New (after loadIndex) so a node that boots already over budget reclaims disk immediately, rather than waiting for the next cache miss. Fixes a steady-state, all-hits node never shrinking. - Untar now returns the total regular-file bytes written, and extract uses it to size the cache entry, dropping the extra full-tree dirSize walk on the miss path. Hardlinks are no longer double-counted.
…ailure The overlay rootfs mount is performed by the privileged ateom worker after atelet has already returned from bundle preparation, so atelet cannot fall back in-band; it only learns of a mount failure through the RPC error. Introduce internal/overlayfallback as the cross-process contract: ateom wraps mount failures as a FailedPrecondition status carrying a marker that survives the server interceptor's status rebuild, and atelet recognizes it, re-prepares the bundles with a plain untar (clearing the overlay-lower marker), and retries the RPC once. This guarantees a working rootfs even when the overlay mount fails, addressing R5 (ensure fallback to untar on mount failure).
Add --rootfs-cache-max-bytes to atelet, defaulting to rootfscache.DefaultMaxCacheBytes (20 GiB), replacing the hardcoded 0. Behavior is unchanged by default; operators can now tune the node-local overlayfs rootfs cache budget without a rebuild.
Summary
Implements #228: cache one extracted, read-only rootfs per immutable image digest on each node, and materialize each actor's bundle as a thin overlayfs mount instead of re-untarring the whole image on every restore.
Problem
Every
Restorecall in atelet fully reconstructs the rootfs by pulling and untarring the OCI image — even when the same image digest has already been extracted on the same node many times before. Observed cost:prepareOCIDirectory(untar rootfs): ~15–20srunsc restore(checkpoint restore): ~268msRootfs extraction dominates resume latency by ~99%.
Solution
Change the scaling behavior from "every restore pays extraction cost" to "first restore of a digest on a node pays extraction; later restores pay only an overlay mount."
On first use of an image digest on a node:
/var/lib/ateom-gvisor/rootfs-cache/<sha256>/lower/On every restore using the same digest:
RemoveAll+untar, set up an overlayfs mount for the actor bundle's rootfs:lowerdir= the cached, read-only extracted rootfs (shared, never mutated)upperdir+workdir= per-actor, actor-private writable layersChanges
internal/ateompath/ateompath.goRootfsCacheDirandRootfsCacheLowerDir()cmd/atelet/internal/rootfscache/rootfscache.goEnsureRootfs,Untar,ValidateTarName, LRU eviction, concurrent dedupcmd/atelet/internal/rootfscache/rootfscache_test.gocmd/atelet/overlay.goisOverlayfsAvailable()cmd/atelet/oci.goprepareOCIDirectoryintegrates overlayfs path with untar fallback;extractDigestFromRef;unmountActorRootfscmd/atelet/main.goAteomHerder,resetActorDirsadds unmount before cleanupKey Design Decisions
inflightEntrydedup — N goroutines requesting the same digest only trigger 1 untar.readysentinel file;loadIndexauto-cleans partial entries from previous crashes.last_accesstimestamp, async trigger, 20GB default capresetActorDirsdoesMNT_DETACHunmount on overlayfs rootfs beforeRemoveAllExpected Impact
Testing
go test ./cmd/atelet/...)Open Questions (for follow-up)