Skip to content

registry: hot-path performance — memoized sig verification, lock-free telemetry, chunked reaper, single-write framing#100

Open
TeoSlayer wants to merge 1 commit into
mainfrom
perf/registry-hotpath
Open

registry: hot-path performance — memoized sig verification, lock-free telemetry, chunked reaper, single-write framing#100
TeoSlayer wants to merge 1 commit into
mainfrom
perf/registry-hotpath

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Why

Production profiling on the live registry during today's fleet-wide outage (207K conns, reconnect storm) showed:

  • ~30% of all CPU in crypto/ed25519.Verify — re-verifying deterministic challenges (heartbeat:%d, resolve:%d:%d) whose signatures are identical on every repeat (ed25519 is deterministic)
  • ~26% in raw write syscalls (writeMessage = 2 writes/frame)
  • ~3.5K goroutines blocked on RLock behind ReapStaleNodes, which collected + sorted the full 240K-node key set under the write lock on every sweep — even when nothing was stale
  • an extra s.mu.RLock per message for a standby flag that already has its own lock, plus per-network telemetry counters queueing behind writers

What

  • authz.VerifyCached: sharded, bounded memoization of successful (pubkey, message, sig) verifications (sha256 key, 64 shards × 8192, reset-on-full). Identical tuple ⇒ identical verify result, so this is semantically equivalent; failures are never cached. Wired into VerifyNodeSignature (all JSON paths) and the binary heartbeat + binary resolve paths.
  • handleMessage: IsStandby() no longer wrapped in s.mu; per-network request counters use TryRLock and skip a telemetry tick under writer contention instead of blocking the request.
  • ReapStaleNodes: snapshot + sort + chunk-scan under RLock; write lock only when confirmed-stale nodes exist (staleness re-checked under the lock); Save() signalled outside. The common no-reap sweep now takes no write lock at all.
  • accept.writeMessage: one write per frame (len-prefix + body in a single buffer).

Production results (deployed to pilot-rendezvous-new 2026-07-24)

Reconnect-storm phase at equivalent conn counts (~100–200K):

before after
CPU 886–1092% 121–239%

Tests

  • New TestVerifyCached (valid→cached→valid, tampered sig rejected on repeat, wrong key/message not confused by cache, failure not cached) + shard-cap test
  • go test -race ./authz/... ./directory/... ./accept/... green; full go test ./... green (20 pkgs); go vet clean

🤖 Generated with Claude Code

…k-free telemetry, chunked reaper, single-write framing

Production profiling on the live registry (207K conns, reconnect storm)
showed ~30% of CPU in repeat ed25519 verifications of deterministic
challenges, ~26% in per-message write syscalls, and reader pile-ups
behind the reaper's full-map sweep under the write lock.

- authz.VerifyCached: sharded memoization of successful (pubkey,
  message, sig) verifications; ed25519 is deterministic and registry
  challenges are static per node/request shape, so identical tuples
  always re-verify identically. Failures are never cached. Wired into
  VerifyNodeSignature and the binary heartbeat/resolve paths.
- handleMessage: drop the s.mu wrap around walStore.IsStandby (wal has
  its own lock); per-network request counters use TryRLock and skip a
  telemetry tick instead of queueing behind writers.
- ReapStaleNodes: collect+sort+scan under RLock, write-lock only to
  delete confirmed-stale nodes (re-checked under the lock), Save()
  signalled outside the lock. Common no-reap sweep takes no write lock.
- accept.writeMessage: single buffered write per frame instead of
  prefix+body (halves write syscalls).

Deployed 2026-07-24: storm-phase CPU dropped from ~1000% to ~240% at
equivalent connection counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread accept/accept.go

var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(body)))
frame := make([]byte, 4+len(body))
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.

3 participants