Repository navigation
(13) dpdk, dataplane: RSS, port counters, worker lcores and datapath fixes - #1892
daniel-noland wants to merge 12 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
f9d6186 to
c617d4f
Compare
76fd1ef to
83229f1
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
d420126 to
f4e8ced
Compare
d6b451e to
cada229
Compare
1c6c248 to
cba901d
Compare
1822fbd to
df57db5
Compare
cba901d to
f071e76
Compare
df57db5 to
bfc06df
Compare
f071e76 to
65a9eae
Compare
aed90ca to
6c9cf2a
Compare
4de7fe0 to
2ac368c
Compare
de95cd8 to
68605fa
Compare
2bdcfc3 to
f41e8c7
Compare
68605fa to
8321a55
Compare
8321a55 to
8a76f6d
Compare
mvachhar
left a comment
There was a problem hiding this comment.
Note
AI-assisted review: This review was drafted with Claude and reviewed by @mvachhar.
Requesting changes for one blocker: with /mtu now configurable, jumbo MTUs silently truncate control-plane frames through the tap (comment on args/src/lib.rs). The pool-sizing comment on port.rs isn't a blocker at the defaults, but please reply to it either way: fix here, or a follow-up.
| let mtu = value | ||
| .parse::<u16>() | ||
| .map_err(|e| format!("Bad mtu '{value}': {e}"))?; | ||
| if mtu < 68 { |
There was a problem hiding this comment.
Note
AI-assisted review: This comment was drafted with Claude and reviewed by @mvachhar.
Jumbo MTUs silently truncate control-plane frames. This needs fixing before DPDK ships.
/mtu=N has no upper bound here, the DPDK port takes that MTU, and #1929's dress_one copies it onto the port's tap. But the tap pump still reads into a fixed vec![0u8; TapDevice::MAX_FRAME] of 9216 bytes (dataplane/src/drivers/cpbridge.rs, interface-manager/src/interface/tap.rs). MTU excludes the Ethernet header on both DPDK and Linux, so with /mtu=9216 (common on fabric underlays) the kernel builds frames of up to 9230 bytes, 9234 with a VLAN tag. read(2) on a tap truncates to the buffer and reports success, and the pump injects the truncated frame onto the wire with no error and no counter.
The visible failure is BGP: the kernel derives the session's MSS from the tap MTU, so large UPDATEs go out as truncated, corrupt segments. The peer drops them, TCP retransmits at the same size, and the session stalls or flaps. Before this PR every port ran at 1500, so this couldn't happen; /mtu is what makes it reachable.
Simplest fix: reject /mtu above what the tap path can carry, TapDevice::MAX_FRAME - 18 (9198), with an error that says why. Alternatively, size the pump's buffer from the port MTU, or clamp the tap MTU in dress_one and log it. Either way, please make the limit and the units explicit in MAX_FRAME's doc ("L2 frame, excluding FCS").
There was a problem hiding this comment.
Fixed one PR down, in #1927 (547eedd430). TapDevice::MAX_FRAME is now the largest frame a tap can produce: its largest MTU (65535) plus an ethernet header and a VLAN tag. Its doc says so, in those units. read takes a &mut [u8; MAX_FRAME], so the pump can't pass a buffer that truncates. A VM test sends a full-size frame at MTU 9216 through a tap and reads it back whole; it fails with the old 9216-byte constant. So /mtu needs no cap for the tap's sake. The device still clamps it to its own range.
|
|
||
| // Checked: `/rxd` times the worker count can pass `u32` for absurd worker counts, and a | ||
| // wrapped product would make a uselessly small pool rather than an error. | ||
| let pool_mbufs = POOL_MBUFS_PER_RX_DESCRIPTOR |
There was a problem hiding this comment.
Note
AI-assisted review: This comment was drafted with Claude and reviewed by @mvachhar.
The pool size ignores TX rings, so a dead output link can starve a healthy input port. Not a blocker at the defaults, but it becomes one as soon as /rxd is lowered or there are more ports.
4 × rxd × workers covers the RX ring, but a port's RX pool also backs everything it forwards. A packet keeps its original mbuf, so port A's mbufs sit in the TX ring of whichever port the packet leaves on until that NIC completes the descriptor. Per worker, port A's pool can be held by:
| Holder | Up to |
|---|---|
| A's RX ring | rxd |
| one burst in the pipeline | 32 |
| this lcore's mempool cache | 256 |
| TX rings of every output port | TX_DESCRIPTORS (1024) × ports |
When an output port B loses link, its TX rings stop completing but rte_eth_tx_burst keeps accepting until they fill, so up to 1024 of A's mbufs per worker are stranded until B comes back:
- At the default (
rxd = 1024, 4096 per worker) A still has about 1,760 spare after its RX ring, a burst, its cache and the 1024 stranded in B, which is fine. - At
/rxd=256(1024 per worker), B's rings can absorb A's whole pool. A can't refill its RX ring, and everything arriving on it, BGP and BFD included, is dropped asrx_nombufbecause a different link went down. - Even at the default, the worst case passes 4096 per worker from three ports up.
Could the pool be sized from what actually holds mbufs, e.g. workers × (rxd + MBUF_BURST + cache_size + TX_DESCRIPTORS × ports) plus a small margin for injected control frames? Since this changes how many hugepages the pools need, it'd be good if #1884's per-node hugepage requirement came from the same numbers rather than being a separate fixed figure.
There was a problem hiding this comment.
Fixed here, sized from what actually holds mbufs, as you suggest (5b50504273). A port's receive pool now holds, per worker, its receive ring, two bursts (one in the pipeline, one being injected), its cache, and a full transmit ring on every port (args::dpdk_memory::pool_mbufs_per_worker). A dead link can strand at most its rings' worth, so it can't starve another port's receive ring, at any /rxd. #1884's hugepage requirement now comes from the same function, per node and using each interface's /rxd, so the reservation and the pools can't disagree. At the defaults on the BF-3 gateway (two ports, 32 workers) that's 3,456 mbufs per worker instead of 4,096, and the requirement comes to just under 3 GiB.
8a76f6d to
ec53220
Compare
4f5bf45 to
f1f6877
Compare
80ab0dc to
9fee41c
Compare
Register workers to enable per-lcore mempool caches, with a thread-bound RAII token that unregisters on drop. Duplicate registration is rejected; failed registration leaves the worker using the shared pool ring. Support registered workers in NUMA lookups and warn if any remain during EAL teardown. Use OS-thread locks for DPDK resources and reject EAL initialization under model checkers, which cannot model its C state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Enable RSS over supported IP, TCP and UDP hash types using the driver's default key, so traffic reaches multiple workers. Validate requested hash types and warn when RSS is unavailable. Hash reporting into mbufs stays off until something reads it. Shared flow state lets any worker handle either direction, including NAT's translated reverse tuples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Export eight cumulative device counters per port, including RX descriptor and mbuf shortages that pipeline counters cannot observe. The scoped supervisor polls devices and publishes owned snapshots for metrics. Register series once, publish absolute totals, and log each continuous read failure once. Tests cover values, labels and repeated publication. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Raise moved datapath interfaces and loopback, with typed errors. Report DPDK carrier state once every port has started, giving autonegotiation up to five seconds before warning that a link is down. Use ifindices because devlink reload may rename devices. Expose control, datapath and host namespaces through symlinks to init's retained descriptors. A fresh private /run/netns preserves inherited names and supports ip netns exec without binding namespace lifetimes to mounts. A VM test checks cross-process entry, both directory aliases, inherited name preservation and failed lookup after the descriptor closes. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Carry /mtu=N from the interface argument into DPDK port configuration and kernel netlink setup. Read back the kernel MTU before reporting it to the control-plane bridge. Omitted values preserve existing driver defaults; configuration failures stop startup. Ports and kernel interfaces are matched to their own arguments rather than by position. Validate argument parsing and use a VM test to verify MTU application, preservation when omitted, bridge identity, and kernel rejection. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Receive directly into a worker-owned MbufArray and drain it in place, avoiding pointer-array copies on every poll. Reuse the pipeline Vec and bulk-free any mbufs left before refilling. Keep one transmit batch per port instead of building a map on every burst. Keep receive as a convenience wrapper around receive_into. Tests cover refill reclamation and drain ownership; occupancy checks do not detect double frees without DPDK debug checks. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Accept /rxd=N alongside /mtu=N in either order. Fit the receive descriptor count to the device's minimum, maximum and alignment, and scale pool capacity with the ring depth and worker count using checked arithmetic. A pool that cannot be allocated reports its size and how to shrink it. Keep the default at 1024 descriptors and the fixed 12,188-byte mbuf data room. Tests cover suffix parsing, composition, invalid counts and descriptor fitting. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Prefetch two cache lines per packet, covering VXLAN's inner IPv4 header. Use temporal hints because serialization writes the headers again. Throughput benefit remains unmeasured. Use wrapping pointer arithmetic for hints past small or trimmed buffers. This adds no length reads or bounds checks and produces identical optimized x86-64 instructions to the original implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
…bufs A port's receive pool was 4 x rxd x workers, which covers its receive ring but not everything else that holds the pool's mbufs: a packet keeps its mbuf until the NIC that transmits it completes the descriptor, so the transmit rings of every output port hold them too. When an output link dies its rings stop completing, and up to a ring's worth per worker is stranded; at /rxd=256 that is the whole pool, so a dead link elsewhere starved this port's receive ring, BGP and BFD included. The pool now holds, per worker, its receive ring, two bursts (one in the pipeline, one being injected), its cache, and a full transmit ring on every port (args::dpdk_memory::pool_mbufs_per_worker). Init's hugepage requirement comes from the same function, per node and with each interface's /rxd, so the reservation and the pools cannot disagree. At the defaults with two ports and 32 workers that is 3,456 mbufs per worker rather than 4,096. Reported-by: mvachhar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
…heckers Registering workers as lcores made `eal::init` refuse to run under shuttle and loom, which cannot model DPDK's OS-thread-local state. Eight model tests still started the EAL; three of them ran under shuttle and panicked, and all eight would under loom. Skip them under the model checkers, as the tests that cannot run there already are; the stress backend still runs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`metrics` keeps the handle, so it must be a real `std::sync::Arc`. The counter recorder used the facade's, which is loom's under `--features loom`, so the loom build failed. The gauge recorder already used `process_global::Arc`; do the same for counters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
542b810 to
e6bb09c
Compare
9fee41c to
4093345
Compare
Enable RSS across workers and per-lcore mempool caches, export device counters, and reuse receive/pipeline burst storage. Add configurable receive ring depth and packet-header prefetching.
Apply
/mtu=Nto both drivers, and raise moved datapath interfaces. (Multicast delivery to the control plane now comes from #1928, which marks LLDP and IPv6 neighbor discoveryLocal.) Keep the fixed 12,188-byte mbuf data room. Expose operator namespace names through symlinks to init's retained descriptors, so the names cannot strand NIC namespaces after exit.Validation: Clippy across all five affected crates; native DPDK, args, init and stats tests; DPDK doctests; focused dataplane regressions; VM tests for namespace visibility/lifetime and kernel MTU application. Header-prefetch pointer correction produces identical optimized x86-64 instructions. Hardware throughput and physical-NIC restart behavior were not remeasured during review.
Stacked on #1884. Replaces #1824, which GitHub marked merged during the stack re-cut; earlier review discussion remains there.