fix(server): zero-copy in-memory artifact cache to kill PXE-boot starvation - #36
Merged
Merged
Conversation
…reads
Under concurrent PXE boot, N machines pull the same large artifact (modloop
~295 MiB, initramfs, vmlinuz, apkovl) at once. read_file_as_stream served each
from disk independently, so N concurrent clients re-read the same file N times
off one PVE-backed store (loop0 over vm-1000-disk-0.raw) — disk throughput
collapsed and stalled every consumer. The intended single-flight pullthrough
(stream_download_with_caching) is dead code (0 fires; the iPXE script points
clients at /boot/{arch}/* local files, not /ipxe-artifacts), so it never
deduped the reads.
Add ArtifactCache (on AppState): load each file into memory ONCE into a shared
Bytes; every concurrent client streams Bytes::slice zero-copy views of that one
allocation (refcounted — CoW-on-write, never written) in 64 KiB chunks.
Population is single-flight (per-path tokio::Mutex, double-checked) so the cold
read happens once and concurrent readers await then slice the shared buffer —
no duplicate loads, no torn reads. Bounded LRU (default 1 GiB,
DRAGONFLY_ARTIFACT_CACHE_BYTES); files over the cap stream from disk unchanged.
read_file_as_stream now serves from the cache when AppState is available and
the file fits (early-return on Cached, fall through to the existing disk path
on TooLarge/error). This also unifies the range + full branches for cached
files: range requests slice the shared buffer instead of read_to_end into a
per-request Vec — the "well-designed range" the per-request buffering was
supposed to be. Wired serve_boot_asset (already passed Some(state)),
serve_cached_image, and serve_os_image (signature + the /os route closures) to
reach the cache.
Tests (artifact_cache, 5): load-once-then-memory, single-flight (16 concurrent
callers share one allocation — proven by Bytes pointer equality), zero-copy
slice, LRU eviction, oversized-not-cached. dragonfly-server lib 198/198 green;
fmt clean; no new clippy.
Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Under concurrent PXE boot, N machines pull the same large boot assets (modloop
~295 MiB, initramfs, vmlinuz, apkovl — served from
/boot/{arch}/*local files)at once.
read_file_as_streamserved each from disk independently, so N clientsre-read the same file N times off one PVE-backed store (
loop0overvm-1000-disk-0.raw). Disk throughput collapsed and stalled every consumer — abimodal 2-fast (~87s) / 3-slow (~280s) straggler pattern across the 5-node
test-k8s fleet.
Root cause
Investigation traced every serve path. The "scary" ones turned out to be dead
code (0 fires across months of journal):
stream_download_with_caching— a no-single-flight pullthrough proxy with theexact N×-re-fetch flaw, but the iPXE script (
api.rs:2575-2587) points clientsat
/boot/{arch}/*local files, not the/ipxe-artifactsproxy.read_to_end-into-a-Vecbuffering (0 RANGE_READ).AppState.client_ip(never written),track_download_progress(gated off).The live culprit was the local-file path itself: lock-free and correct per
request, but re-reading the same artifact from disk once per concurrent client.
Fix
ArtifactCacheonAppState(new moduleartifact_cache.rs):Bytes. Population issingle-flight (per-path
tokio::Mutex+ double-check): the cold readhappens once; concurrent readers await, then slice the shared buffer (no
duplicate loads, no torn reads).
Bytes::slicezero-copy views of that one allocation(refcounted — CoW-on-write, never written) in 64 KiB chunks. One disk read, N
independent seeks.
DRAGONFLY_ARTIFACT_CACHE_BYTES). Files overthe cap return
TooLargeand the caller streams them from disk unchanged.read_file_as_streamserves from the cache whenAppStateis available andthe file fits (early-return on
Cached, disk fallback onTooLarge/error).This also unifies the range + full branches for cached files: range
requests slice the shared buffer instead of
read_to_endinto a per-requestVec.serve_boot_asset(the live/boot/{arch}/*path),serve_cached_image,and
serve_os_image.Verification
Same 5-box boot storm, before → after deploying the fix:
/metrics)The lone 112s straggler is the single cold cache load (modloop's first disk
read, the 368 ms max in
/metrics); every box after serves from RAM. RSS 366MiB with the cache warm, well under the 1 GiB cap.
Tests
5 new
artifact_cachetests: load-once-then-memory, single-flight (16concurrent callers share one allocation — proven by
Bytespointer equality),zero-copy slice, LRU eviction, oversized-not-cached.
dragonfly-serverlib198/198 green; fmt clean; no new clippy warnings.
Notes
a boot storm (assets are immutable mid-storm); a re-staged
modloopneeds arestart to be picked up.
mainautomatically when that merges.🤖 Generated with Claude Code