Skip to content

feat(ebpf): Pick an egress shard per tenant address in usid_egress - #835

Merged
privateip merged 1 commit into
mainfrom
feat/egress-aa-datapath
Oct 12, 2026
Merged

privateip merged 1 commit into
mainfrom
feat/egress-aa-datapath

Conversation

@privateip

@privateip privateip commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Each tenant VRF's egress route names one egress shard, so a compute node sends all of its metro's egress to the first shard that resolves and keeps the others idle. Losing that shard drops every session in the metro.

This is the datapath half of active/active egress, the first of three stacked PRs. An egress_route_table entry can now name a shard group instead of a shard. usid_egress hashes the tenant's source address (or the 5-tuple), follows a live pin or else the group's Maglev table, and encapsulates toward the chosen shard as it does today.

  • Each group is one immutable snapshot (Maglev table plus every member's SID, next hop, flags and slot generation), swapped into an ARRAY_OF_MAPS slot in one update. A packet sees the old snapshot or the new one, never a mix: 0 mixed packets of 122,312 across 400 concurrent swaps, against 17 of 54,777 when a published value was rewritten in place.
  • A pin is honoured only while its slot carries the generation it recorded, so a reused slot never inherits another shard's pins, even after a publication that failed half way.
  • There is one group per translation class: NAT66, and up to three NAT64 prefixes.
  • An empty or disabled group drops the packet and counts it, rather than failing open.

Nothing reads a group until something publishes one, which only the next PR's hashed mode does. With every group disabled, CNI ADD and repair behave as before. All the maps are new and no existing map changes layout, so upgrades keep every pinned row. Datapath memory grows from about 59Mi to 67Mi, inside galactic-cni's 128Mi limit.

Stack

  1. This PR: datapath and map API
  2. feat(cni): Spread egress across all EgressShards in hashed mode #836: hashed mode in galactic-cni (needs feat: Add drain to EgressShard network#32 released)
  3. docs(egress): Document hashed egress with alerts and a lab check #837: docs, alerts, rollout checklist and lab check

The feature's review and re-review are in the third PR. Three re-review findings are still open and block enabling hashed mode. Two of them touch this PR's code: CNI ADD skips a NAT64 prefix that no shard serves, so it falls through to the NAT66 route (R03), and closing the groups has no barrier against an in-flight ADD (R01, repaired within one sweep). Neither affects ordered mode, the default.

Test plan

  • Build, lint (0 issues) and unit tests pass on this commit alone
  • Every root-gated package passes as root, including the BPF_PROG_RUN placement, publication-fault and concurrent-swap tests (go test -race -p 1)
  • Datapath tests pass with clang-18 objects (CI's compiler); generation checked with clang-19 (the image builder's)
  • CI's privileged job passes

Running the root-gated packages in parallel, as task test:unit-root does, can fail internal/cnibgp and internal/cni tests that collide on host interfaces. Run one at a time they pass, here and on main.

🤖 Generated with Claude Code

A tenant VRF's egress route names exactly one shard, so every compute node sends a metro's egress to the first shard that resolves and keeps the rest as standby. Losing that shard loses every session in the metro, and the other shards' capacity sits idle.

An egress_route_table entry can now name a shard group instead: link_ifindex 0xFFFFFFFF, the group ID in the first four bytes of sid and the tenant's Argument where a SID carries it. usid_egress hashes the tenant's source address (or, per group, the 5-tuple) with FNV-1a, follows a live pin in egress_shard_pin or else the group's 1021-slot Maglev table, completes the chosen shard's base SID with the Argument and encapsulates exactly as for an ordinary entry. An empty or disabled group drops the packet and counts it in egress_shard_stats rather than failing open into the main table.

Each group is one immutable snapshot (Maglev table plus every member's SID, next hop, flags and slot generation) in a one-entry inner map of egress_shard_groups, an ARRAY_OF_MAPS. egressroutemap publishes a change by creating a new inner map, filling it and swapping the outer slot in one update, so a packet sees the old snapshot or the new one, never a mix. Updating a published value in place produced 17 torn packets out of 54,777 on a kernel; the swap produced 0 out of 122,312 across 400 concurrent swaps. A pin is honoured only while its slot still carries the generation the pin recorded. Generations come from next_slot_generation in the published snapshot plus an in-process high-water mark that also counts failed publications, so a reused slot never inherits another shard's pins, even after a publication that failed half way. egress_shard_group_template, an unused instance of the inner map type, makes clang before 21 emit the value's full BTF.

There is one group per translation class: group 0 for NAT66 and groups 1-3 for up to three NAT64 prefixes. CNI ADD (attachreg.installVRFEgress) and the datapath repair route each class to its group when the group is open and has a reachable shard, and otherwise keep today's ordered path. A group marked CLOSING still forwards but takes no new routes, and ADD re-reads the groups after writing and falls back to the ordered install if they closed meanwhile. Refresh, given WithShardGroup, moves shard-bound entries onto the groups and, without it, moves group routes back to the first resolvable shard of the static list, returning every error.

Nothing changes until something publishes a group: with every group disabled, ADD and repair behave as before. Every piece is a new map and no existing map changes layout, so attach.Load reuses every pinned row on upgrade; it removes the pins of two group maps that only pre-release lab builds created. The maps grow the datapath from about 59Mi to 67Mi, inside galactic-cni's 128Mi limit.

The privileged tests run usid_egress under BPF_PROG_RUN against maps the Go side published and check every placement against ShardHash and internal/maglev: the spread, a failure moving only its shard's tenants, pins through added and draining shards, slot reuse, the drop paths, flow hashing with fragments, IPv4, publication faults at each step, and concurrent swaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@privateip
privateip merged commit 5dbea29 into main Oct 12, 2026
15 checks passed
@privateip
privateip deleted the feat/egress-aa-datapath branch October 12, 2026 16:02
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