Skip to content

benchmarking: add the egress-tunnels workload - #2716

Merged
Benjamin Elder (BenTheElder) merged 6 commits into
agent-substrate:mainfrom
ygao-g:egress-tunnels-workload
Oct 26, 2026
Merged

Benjamin Elder (BenTheElder) merged 6 commits into
agent-substrate:mainfrom
ygao-g:egress-tunnels-workload

Conversation

@ygao-g

@ygao-g Yuan Gao (ygao-g) commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #2715

First of the PRs that measure how many CONNECT tunnels the egress gateway can hold and what each costs. This PR adds benchmark-egress, one binary with two subcommands: actor, which runs inside each Actor and holds one tunnel per endpoint through the gateway, and target, the Deployment behind the endpoint Services. The driver, the deploy script and the orchestrator entry follow in later PRs.

Design and details: https://github.com/ygao-g/substrate/tree/egress-tunnels-workload/cmd/benchmarking/egress. The README there is the design doc: what the benchmark measures (tunnel, blended, settled), the topology, the traffic shape over a run, what the actor does and reports, what a run computes, and the decisions with the alternatives they displaced. More specifically,

  • Pacing. The gateway's stream idle timeout is 5 minutes by default, so every tunnel has to stay busy. With C endpoints the actor runs one goroutine per endpoint: endpoint i opens i intervals after /start, then each is requested once per period of C × intervalMs.
  • Report. The actor records its four latency distributions as OTel histograms on a private MeterProvider behind a manual reader in delta temporality, and serves them as window cuts: POST /collect?window=N answers the counters and the distributions since the previous cut, each distribution as the fields of an OTel histogram data point with the bounds in the payload, so the driver merges across actors with Point.Merge and refuses an image that buckets differently. The actor caches the last cut by id: a lost answer is asked for again and costs a retry, not a window; a skipped or older id is 409. GET /stats serves the cumulative counters for a driver's gates. POST /stop is the last cut.
  • Target. One Deployment behind every endpoint Service, one backend port per Service. For HTTPS it serves a certificate from a throwaway CA name-constrained to the endpoint domain.

Overview of Commits.

  1. the binary: Cobra root, no subcommands yet
  2. the contract: the routes and the /start fields, the Stats counters, Point with Merge and Quantile, the shared bounds, the collect route and window id, protocol version 2
  3. the name-constrained CA and certificate the target serves
  4. the actor subcommand: the loop, the recorder, /stats, /collect with window ids, /stop, the protocol header, classify
  5. the target subcommand
  6. the README

Testing

