Skip to content

benchmarking: add Nighthawk router-capacity benchmark - #998

Open
Chuang Wang (chuangw6) wants to merge 1 commit into
agent-substrate:mainfrom
chuangw6:nighthawk-router-capacity
Open

benchmarking: add Nighthawk router-capacity benchmark#998
Chuang Wang (chuangw6) wants to merge 1 commit into
agent-substrate:mainfrom
chuangw6:nighthawk-router-capacity

Conversation

@chuangw6

@chuangw6 Chuang Wang (chuangw6) commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #665

Measures the max RPS atenet-router sustains at a given Envoy CPU limit, under a tail-latency SLO, using Nighthawk's adaptive load controller in open-loop mode against the real routing path (Host-header routing via ext_proc to warmed glutton actors). See benchmarking/nighthawk/README.md for design, knobs, and usage.

  • benchmarking/nighthawk/: runner Job that creates and warms the actor fleet, drives nighthawk_service + nighthawk_adaptive_load_client with Host rotation across actors, and uploads JSON/JSONL results to GCS.
  • Search converges on three thresholds — tail latency (measured mean+2σ must stay under tailLatencySloMs), success-rate, and send-rate — and records which one bounded the run. The client is oversized (fixed event loops, large pools) so the harness is never the ceiling.
  • orchestrator.py: new type: nighthawk tests.yaml entries (default locust, back-compatible); pins the router (cpu requests=limits, envoy --concurrency) before each run.
  • run-dev.sh: one-command dev runs (~8 min) against a prepared cluster.

Validated end to end on a dev GKE cluster: ~9.4k RPS at 2 Envoy CPUs under a 25 ms tail-latency SLO (latency mean+2σ ≤ 25 ms, ≈p95; Nighthawk's search cannot gate on true percentiles).

Measures the max RPS atenet-router sustains at a given Envoy CPU limit,
under a tail-latency SLO, using Nighthawk's adaptive load controller in
open-loop mode against the real routing path (Host-header routing via
ext_proc to warmed glutton actors).

- benchmarking/nighthawk/: runner Job that creates and warms the actor
  fleet, drives nighthawk_service + nighthawk_adaptive_load_client with
  Host rotation across actors, and uploads JSON/JSONL results to GCS.
- Search converges on three thresholds — tail latency (measured
  mean+2stdev must stay under tailLatencySloMs), success-rate, and
  send-rate — and records which one bounded the run. The client is
  oversized (fixed event loops, large pools) so the harness is never
  the ceiling.
- orchestrator.py: new `type: nighthawk` tests.yaml entries; pins the
  router (cpu requests=limits, envoy --concurrency) before each run.

Validated end to end on a dev GKE cluster: ~8.9k RPS at 2 Envoy CPUs
under a 25ms tail-latency SLO.

def parse_output_textproto(text: str, desc_path: str) -> dict:
# Deferred imports: the transforms below must work without protobuf.
from google.protobuf import json_format

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.

what does this mean?

@bowei

Copy link
Copy Markdown
Collaborator

we might want to name this to make it clear this is measuring the Ingress side. We also need a separate benchmark for Actor Egress.

@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.

Comments, mostly for clarification.

runs `hack/install-microvm-deps.sh --install` between the two, which
stages kata + cloud-hypervisor + virtiofsd assets to the cluster's object
store bucket and applies the cluster-wide `microvm` SandboxConfig.
For a `nighthawk` test the orchestrator additionally patches the

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.

why does the test run have to patch the Deployment?

# See the License for the specific language governing permissions and
# limitations under the License.

# Generates nighthawk.desc: one FileDescriptorSet covering Nighthawk's

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.

Probably change this to

"Generate nighthawk.desc to avoid putting a large number of
dependencies into the repository for running the benchmark"

ATEAPI_HOST = "api.ate-system.svc.cluster.local:443"
TOKEN_FILE = "/run/ateapi-token/token"

# Shared atespace (locust-harness convention); sb-<uuid> names can't collide.

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.

I don't understand this comment. Is the atespace named with sb- or just called benchmark

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.

FWIW the "sb-" convention was just taken from counter demo back in the day. Actors can have whatever name makes sense.

TOKEN_FILE = "/run/ateapi-token/token"

# Shared atespace (locust-harness convention); sb-<uuid> names can't collide.
ATESPACE = "benchmark"

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.

Should benchmarks use uniquely named atespaces?

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.

I think this is an interesting discussion. From what I remember from the summit, atespaces were expected to be fairly low cardinality. Unsure if that is still the intent, or what the performance implications of violating that guideline are.



def _warm_one(
stub, router_url: str, name: str, atespace: str, deadline: float, log

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.

put a docstring describing what this is meant to do

I think I know what "_warm_one" is intended to do, but can be unclear.

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.

probably should be called warm_actor?

SANDBOX_CLASSES = ("gvisor", "microvm")


def validate_and_normalize_tests(tests: list[dict[str, Any]]) -> None:

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.

I get the feeling nighthawk won't be the last time we have different types of performance tests. Can you split nighthawk into its own file and we can try to make these things more pluggable?

I'm thinking:

  • A registry function that adds subcommands and flags for those subcommands (think kubectl ate get actors)
  • A lifecycle object that provides hooks for the test to hook into (pre-test, test, post-test)
  • Try to find common library functions.

Though we'd need to see how well that holds up to scrutiny.

Keeping it in a separate file we can start to tease out the primitives with the goal of moving to separate packages per test-type.

I don't want to put you on the hook for refactoring orchestrator though, so a separate file seems like a happy medium.

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.

Measure atenet-router request capacity

3 participants