Skip to content

feat(proxy): opt-in latency-aware upstream selection - #194

Merged
lexfrei merged 4 commits into
masterfrom
feat/latency-selection
Oct 9, 2026
Merged

lexfrei merged 4 commits into
masterfrom
feat/latency-selection

Conversation

@lexfrei

@lexfrei lexfrei commented Oct 9, 2026

Copy link
Copy Markdown
Owner

What

This adds an opt-in mode that sends new connections to the closest control plane endpoints. It is enabled with --upstream-selection=latency (env EP_UPSTREAM_SELECTION). The default stays random and picks exactly as before.

In latency mode, the connect time of every successful health check goes into a per-upstream EWMA. Upstreams are grouped into tiers by that average. A tier holds every remaining upstream whose average is at most max(best + 2ms, 2 × best). A new connection goes to a random healthy, non-draining upstream from the lowest tier that has one. Upstreams with no sample yet are picked together with that tier.

Why

Control plane nodes can sit in different countries. With random picking, a node that has an API server a fraction of a millisecond away still sends some of its new connections to one 50-100 ms away.

I use tiers and not "always the fastest" on purpose. In a single-site cluster all upstreams land in one tier, so it behaves like random and every node does not pile onto the same API server.

Behaviour worth knowing

  • Health does not move upstreams between tiers. An unhealthy or draining member keeps its tier, so losing part of a tier does not let a farther tier in. Only a tier with no pickable member hands over to the next one.
  • A member leaves its tier only when its average goes 25% over the limit. This hysteresis follows the group, not the tier number, so a tier appearing or disappearing above does not split a group.
  • A sample is capped at four times the current average, and never below 2ms. One retransmitted SYN moves the average by a bounded step and does not push a near upstream into a far tier for minutes.
  • A sample below a quarter of the average replaces it. So a slow first check is undone by the next normal one.
  • There are two new metrics, extractedprism_upstream_rtt_seconds and extractedprism_upstream_latency_tier. They exist only in latency mode and are deleted together with the upstream.
  • Existing connections are not moved.

Closes #190

Notes

  • proxy.New now takes *Config, because the new field pushed the struct over gocritic's hugeParam limit. The flag is wired through a new proxyConfig helper in internal/server.
  • For an endpoint given as a hostname, the connect time includes name resolution. This is documented.
  • A static endpoint that is dead for good keeps its last average and its tier. It can narrow the tier of the live upstreams: dead A at 1ms, live B at 2.5ms and C at 4ms puts C in the next tier. This is the price of keeping tiers independent of health, and it is tracked in proxy: stale RTT of a long-dead backend keeps shaping latency tiers #193.
  • .markdownlint.yaml comes in a separate chore commit. README prose is one line per paragraph, and the README opens with a centered logo block. markdownlint does not run in CI.
  • The chart value for the new flag will go into a separate lexfrei/charts PR after this is released.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Container image available

Multi-arch image (amd64 + arm64):

podman pull ttl.sh/extractedprism:pr-194-1d

Note: Image is rebuilt on every commit and expires after 24 hours (hosted on ttl.sh).

Latency-aware upstream selection needs to expose the smoothed connect
time it ranks upstreams by and the tier each upstream is grouped into.
Both series are removed together with the other per-upstream series
when an upstream is removed.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
Control plane nodes can sit in different sites, and uniform random
picking sends a share of new connections to an upstream tens of
milliseconds away while one under a millisecond away is available.

With latency selection enabled, successful health check dials feed their
connect time into a per-upstream EWMA. A sample is capped at four times
the current average, so one retransmitted SYN moves the average by a
bounded step instead of sending a near upstream into a far tier for
minutes, and a sample below a quarter of the average replaces it, so a
slow first sample is undone by the next normal one. Sampled upstreams
are grouped into tiers by latency alone: a tier holds every remaining
upstream whose average is at most max(best+2ms, 2*best), best being the
lowest remaining average. A hysteresis band keeps a member near the
boundary from flapping; it follows the group rather than the tier
number, so tiers appearing or disappearing above do not split a group.

Because health does not move upstreams between tiers, the tiers change
only with new samples and with the endpoint set. New connections are
picked at random among the pickable upstreams of the lowest tier that
has one, plus pickable upstreams without a sample, so unhealthy or
draining members of a tier do not let a lower tier in, and a tier
without a pickable member hands over to the next one. Random selection
keeps its pick semantics and no latency state.

proxy.New now takes the config by pointer: the new field pushed the
struct over gocritic's hugeParam limit.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
Expose latency-aware upstream selection as an opt-in mode. The default
stays random; any value other than random or latency fails validation.

Closes: #190
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
Prose in this repository is written one line per paragraph and left to
the renderer to wrap; hard-wrapping it breaks narrow viewports and list
rendering. MD013 flags every such paragraph, so it is turned off rather
than satisfied.

The README opens with a centered logo block, which needs inline <p> and
<img> and cannot start with a heading: MD033 allows those two elements
and MD041 is off.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
@lexfrei
lexfrei force-pushed the feat/latency-selection branch from e412972 to d2d7d8e Compare October 9, 2026 16:18
@lexfrei
lexfrei merged commit 9750d07 into master Oct 9, 2026
8 checks passed
@lexfrei
lexfrei deleted the feat/latency-selection branch October 9, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(proxy): opt-in latency-aware upstream selection

1 participant