Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,200 @@
|
||||
"""同 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()
|
||||
Reference in new issue
Block a user