Files
buzz/perf/relay_bus_scaling.py
T
cls 4a79c0072c
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Import exact Chinese Buzz source tree
Signed-off-by: cls_???? <908705107@qq.com>
2026-08-13 18:38:15 +08:00

559 lines
20 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Reproducible Buzz relay Redis bus scaling harness.
The harness isolates the relay's Redis fan-out boundary, not client rendering or
DB ingest. It compares the pre-rewrite global-firehose shape (every pod receives
every published event) with the multi-tenant scoped-topic shape (a pod receives
only community topics it has retained because it has local subscribers).
Default scenario:
* 64 communities publish an equal one-second-equivalent event count.
* one target community has local subscribers on every relay pod.
* all other communities are irrelevant to those pods.
Modes:
* redis: measured Redis PUB/SUB delivery using only Python stdlib (default).
* model: deterministic no-service arithmetic model.
"""
from __future__ import annotations
import argparse
import os
import socket
import threading
import time
from dataclasses import dataclass
from typing import BinaryIO
from urllib.parse import urlparse
from uuid import UUID
BUZZ_PREFIX = "buzz"
OLD_GLOBAL_CHANNEL = f"{BUZZ_PREFIX}:global"
@dataclass(frozen=True)
class Scenario:
pods: int
communities: int
events_per_community_per_sec: float
subscribed_communities: int
interested_pods_per_subscribed_community: int
@property
def total_event_rate(self) -> float:
return self.communities * self.events_per_community_per_sec
@property
def subscribed_event_rate(self) -> float:
return self.subscribed_communities * self.events_per_community_per_sec
def old_global_firehose(self) -> tuple[float, float, float]:
"""Return (cluster ingress/sec, avg pod ingress/sec, irrelevant pct)."""
cluster = self.pods * self.total_event_rate
per_pod = self.total_event_rate
irrelevant = max(self.total_event_rate - self.subscribed_event_rate, 0.0)
irrelevant_pct = 100.0 * irrelevant / self.total_event_rate
return cluster, per_pod, irrelevant_pct
def scoped_bus(self) -> tuple[float, float, float]:
"""Return (cluster ingress/sec, avg pod ingress/sec, irrelevant pct)."""
interested_pods = min(self.pods, self.interested_pods_per_subscribed_community)
cluster = (
self.subscribed_communities
* interested_pods
* self.events_per_community_per_sec
)
per_pod = cluster / self.pods
return cluster, per_pod, 0.0
@dataclass(frozen=True)
class Measurement:
pods: int
old_cluster: int
old_avg_pod: float
old_irrelevant_pct: float
new_cluster: int
new_avg_pod: float
new_irrelevant_pct: float
reduction: float
published: int
@dataclass(frozen=True)
class RedisAddress:
host: str
port: int
db: int
username: str | None = None
password: str | None = None
def fmt(value: float) -> str:
if abs(value - round(value)) < 1e-9:
return f"{int(round(value)):,}"
return f"{value:,.2f}"
def community_id(index: int) -> str:
"""Deterministic UUIDs matching the relay's topic format."""
return str(UUID(int=index + 1))
def scoped_global_channel(community: str) -> str:
# Mirrors crates/buzz-pubsub/src/topic.rs EventTopic::Global:
# format!("buzz:{}:global", self.community_id)
return f"{BUZZ_PREFIX}:{community}:global"
def encode_command(*parts: object) -> bytes:
encoded = [str(part).encode("utf-8") for part in parts]
frame = [f"*{len(encoded)}\r\n".encode("ascii")]
for part in encoded:
frame.append(f"${len(part)}\r\n".encode("ascii"))
frame.append(part)
frame.append(b"\r\n")
return b"".join(frame)
def _read_line(stream: BinaryIO) -> bytes:
line = stream.readline()
if not line:
raise EOFError("Redis connection closed")
if not line.endswith(b"\r\n"):
raise ValueError(f"malformed Redis line: {line!r}")
return line[:-2]
def read_resp(stream: BinaryIO) -> object:
prefix = stream.read(1)
if not prefix:
raise EOFError("Redis connection closed")
if prefix == b"+":
return _read_line(stream).decode("utf-8")
if prefix == b"-":
raise RuntimeError(_read_line(stream).decode("utf-8"))
if prefix == b":":
return int(_read_line(stream))
if prefix == b"$":
length = int(_read_line(stream))
if length == -1:
return None
data = stream.read(length)
terminator = stream.read(2)
if len(data) != length or terminator != b"\r\n":
raise ValueError("malformed Redis bulk string")
return data.decode("utf-8")
if prefix == b"*":
length = int(_read_line(stream))
if length == -1:
return None
return [read_resp(stream) for _ in range(length)]
raise ValueError(f"unknown Redis RESP prefix: {prefix!r}")
class RedisClient:
def __init__(self, address: RedisAddress, timeout: float) -> None:
self._address = address
self._sock = socket.create_connection((address.host, address.port), timeout=timeout)
self._sock.settimeout(timeout)
self._stream = self._sock.makefile("rwb")
self._authenticate_and_select()
def _authenticate_and_select(self) -> None:
if self._address.password:
if self._address.username:
self.command("AUTH", self._address.username, self._address.password)
else:
self.command("AUTH", self._address.password)
if self._address.db:
self.command("SELECT", self._address.db)
def command(self, *parts: object) -> object:
self._stream.write(encode_command(*parts))
self._stream.flush()
return read_resp(self._stream)
def close(self) -> None:
try:
self._stream.close()
finally:
self._sock.close()
def __enter__(self) -> "RedisClient":
return self
def __exit__(self, *_exc: object) -> None:
self.close()
class RedisSubscriber(threading.Thread):
def __init__(
self,
*,
pod: int,
address: RedisAddress,
channels: list[str],
relevant_communities: set[str],
timeout: float,
) -> None:
super().__init__(name=f"redis-sub-pod-{pod}", daemon=True)
self.address = address
self.channels = channels
self.relevant_communities = relevant_communities
self.timeout = timeout
self.ready = threading.Event()
self.stop_requested = threading.Event()
self.error: BaseException | None = None
self.received = 0
self.irrelevant = 0
self._sock: socket.socket | None = None
def run(self) -> None:
try:
sock = socket.create_connection((self.address.host, self.address.port), timeout=self.timeout)
self._sock = sock
sock.settimeout(0.2)
stream = sock.makefile("rwb")
try:
if self.address.password:
if self.address.username:
self._write_and_read(stream, "AUTH", self.address.username, self.address.password)
else:
self._write_and_read(stream, "AUTH", self.address.password)
if self.address.db:
self._write_and_read(stream, "SELECT", self.address.db)
stream.write(encode_command("SUBSCRIBE", *self.channels))
stream.flush()
subscribed = 0
while subscribed < len(self.channels):
frame = read_resp(stream)
if isinstance(frame, list) and frame and frame[0] == "subscribe":
subscribed += 1
self.ready.set()
while not self.stop_requested.is_set():
try:
frame = read_resp(stream)
except socket.timeout:
continue
if not (isinstance(frame, list) and len(frame) >= 3 and frame[0] == "message"):
continue
payload = str(frame[2])
community = payload.split("|", 1)[0]
self.received += 1
if community not in self.relevant_communities:
self.irrelevant += 1
finally:
stream.close()
sock.close()
except OSError as exc:
if not self.stop_requested.is_set():
self.error = exc
self.ready.set()
except BaseException as exc:
self.error = exc
self.ready.set()
@staticmethod
def _write_and_read(stream: BinaryIO, *parts: object) -> object:
stream.write(encode_command(*parts))
stream.flush()
return read_resp(stream)
def stop(self) -> None:
self.stop_requested.set()
if self._sock is not None:
try:
self._sock.shutdown(socket.SHUT_RDWR)
except OSError:
pass
try:
self._sock.close()
except OSError:
pass
def parse_redis_url(raw: str) -> RedisAddress:
parsed = urlparse(raw)
if parsed.scheme not in {"redis", "rediss"}:
raise ValueError(f"unsupported Redis URL scheme: {parsed.scheme!r}")
if parsed.scheme == "rediss":
raise ValueError("rediss:// is not supported by this stdlib harness; use redis://")
return RedisAddress(
host=parsed.hostname or "127.0.0.1",
port=parsed.port or 6379,
db=int(parsed.path.lstrip("/") or "0"),
username=parsed.username,
password=parsed.password,
)
def publish_scenario(
*,
address: RedisAddress,
channels_by_community: list[str],
events_per_community: int,
timeout: float,
) -> None:
with RedisClient(address, timeout) as redis:
for community_index, channel in enumerate(channels_by_community):
community = community_id(community_index)
for event_index in range(events_per_community):
redis.command("PUBLISH", channel, f"{community}|{event_index}")
def wait_until_ready(subscribers: list[RedisSubscriber], timeout: float) -> None:
for sub in subscribers:
if not sub.ready.wait(timeout):
stop_subscribers(subscribers)
raise TimeoutError(f"subscriber {sub.name} did not SUBSCRIBE within {timeout}s")
if sub.error is not None:
stop_subscribers(subscribers)
raise RuntimeError(f"subscriber {sub.name} failed: {sub.error}")
def wait_for_counts(subscribers: list[RedisSubscriber], expected_total: int, timeout: float) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if any(sub.error for sub in subscribers):
break
if sum(sub.received for sub in subscribers) >= expected_total:
return
time.sleep(0.01)
actual = sum(sub.received for sub in subscribers)
raise TimeoutError(f"timed out waiting for {expected_total} pub/sub deliveries; got {actual}")
def stop_subscribers(subscribers: list[RedisSubscriber]) -> None:
for sub in subscribers:
sub.stop()
for sub in subscribers:
sub.join(timeout=1.0)
def measure_redis_for_pods(args: argparse.Namespace, pods: int) -> Measurement:
events_per_community = int(args.events_per_community_per_sec)
if events_per_community != args.events_per_community_per_sec:
raise ValueError("--mode redis requires integer --events-per-community-per-sec")
address = parse_redis_url(args.redis_url)
communities = [community_id(i) for i in range(args.communities)]
relevant = set(communities[: args.subscribed_communities])
interested = pods if args.interested_pods_per_subscribed_community == 0 else min(
pods, args.interested_pods_per_subscribed_community
)
with RedisClient(address, args.redis_timeout) as redis:
assert redis.command("PING") == "PONG"
old_subs = [
RedisSubscriber(
pod=pod,
address=address,
channels=[OLD_GLOBAL_CHANNEL],
relevant_communities=relevant,
timeout=args.redis_timeout,
)
for pod in range(pods)
]
for sub in old_subs:
sub.start()
wait_until_ready(old_subs, args.redis_timeout)
publish_scenario(
address=address,
channels_by_community=[OLD_GLOBAL_CHANNEL for _ in communities],
events_per_community=events_per_community,
timeout=args.redis_timeout,
)
wait_for_counts(old_subs, pods * args.communities * events_per_community, args.redis_timeout)
stop_subscribers(old_subs)
new_subs = [
RedisSubscriber(
pod=pod,
address=address,
channels=[scoped_global_channel(community) for community in communities[: args.subscribed_communities]],
relevant_communities=relevant,
timeout=args.redis_timeout,
)
for pod in range(interested)
]
for sub in new_subs:
sub.start()
wait_until_ready(new_subs, args.redis_timeout)
publish_scenario(
address=address,
channels_by_community=[scoped_global_channel(community) for community in communities],
events_per_community=events_per_community,
timeout=args.redis_timeout,
)
wait_for_counts(new_subs, interested * args.subscribed_communities * events_per_community, args.redis_timeout)
stop_subscribers(new_subs)
old_cluster = sum(sub.received for sub in old_subs)
new_cluster = sum(sub.received for sub in new_subs)
old_irrelevant = sum(sub.irrelevant for sub in old_subs)
new_irrelevant = sum(sub.irrelevant for sub in new_subs)
published = args.communities * events_per_community
return Measurement(
pods=pods,
old_cluster=old_cluster,
old_avg_pod=old_cluster / pods,
old_irrelevant_pct=100.0 * old_irrelevant / old_cluster if old_cluster else 0.0,
new_cluster=new_cluster,
new_avg_pod=new_cluster / pods,
new_irrelevant_pct=100.0 * new_irrelevant / new_cluster if new_cluster else 0.0,
reduction=old_cluster / new_cluster if new_cluster else float("inf"),
published=published,
)
def model_measurements(args: argparse.Namespace, pods_values: list[int]) -> list[Measurement]:
rows = []
for pods in pods_values:
scenario = Scenario(
pods=pods,
communities=args.communities,
events_per_community_per_sec=args.events_per_community_per_sec,
subscribed_communities=args.subscribed_communities,
interested_pods_per_subscribed_community=args.interested_pods_per_subscribed_community or pods,
)
old_cluster, old_pod, old_irrelevant = scenario.old_global_firehose()
new_cluster, new_pod, new_irrelevant = scenario.scoped_bus()
rows.append(
Measurement(
pods=pods,
old_cluster=int(old_cluster),
old_avg_pod=old_pod,
old_irrelevant_pct=old_irrelevant,
new_cluster=int(new_cluster),
new_avg_pod=new_pod,
new_irrelevant_pct=new_irrelevant,
reduction=old_cluster / new_cluster if new_cluster else float("inf"),
published=int(scenario.total_event_rate),
)
)
return rows
def redis_measurements(args: argparse.Namespace, pods_values: list[int]) -> list[Measurement]:
return [measure_redis_for_pods(args, pods) for pods in pods_values]
def assert_scaling(args: argparse.Namespace, rows: list[Measurement]) -> None:
ideal = args.communities / args.subscribed_communities
min_reduction = ideal * args.min_reduction_ratio
for row in rows:
if row.reduction < min_reduction:
raise AssertionError(
f"pods={row.pods}: reduction {row.reduction:.2f}× < required {min_reduction:.2f}×"
)
if row.new_irrelevant_pct > args.max_scoped_irrelevant_pct:
raise AssertionError(
f"pods={row.pods}: scoped irrelevant {row.new_irrelevant_pct:.2f}% > "
f"allowed {args.max_scoped_irrelevant_pct:.2f}%"
)
def print_rows(args: argparse.Namespace, rows: list[Measurement]) -> None:
print("Buzz relay Redis bus scaling harness")
print("====================================")
print(
"scenario: "
f"{args.communities} communities × {fmt(args.events_per_community_per_sec)} events/s, "
f"{args.subscribed_communities} subscribed community topic(s), "
f"interested pods per subscribed community = "
f"{args.interested_pods_per_subscribed_community or 'all pods'}, "
f"mode={args.mode}"
)
if args.mode == "redis":
print(f"redis: {args.redis_url}")
print()
print(
"| pods | old global cluster ingress/s | old avg pod ingress/s | "
"new scoped cluster ingress/s | new avg pod ingress/s | reduction | old irrelevant/pod | new irrelevant/pod |"
)
print("|---:|---:|---:|---:|---:|---:|---:|---:|")
for row in rows:
print(
f"| {row.pods} | {fmt(row.old_cluster)} | {fmt(row.old_avg_pod)} | "
f"{fmt(row.new_cluster)} | {fmt(row.new_avg_pod)} | {row.reduction:,.1f}× | "
f"{row.old_irrelevant_pct:.2f}% | {row.new_irrelevant_pct:.2f}% |"
)
print()
print("Interpretation:")
print(
"- Old relay/global bus: every pod receives every community's event, so "
"cluster pub/sub ingress = pods × total_event_rate."
)
print(
"- New relay/scoped bus: a pod retains only server-resolved community topics "
"with local subscribers, so ingress = interested_pods × subscribed_community_rate."
)
print(
"- The assertion checks the bus-bound scaling claim; end-to-end latency/DB "
"capacity should be measured separately with a live relay stack."
)
def run(args: argparse.Namespace) -> int:
if args.communities <= 0:
raise ValueError("--communities must be positive")
if args.subscribed_communities <= 0:
raise ValueError("--subscribed-communities must be positive")
if args.subscribed_communities > args.communities:
raise ValueError("--subscribed-communities cannot exceed --communities")
pods_values = [int(p.strip()) for p in args.pods.split(",") if p.strip()]
if not pods_values or any(p <= 0 for p in pods_values):
raise ValueError("--pods must contain positive integers")
rows = model_measurements(args, pods_values) if args.mode == "model" else redis_measurements(args, pods_values)
print_rows(args, rows)
if args.assert_scaling:
assert_scaling(args, rows)
print()
print(
"assertion: PASS "
f"(reduction ≥ {args.min_reduction_ratio:.0%} of ideal, "
f"scoped irrelevant ≤ {args.max_scoped_irrelevant_pct:.2f}%)"
)
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mode", choices=["redis", "model"], default="redis")
parser.add_argument("--pods", default="1,2,4", help="comma-separated pod counts")
parser.add_argument("--communities", type=int, default=64)
parser.add_argument("--events-per-community-per-sec", type=float, default=100.0)
parser.add_argument("--subscribed-communities", type=int, default=1)
parser.add_argument(
"--interested-pods-per-subscribed-community",
type=int,
default=0,
help="0 means all pods are interested in the subscribed community",
)
parser.add_argument("--redis-url", default=os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0"))
parser.add_argument("--redis-timeout", type=float, default=10.0)
parser.add_argument(
"--no-assert-scaling",
dest="assert_scaling",
action="store_false",
help="print measurements without enforcing the scaling invariant",
)
parser.set_defaults(assert_scaling=True)
parser.add_argument(
"--min-reduction-ratio",
type=float,
default=0.95,
help="minimum observed reduction as a fraction of ideal communities/subscribed",
)
parser.add_argument("--max-scoped-irrelevant-pct", type=float, default=0.0)
return run(parser.parse_args())
if __name__ == "__main__":
raise SystemExit(main())