Unit tests cover the window ids (first cut is 1, the last id answers again byte for byte, the next id cuts, anything else is 409 with the next id named), that a repeated /collect consumes nothing and the sample between two answers lands in the next window, the cut invariant under concurrent reads (each window's samples equal the growth of successes since the previous cut), that a /collect racing a /stop is never a 500, Merge refusing differing bounds by index, and the payload budget at C = 1024 (/stats under 1 KiB, a cut under 12 KiB).

Ran on GKE at 250 × 1000 and 1000 × 1000 HTTPS endpoints through one gateway replica, with the actor serving cuts: 0 errors and 0 redials in the settled window, every actor answering every round, 6.0 M settled requests at 10,070 req/s on 8.13 gateway cores at 1000 × 1000, and p99 within one bucket step of the earlier exposition-based run. The target served 1000 Services on the port range from one image.

Prepared with help from an AI coding agent; reviewed and verified by the author.

@ygao-g Yuan Gao (ygao-g) added area/benchmarking kind/feature An enhancement / feature request or implementation labels Oct 15, 2026
@ygao-g
Yuan Gao (ygao-g) marked this pull request as draft October 15, 2026 19:28
@ygao-g
Yuan Gao (ygao-g) force-pushed the egress-tunnels-workload branch 2 times, most recently from c2054a9 to 9cc5d9f Compare October 15, 2026 22:21
@ygao-g
Yuan Gao (ygao-g) marked this pull request as ready for review October 15, 2026 22:33
Comment thread cmd/benchmarking/egress-actor/main.go Resolved

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thx Yuan, defering to Bowei Du (@bowei) on this one. My 2c is why do we need to add more actors, cant we reuse images of existing ones?

@ygao-g
Yuan Gao (ygao-g) force-pushed the egress-tunnels-workload branch 4 times, most recently from e9156ff to 8c66e38 Compare October 16, 2026 21:14
@ygao-g
Yuan Gao (ygao-g) force-pushed the egress-tunnels-workload branch 4 times, most recently from f7d637b to f27c177 Compare October 19, 2026 22:45
// Package egresstunnels is shared by the egress-tunnels driver, actor and
// target: the routes the actor serves, the request that starts its loop, the
// stats it reports, the endpoint names, and the target's certificate.
package egresstunnels

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should keep all of the benchmarking code into the cmd/benchmarking/... package instead of at the top level

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.

Done. The package now lives at cmd/benchmarking/internal/egresstunnels. PTAL

@bowei

Copy link
Copy Markdown
Collaborator

Consolidate this into one executable to reduce the number of executables we need to build. Use Cobra to create subcommands.

Use this package structure:

cmd/benchmarking/egress/main.go
cmd/benchmarking/egress/README.md

Use Cobra flags to create two subcommands for the executable:

benchmark-egress actor and benchmark-egress target.

Not clear why we need to split off into a separate package. Can we keep everything in cmd/benchmarking/egress/xxx.go?

Make sure you unexport all of the symbols.

// Histogram counts durations in fixed log-spaced buckets. Every Histogram
// shares the same buckets, so histograms from many actors merge exactly.
// The zero value is empty and ready to use.
type Histogram struct {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we reuse the otel.metric data structures vs creating our own?

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.

Done. Each measurement is an OTel instrument. The OTel Prometheus exporter serves them on GET /metrics. The driver sums the scrapes per le label and reads the quantiles from the sum.

One part stays in histogram.go: the bucket bounds. All actors must use the same bounds, so the scrapes add bucket by bucket. The SDK does not give fixed log-spaced bounds, and math.Pow differs across architectures. The code rounds each bound once to four significant digits. README §4 lists the families.

@@ -0,0 +1,67 @@
# egresstunnels

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we work on this README?

Rewrite this readme with the following sections.

  1. The goal of what the egress benchmark tests
  2. General topology of the setup, seems like you run benchmark-egress actor inside the Actor and benchmark-egress target for the traffic sink.
  3. Workflow that the Actor runs.
  4. Metrics that you are collecting from the benchmark Actor.
  5. What metrics are aggregated and will be summarized by running the experiment.

After rewriting the readme, you should structure your code etc to match the above description.

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.

Done. The README has these five sections:

  1. What the benchmark measures: the goal, the terms tunnel, blended and settled, and the costs a cell reports.
  2. Topology: driver, benchmark-egress actor in the Actor, gateway, endpoint Services, and benchmark-egress target as the backend.
  3. What the actor does: the routes, the /start body, and the per-endpoint loop.
  4. What the actor reports: each family on /metrics, what it counts, and what it is used for.
  5. What a run aggregates: the merge rules, the two windows, and how each result is computed.

@ygao-g
Yuan Gao (ygao-g) force-pushed the egress-tunnels-workload branch 2 times, most recently from bbcbdbe to fd05f73 Compare October 20, 2026 21:19

@bowei Bowei Du (bowei) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks ok.

This is benchmarking code.

The egress scale test holds many CONNECT tunnels open through the egress
gateway at a fixed pace and measures what they cost. Its programs are
subcommands of one binary, so there is one image to build and ship. This
is the Cobra root; the subcommands follow.
The actor's contract is what the actor, the target and the driver agree
on: the routes and the /start body, the endpoint names the loop calls
and the egress policy allows, and the Stats the actor answers with. It
opens package actor; the loop follows in its own commit. The Stats is
JSON: plain counters, cumulative since /start and cheap enough to poll,
and on a cut the four latency distributions, opening, latency, dns and
tls, each as a Point, the fields of an OTel histogram data point over
one window. The bounds, 20 per decade from 1 us to 63.1 s out of a
mantissa table, travel in every Point, so a merge across actors refuses
a point from an image that buckets differently; Merge and Quantile give
a driver the fleet's quantiles to one bucket step and an exact maximum.

The contract is plain JSON, so a driver decodes a report with the
standard library alone.
targetcert issues what an HTTPS run trusts: a throwaway CA that is
name-constrained to the endpoint domain, and the certificate for the
endpoint Services that the target serves. The constraint keeps a
certificate the driver hands out from vouching for anything but the
test's own Services. The actor's HTTPS tests verify against it.
The actor is the workload inside the sandbox. On POST /start it runs
one goroutine per endpoint: endpoint i opens its tunnel i intervals
after the start, then requests the endpoint once per period of C
intervals, so the actor sends one request per interval and a request
that outlasts the period delays only its own tunnel. Each request
updates the counters under one lock and records its latencies into
four OTel histograms on a private MeterProvider behind a manual reader
in delta temporality. GET /stats serves the counters. POST
/collect?window=N cuts a window, the counters and the distributions
since the previous cut, under the same lock, and answers the last cut
again for the same id, so a lost answer costs the driver a retry rather
than a window. POST /stop ends the loop and serves the tail window with
the final counters. The loop starts on request rather than at boot
because every actor restores from a golden snapshot taken before any
egress policy exists.

Every answer carries Benchmark-Egress-Protocol: 2, so a driver can
refuse an actor whose image speaks another contract instead of
misreading its report. With it come classify's low-cardinality error
classes.
The target is the endpoint every tunnel leads to: a 200 with a small fixed
body over plain HTTP and, given a certificate, HTTPS. One Deployment sits
behind all of the test's Services, each on its own backend port, so two
Services on distinct ClusterIPs cannot hand it two connections with the
same 4-tuple.
The README is the design the driver and the result reader hold the
workload to: what the benchmark measures, the topology, what the actor
does on each route and in its loop, the fleet's traffic shape over a
run, what a cut holds, how a run merges the cuts into results, and the
decisions behind each piece with the alternatives they displaced. Facts
only a maintainer of the code needs stay as comments in the source.
@ygao-g
Yuan Gao (ygao-g) force-pushed the egress-tunnels-workload branch 2 times, most recently from 0c19343 to 5540993 Compare October 24, 2026 19:08

@bowei Bowei Du (bowei) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved.

Perf testing related code.

@ygao-g

Copy link
Copy Markdown
Collaborator Author

Approved.

Perf testing related code.

Benjamin Elder (@BenTheElder) Julian Gutierrez Oschmann (@juli4n) PTAL and add it to the merge queue if you see fit.

@BenTheElder

Copy link
Copy Markdown
Collaborator

all benchmarking is fine, added to queue.

@BenTheElder
Benjamin Elder (BenTheElder) added this pull request to the merge queue Oct 26, 2026
Merged via the queue into agent-substrate:main with commit ffed855 Oct 26, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/benchmarking kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants