Skip to content

(13) dpdk, dataplane: RSS, port counters, worker lcores and datapath fixes - #1892

Open
daniel-noland wants to merge 12 commits into
pr/daniel-noland/dpdk-hugepagesfrom
pr/daniel-noland/dataplane-perf
Open

daniel-noland wants to merge 12 commits into
pr/daniel-noland/dpdk-hugepagesfrom
pr/daniel-noland/dataplane-perf

Conversation

@daniel-noland

@daniel-noland daniel-noland commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

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=N to both drivers, and raise moved datapath interfaces. (Multicast delivery to the control plane now comes from #1928, which marks LLDP and IPv6 neighbor discovery Local.) 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.

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: b536f3a7-a62e-4ef8-a82d-aac24c7d52df

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@daniel-noland daniel-noland mentioned this pull request Oct 4, 2026
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch 2 times, most recently from f9d6186 to c617d4f Compare October 6, 2026 03:49
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from 76fd1ef to 83229f1 Compare October 6, 2026 04:05
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 3 times, most recently from d420126 to f4e8ced Compare October 7, 2026 03:09
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch 2 times, most recently from d6b451e to cada229 Compare October 8, 2026 00:41
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 2 times, most recently from 1c6c248 to cba901d Compare October 8, 2026 01:53
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch from 1822fbd to df57db5 Compare October 8, 2026 03:32
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from cba901d to f071e76 Compare October 8, 2026 03:32
@daniel-noland daniel-noland self-assigned this Oct 8, 2026
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch from df57db5 to bfc06df Compare October 8, 2026 07:16
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from f071e76 to 65a9eae Compare October 8, 2026 07:16
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 2 times, most recently from aed90ca to 6c9cf2a Compare October 16, 2026 08:43
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch from 4de7fe0 to 2ac368c Compare October 16, 2026 08:43
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 2 times, most recently from de95cd8 to 68605fa Compare October 17, 2026 05:59
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch 2 times, most recently from 2bdcfc3 to f41e8c7 Compare October 17, 2026 06:17
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from 68605fa to 8321a55 Compare October 17, 2026 06:17
@daniel-noland
daniel-noland marked this pull request as ready for review October 17, 2026 21:51
@daniel-noland
daniel-noland requested a review from a team as a code owner October 17, 2026 21:51
@daniel-noland
daniel-noland requested review from sergeymatov and removed request for a team October 17, 2026 21:51

@mvachhar mvachhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread args/src/lib.rs
let mtu = value
.parse::<u16>()
.map_err(|e| format!("Bad mtu '{value}': {e}"))?;
if mtu < 68 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread dataplane/src/drivers/dpdk/port.rs Outdated

// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 as rx_nombuf because 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from 8a76f6d to ec53220 Compare October 19, 2026 20:27
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch 2 times, most recently from 4f5bf45 to f1f6877 Compare October 19, 2026 20:36
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch 2 times, most recently from 80ab0dc to 9fee41c Compare October 19, 2026 21:15
daniel-noland and others added 12 commits October 19, 2026 17:08
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>
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dpdk-hugepages branch from 542b810 to e6bb09c Compare October 19, 2026 23:37
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/dataplane-perf branch from 9fee41c to 4093345 Compare October 19, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants