201 lines
8.7 KiB
Python
201 lines
8.7 KiB
Python
"""同 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()
|