fix(minvmd): coalesce libkrun vsock reads instead of emitting sub-skb packets - #921
Conversation
📝 WalkthroughWalkthroughThe libkrun Unix vsock receive loop now coalesces partial reads after ChangesVsock receive coalescing
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
| @@ -0,0 +1,107 @@ | |||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | |||
There was a problem hiding this comment.
Do we want to name this 0001-... as we removed the other patches?
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@vendor/libkrun/patches/0003-vsock-coalesce-sub-skb.patch`:
- Around line 102-104: Update the coalescing wait loop around waits and poll to
handle poll results: retry EINTR without incrementing waits, while routing all
other poll errors through the existing error path. Increment waits only after a
successful or non-interrupted poll, preserving the coalescing budget when
signals interrupt polling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: da85891d-b656-4f25-a2b2-b4a74aeff47e
📒 Files selected for processing (1)
vendor/libkrun/patches/0003-vsock-coalesce-sub-skb.patch
… packets The unix-vsock proxy issues one recv() per RX descriptor and emits whatever that single read found buffered. Against a bulk sender that hands the stream over in small chunks, the muxer drains the socket faster than the writer fills it, so descriptors carry a fraction of their capacity and the stream fragments into sub-KiB packets while the peer's window is wide open. Linux charges every queued packet SKB_TRUESIZE(0) against buf_alloc regardless of payload -- 576 bytes on arm64 -- so a stream whose mean packet falls below that exhausts the receiver's queue budget long before its byte budget. At a 256 KiB window the queue caps at 455 packets, and since 6.12.92 the receiver resets the connection (ENOBUFS) on the 456th rather than dropping it. That is the failure behind bulk host->guest uploads. Fill the descriptor rather than emit one recv()'s worth, and -- because draining alone does not help when the writer is slower than we are across wakeups -- wait briefly on EAGAIN while still below a packet worth carrying. poll() returns as soon as more data lands, so a fast writer pays nothing, a slow one costs at most 4 ms, and data is always delivered rather than held. Instrumented on both the guest rejection path and libkrun's emit path: 86% of sub-576-byte packets came from the fill loop exiting on EAGAIN with descriptor space and credit both free, not from the credit clamp (14%, tail only) and never from the descriptor size (constant 3732 B). A/B with libkrun the only variable: 0 of 10 uploads completed without this, 16 of 16 with it, and the instrumented kernel logged no rejections. This is a single self-contained patch against the pinned libkrun commit, numbered 0001 because #923 removed the earlier fill-loop patches from the tree. It subsumes their fill-loop mechanics; it does not re-carry the separate signal-used-queue credit-request fix, which is worth its own PR. Refs: #869
0096dc7 to
7eedbf9
Compare
|
Rebased onto current
No behavioural change from the approved version — same emitted code, same measurements. |
Carries a single libkrun patch on the macOS source build: fill the RX descriptor and, when the socket drains below a packet worth carrying, wait briefly on EAGAIN rather than emit a vsock packet too small to carry its own receive-queue accounting.
macOS only. Linux needs the equivalent through
gominimal/pkgs(pkgs#506, unmerged).Why the shipped fix is not enough
Current
unstable(159f8839) carries the fill loop from #884 and still fails. Confirmed on Tom's machine and reproduced here — including on the real repo, a 141 MB compressed upload.The fill loop stops the stream fragmenting when libkrun outruns the writer inside one wakeup. It does nothing when the writer is slower across wakeups: each notification finds a few hundred bytes buffered, the loop drains them, hits EAGAIN, and ships a packet a fraction of the descriptor's size.
Linux charges every queued packet
SKB_TRUESIZE(0)againstbuf_allocregardless of payload — 576 bytes on arm64 — so a stream whose mean packet falls below that exhausts the receiver's queue budget long before its byte budget. At the 256 KiB default window the queue caps at 455 packets, and since 6.12.92 ("vsock/virtio: reset connection on receiving queue overflow") the receiver answers the 456th with a reset (ENOBUFS) instead of a drop.Where the small packets come from — measured in the VMM
An instrumented
recv_to_pktcounted, per emitted packet, the descriptor size, credit, and emitted length across 86,016 packets on the failing stack:That is the single lever, and it is exactly where this patch inserts a bounded
poll()wait. It also explains why anything that slows the host path makes the bug vanish (a client sleep,RUST_LOG=debug,MINVMD_KRUN_LOG=debug) whiletokio::task::yield_now()does not: it needs wall-clock time for bytes to accumulate, not scheduler fairness.The guest-kernel side agrees: every reset was the skb-depth branch, never the credit branch,
rx_qlenexactly 455,buf_alloc262144.Measurement
libkrun the only variable — same host, same VMM binary, same installed guest, same fixture.
This patch, built in isolation (the exact single patch this PR carries, nothing else) against the current shipped unpatched dylib:
94e9d72dunstable)3b26aeb1Earlier three-way run (before #923 removed the old patches from the tree), fill loop as the baseline:
db6d26295ddaf39f01282c7eThe credit floor is refuted twice — once against stock, once on top of the fill loop. It should not be proposed again.
What changed since approval
#923 removed the old
0001/0002libkrun patches from the tree (to test a client-side buffer in isolation). So this patch is now the first and only carried patch, and has been consolidated from a delta-on-the-fill-loop into a single self-contained patch (0001-vsock-coalesce-sub-skb.patch) that applies directly to the pinned libkrun commit728df812. Verified: itgit applys cleanly to stock and produces a tree byte-identical to the16/0build above. No behavioural change from the approved version — same emitted code.It does not re-carry the separate signal-used-queue credit-request hang fix that #923 also removed; that is independent and worth its own PR.
On the client-side buffer (#923)
#923's 4 KiB
BufWriteraround the upload does not help — measured 0/6, with a socket write histogram identical to unbuffered. russh already batches at the transport, our writes average ~2.5 KB, and the fragmentation is created inside libkrun downstream of anything the client does. The fix has to live in the VMM.Cost
poll()returns as soon as more data lands, so a fast writer pays nothing. A slow one costs at mostCOALESCE_ROUNDS× 1 ms = 4 ms per descriptor, and data is always delivered rather than held, so no stream can stall on this.Upstreaming
Coalescing changes latency for every unix-backed vsock port, not just bulk uploads. Tom's read is that this "basically implements nagle's algorithm", which would break any consumer expecting
send()to be 1:1 withrecv()— not us (our protocol is stream-oriented), but worth putting behind an option if we upstream to containers/libkrun. Carrying it locally needs no such decision.Companion PRs
159f8839, pre-revert(minimald): restore the 8 MiB guest vsock receive window #922; measuredbuf_alloc= 256 KiB), so all the failures above are against the true default baseline.macOS CI exercises this patch — #910 folded
vendor/libkrun/patches/**into thesetup-libkrun-macoscache key, so adding or changing a patch invalidates the cache and forces a rebuild.Refs: #869