io_uring: re-enable GSO batching (MAX_GSO_SEGMENTS_PER_SEND=1 makes UDP_SEGMENT a no-op) #17
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
femboy/yip#17
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The
UringDriver's GSO egress path is effectively disabled by a one-line constant, which makes it more expensive than a plain send while delivering none of the batching benefit. This is the primary reason the io_uring driver regressed tunnel RTT and was demoted from the default in #16.The bug
crates/yip-io/src/uring.rs:min(1, 64) = 1, soclamp(_, 1)returns 1 regardless ofmtu_cap.queue_udp_batchthen doesdatagrams.chunks(1)— one datagram per "GSO batch". Every multi-datagram egress that should coalesce into a singlesendmsg+UDP_SEGMENTcmsg instead issues oneSendMsgper datagram, each paying the extra iovec/msghdr/cmsg construction (GsoSendContext::prepare,CMSG_*, plus aVec::with_capacitycopy) — strictly more expensive than the plainSendpath it replaces, for zero batching.This directly matches the bench profile: the pipeline emits ~2 same-size symbols per sealed packet (source + repair), exactly the case GSO exists to coalesce — and instead it issues two full
SendMsg+cmsg calls.Also: the unit test
uring_gso_large_batch_chunks_payload_to_udp_limitscomputes itsexpected_submissionsby calling the same (buggy)max_gso_datagrams_for_segmentunder test — it's tautological and cannot catch this regression. Fix the test to use an independent expected value.Why it was capped to 1
Deliberate, per the Phase B task report:
MAX_GSO_SEGMENTS_PER_SEND=1was set to stoparq_recovers_bulk_lossfailing — coalesced GSO super-frames over veth+netem drop as a unit, so one drop loses many segments and defeats the loss test. So raising the cap is not a one-liner: it must be re-validated against the ARQ loss test.Plan
MAX_GSO_SEGMENTS_PER_SENDto a real batch size (e.g. 8–64) and confirmmax_gso_datagrams_for_segmentscales withmtu_cap.arq_recovers_bulk_lossinteraction. Options: bound GSO batch size on lossy paths, keep GSO for clean-link bulk only, or accept the segmentation behaviour if netns loss no longer regresses. Understand why coalescing hurt the test (veth GSO/GRO offload keeping the super-frame intact through netem).uring_gso_large_batch_chunks_payload_to_udp_limits(independent expected value).run-driver-ab-rtt.sh+ throughput A/B. Only if uring beats epoll does it earn the default back (tunnel.rsselection).Related (out of scope here, documented in crates/yip-bench/README.md)
Vecalloc+copy on every recv and send (poll.rs uses a reused stack buffer).SEND_SLOTS=256shared pool silently drops on exhaustion vs poll's 4 MiBSO_SNDBUF.IORING_SETUP_SQPOLL.Context: review + demotion in #16; measured regression in
crates/yip-bench/README.md("io_uring Phase B — driver A/B").Mechanism confirmed empirically
Bumped
MAX_GSO_SEGMENTS_PER_SEND1 → 8 and ranarq_recovers_bulk_lossunderYIP_USE_URING=1:Root cause pinned down: GSO is applied only on the TUN→UDP egress path (
handle_dispatch_tun→queue_udp_batch(pkts, allow_gso=true)). That batch is exactly one inner packet's symbols = source + its repair symbols, all the same size, socan_coalesce_gsomerges them into a singleUDP_SEGMENTskb. The driver can't tell source from repair (opaqueVec<u8>). Over veth the super-skb isn't segmented beforenetem, so one drop takes the source and its repair together → FEC can't recover. That's why the cap was pinned to 1: it silently disables the feature to keep FEC intact.Design conclusion: you cannot safely coalesce symbols within one object. A correct GSO batch must span different objects (independent w.r.t. loss — each object's repair rides a different skb). That means accumulating same-size datagrams across multiple
on_tundispatches and flushing them as GSO batches at end ofpoll_once, not the current per-packet coalescing. Bigger change than raising a constant.Important scoping caveat: GSO batching is a throughput lever, not a latency one. The RTT regression that got uring demoted (#16) is a single-flow ping-pong where there is little to batch; it comes from io_uring's per-op cost + per-packet heap copies, not GSO. So fixing GSO will not on its own earn uring the default back — that likely needs
IORING_SETUP_SQPOLLand/or killing the per-packet malloc. GSO and RTT are separate workstreams.Concrete design (why this is a cross-crate change, not a driver tweak)
Deeper than the first analysis: a GSO skb fate-shares every segment in it under netem (confirmed: cap=8 → ~95% delivery, FEC can't recover). Since an object's source and repair symbols are the same size, they are always GSO-coalescing candidates — so cross-object accumulation alone doesn't fix it either: append order
[o1_src, o1_rep, o2_src, o2_rep]still putso1_src+o1_repin one skb.For GSO to coexist with FEC, no GSO skb may contain two datagrams from the same object. The driver can't enforce that because egress datagrams are opaque
Vec<u8>— it can't tell source from repair or which object a symbol belongs to.Proposed design (needs a small yip-transport → yip-io API addition):
on_tunreturns each egress datagram tagged with a fate group (object id, or simply source-vs-repair + object index). Cheap: the transport already knows this at encode time.poll_onceegress accumulator. It coalesces same-size datagrams into GSO skbs under the invariant at most one datagram per fate group per skb (e.g. skb A = one source symbol from each of N objects; skb B = the matching repair symbols). Flush at end ofpoll_once.uring_gso_large_batch_chunks_payload_to_udp_limits; add a netns test that asserts N-object coalescing keeps delivery ≥ threshold under loss.Alternative (simpler, less general): gate GSO to the zero-repair regime only — when the controller has decayed repair to 0 (clean-link Bulk, the existing bypass), there is no repair to fate-share, so coalescing source symbols across packets is safe. Needs only a boolean "no redundancy in flight" signal from the transport, not full fate tags. Loses GSO on lossy links (acceptable — those are FEC-bound, not syscall-bound).
Either way this touches the FEC egress contract, so it wants a short design pass before implementation — not a solo constant change. Holding here rather than shipping a subtly FEC-defeating coalescer.
Reminder: none of this addresses the RTT regression (separate: SQPOLL + per-packet malloc).