Skip to content

Add configurable affinity_mode (spread) for egress pod selection [port #1209] - #1

Open
Harishkrishna17 wants to merge 5 commits into
mainfrom
feat/affinity-mode-spread
Open

Add configurable affinity_mode (spread) for egress pod selection [port #1209]#1
Harishkrishna17 wants to merge 5 commits into
mainfrom
feat/affinity-mode-spread

Conversation

@Harishkrishna17

Copy link
Copy Markdown

Ports upstream livekit#1209 (affinity_mode) into the Talview fork — the spread scorer validated live on VR prod (the v1.13.0-tv1 image, SRE-4121).

Why

For a track-only egress fleet, upstream's default consolidate/pack scorer packs many light track recorders onto one pod → CPU saturation → dropped session callbacks (VR saw 9–12 recorders on a 4-vCPU pod). affinity_mode: spread scores by available-CPU ratio and distributes evenly.

Config

affinity_mode: spread   # "pack" (default, upstream behaviour) | "spread"

Commits (cherry-picked from livekit#1209, -x trailer preserved)

  • Add configurable affinity_mode for egress pod selection
  • fix(spread): pending-counter + jitter to fix burst skew
  • fix(spread): idle pod returns 1.0 affinity to trigger ShortCircuitTimeout
  • feat: add soft_reject_floor to prevent all-pods-silent drops
  • revert: remove unused type_aware changes and CI workflow

Deliberately excluded

Upstream livekit#1209 also carries a later feat: add Redis heartbeat for CapacityManager integration commit — that is a separate concern (CapacityManager), not the spread fix, and was not part of what we validated in prod. Left out to keep this PR focused on spread. Raise it separately if the fork wants CapacityManager.

Verification

Cherry-picks applied cleanly onto main. pkg/config, pkg/server, pkg/stats compile clean. Full go test needs gstreamer/cgo (CI/Docker only) — not run locally. This is the livekit#1209 half of the two spread options; livekit#1248 (load_distribution) is the sibling PR.

abhisheksingh-R41 and others added 5 commits July 13, 2026 07:52
The current StartEgressAffinity always scores idle pods at 0.5 and busy
pods at 1.0. Combined with MaximumAffinity=1 in the psrpc client, this
means the first pod to accept any job wins all subsequent jobs until its
CPU budget is exhausted — a staircase pattern rather than even spread.

The existing code comment acknowledges this is intentional for mixed
fleets ("avoids having many instances with one track request each, taking
availability from room composite"). However for a TrackEgress-only fleet
the packing strategy provides no benefit and causes sequential scale-out
delays.

This commit adds a configurable affinity_mode field to ServiceConfig:

  pack (default)  — current behaviour, unchanged
  spread          — CPU-proportional scoring; idle pods score 1.0 and
                    win immediately via MaximumAffinity short-circuit,
                    busy pods score proportionally so the least-loaded
                    pod wins after ShortCircuitTimeout. Best for
                    single-type (TrackEgress-only) fleets.
  type_aware      — RoomComposite/Web requests prefer idle pods (1.0
                    idle / 0.5 busy); Track/Participant requests spread
                    by CPU load. Best of both worlds for mixed fleets.

Default is "pack" so all existing deployments are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit e3ab476)
…e_aware modes

Addresses both root causes of the 24/51-job-on-one-pod skew observed in
the 2026-05-07 load test:

Cause A (strict > tie-break): psrpc's ShortCircuitTimeout means the first
replier wins when all idle pods return the same score. Fixed by subtracting
rand.Float32()*0.001 jitter so idle pods produce distinct scores, making
the strict-> comparison effectively random among equally-idle peers.

Cause B (m.requests.Inc lag): StartEgressAffinity is called before
StartEgress, so the winning pod's m.requests counter stays 0 across an
entire 200ms burst window and all callers see score 1.0. Fixed by a
pendingClaims atomic.Int32 that increments at affinity time and
decrements at StartEgress accept (consumePendingClaim). A 2s self-decay
timer guards against claims that are never fulfilled. A CAS loop in
consumePendingClaim ensures exactly one decrement fires per increment
even when StartEgress and the timer race.

New monitor helper AvailableCPUFractionWithPending deducts
pendingSlots*TrackCpuCost from the available budget so the score
decreases with each in-flight claim.

Image: asia-south1-docker.pkg.dev/avian-pulsar-430509-f6/r41-livekit/egress:v1.12.0-r41.2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit ded32ad)
…eout

In spread mode, an idle pod computed AvailableCPUFraction = 2.4/4.0 = 0.6.
Since psrpc's MaximumAffinity is 1.0, the ShortCircuitTimeout (500ms fast
path) never fired — every dispatch waited the full AffinityTimeout even when
idle pods were ready.

An idle pod (activeRequests=0, pendingClaims<=1) now returns 1.0 plus tiny
jitter, triggering the 500ms short-circuit. Busy pods still return a
proportional fraction.

Also fix jitter direction bug in type_aware heavy-request path:
1.0 - jitter landed below MaximumAffinity; changed to 1.0 + jitter.

(cherry picked from commit d2fc658)
When all egress pods are simultaneously over their CPU budget (Chrome cold-start
storm), every pod returns -1 and the dispatcher gets zero bids. The job is
permanently dropped.

New config fields soft_reject_floor (float, default 0) and max_active_requests
(int, default 0) allow a pod to return a small positive score instead of -1
when CanAcceptRequest is false but the pod is below its design capacity. The
dispatcher can then select this pod as a last resort instead of dropping the job.

Guarded by MaxActiveRequests so genuinely full pods still hard-reject.

Also add unit tests for softRejectScore helper.

(cherry picked from commit acf8a3d)
type_aware mode is not used in production (affinity_mode: spread in both
config blocks). Reverts idle-1.0 and jitter-direction changes in that branch
to keep the diff minimal and production-relevant only.

build-push-ar.yml removed — images are built and pushed manually.

(cherry picked from commit 3c41258)
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