"""同 Pod 两个隔离容器:server 执行固定矩阵,client 只负责施压。 构建产物与本文件、process.py、settings.py 放入 /work;两个角色通过 emptyDir 交接有限测试命令。测量采用闭环长连接,延迟含收发和正文验证,不含建连时间。 """ import argparse import hashlib import http.client import json import os from pathlib import Path import subprocess import time from process import Process, summarize ROOT = Path("/work") def write(path, value): temporary = path.with_suffix(".tmp") temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n") temporary.replace(path) def counters(): return {name: (Path("/sys/fs/cgroup") / name).read_text() for name in ("cpu.max", "cpu.stat", "memory.max", "memory.events")} def client(): request = ROOT / "request.json" while not (ROOT / "finished").exists(): if not request.exists(): time.sleep(0.02) continue item = json.loads(request.read_text()) request.unlink() directory = ROOT / item["name"] before = counters() with Process([ROOT / "bin/rust-http", "load", item["port"], item["path"], item["concurrency"], item["seconds"]], directory, cpus={2, 3, 4, 5}) as process: deadline = time.monotonic() + item["seconds"] + 30 while process.child.poll() is None and time.monotonic() < deadline: process.sample() time.sleep(0.2) if process.child.poll() is None: raise TimeoutError(f"load client timed out: {item}") result = {"exit_code": process.child.returncode, "stdout": process.text(), "stderr": (directory / "stderr.log").read_text(), "resources": summarize(process.samples), "samples": process.samples, "cgroup_before": before, "cgroup_after": counters()} write(directory / "result.json", result) def load(process, name, port, path, concurrency, seconds): result_path = ROOT / name / "result.json" write(ROOT / "request.json", {"name": name, "port": port, "path": path, "concurrency": concurrency, "seconds": seconds}) deadline = time.monotonic() + seconds + 40 while not result_path.exists() and time.monotonic() < deadline: process.sample() time.sleep(0.2) result = json.loads(result_path.read_text()) if result["exit_code"] == 0: result["measurement"] = json.loads(result["stdout"]) return result def probe(port): connection = http.client.HTTPConnection("127.0.0.1", port, timeout=5) try: connection.request("GET", "/json") response = connection.getresponse() return response.status == 200 and response.read() == b'{"ok":true}' except OSError: return False finally: connection.close() def command(language, capacity=2048): if language == "dever": return [ROOT / "bin" / ("dever-http" if capacity == 2048 else "dever-overload")] if language == "rust": return [ROOT / "bin/rust-http", "serve", capacity] binary = {"go": "go-http", "axum": "axum-http", "fiber": "fiber-http"}[language] return [ROOT / "bin" / binary, capacity] def measure(language, path, concurrency, seconds, name): with Process(command(language), ROOT / name / "server", cpus={0, 1}) as process: port = process.ready() if not probe(port): raise RuntimeError(f"smoke failed: {name}") warmup = load(process, name + "/warmup", port, path, concurrency, 2) process.samples.clear() before = counters() result = load(process, name + "/load", port, path, concurrency, seconds) after = counters() recovered = probe(port) record = {"name": name, "language": language, "path": path, "concurrency": concurrency, "seconds": seconds, "warmup": warmup, "client": result, "server": summarize(process.samples), "samples": process.samples, "server_alive": process.child.poll() is None, "recovery_probe": recovered, "stderr": (process.directory / "stderr.log").read_text(), "cgroup_before": before, "cgroup_after": after} write(ROOT / name / "result.json", record) with (ROOT / "results.jsonl").open("a") as output: output.write(json.dumps(record) + "\n") print(json.dumps({"name": name, "measurement": result.get("measurement"), "rss_mib": record["server"]["rss_high_water_bytes"] / 1048576, "server_cores": record["server"]["cpu_cores_sampled"], "client_cores": result["resources"]["cpu_cores_sampled"], "recovery": recovered}), flush=True) for phase in (warmup, result): measurement = phase.get("measurement", {}) if phase["exit_code"] != 0 or measurement.get("errors") != 0 or not measurement.get("succeeded"): raise RuntimeError(f"load failed; retained result: {name}") if not record["server_alive"] or not recovered: raise RuntimeError(f"server did not recover: {name}") def overload(language): name = "overload/" + language with Process(command(language, 8192), ROOT / name / "server", cpus={0, 1}) as process: port = process.ready() connections = [] successful = 0 failed = 0 started = time.monotonic() try: for _ in range(4500): connection = http.client.HTTPConnection("127.0.0.1", port, timeout=2) connections.append(connection) connection.request("GET", "/json") response = connection.getresponse() if response.status != 200 or response.read() != b'{"ok":true}': failed += 1 break successful += 1 except (OSError, http.client.HTTPException): failed += 1 held_seconds = time.monotonic() - started process.sample() for connection in connections: connection.close() time.sleep(6) recovered = probe(port) record = {"language": language, "connections_target": 4500, "successful_connections": successful, "failed": failed, "open_seconds": held_seconds, "server_alive": process.child.poll() is None, "recovered_after_release": recovered, "stdout": process.text(), "stderr": (process.directory / "stderr.log").read_text(), "samples": process.samples} write(ROOT / name / "result.json", record) print(json.dumps(record), flush=True) def server(mode, suite): languages = ["dever", "axum", "fiber"] if suite == "frameworks" else ["dever", "rust", "go"] if mode == "smoke": for language in languages: for path in ("/json", "/echo"): measure(language, path, 4, 1, f"smoke/{language}/{path[1:]}") return manifest = {"suite": suite, "languages": languages, "protocol": "HTTP/1.1", "client": "closed loop; one outstanding request per connection", "server_cpus": [0, 1], "client_cpus": [2, 3, 4, 5], "replicates": 3, "seconds": 12, "warmup_seconds": 2, "server_capacity": 2048, "concurrency": [64, 256, 1024], "paths": {"/json": "GET; 11-byte JSON", "/echo": "POST; 65536-byte body returned unchanged"}, "hashes": {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in (ROOT / "bin").iterdir()}} write(ROOT / "manifest.json", manifest) for replicate in range(3): for path in ("/json", "/echo"): for concurrency in (64, 256, 1024): for language in languages[replicate:] + languages[:replicate]: name = f"matrix/{replicate}/{path[1:]}/{concurrency}/{language}" measure(language, path, concurrency, 12, name) for language in languages: measure(language, "/json", 1024, 60, "sustained/" + language) if suite == "core": overload(language) (ROOT / "finished").touch() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("role", choices=("server", "client")) parser.add_argument("--mode", choices=("smoke", "matrix"), default="matrix") parser.add_argument("--suite", choices=("core", "frameworks"), default="core") arguments = parser.parse_args() if arguments.role == "client": client() else: try: server(arguments.mode, arguments.suite) finally: (ROOT / "finished").touch()