Add load test harness for pool and fleet sizing
Adds the scripts used to size the connection pool and validate fleet polling under load: burst arrival and mixed workload drivers, pool and resource samplers, a UI probe, host seed/drop SQL, and a gevent Dockerfile. Hosts are addressed by compose service name (api:5001) rather than a local path, so the harness runs against any stack rather than one machine. loadtest_token.py mints a scoped collector token named LOADTEST and prints the secret once, then revokes only that token by name so a real collector token is never touched. envkeyfallback is false, so the harness needs a genuine managed token rather than an env key.
This commit is contained in:
80
scripts/loadtest/sample_resources.py
Normal file
80
scripts/loadtest/sample_resources.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Sample container CPU and memory during a sizing run.
|
||||
|
||||
WHY: the sizing table claims specific vCPU and RAM figures. Those can only be
|
||||
confirmed by watching the container under load, not by reasoning about greenlet
|
||||
size. This records peak RSS and CPU so a claimed "4 GB is enough" is either
|
||||
demonstrated or falsified.
|
||||
|
||||
Runs on the HOST (needs the docker CLI), unlike the generators.
|
||||
|
||||
Usage:
|
||||
python sample_resources.py --container shopdb-sizing-api --seconds 120
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def parse_size(text):
|
||||
"""Docker stats prints like '1.234GiB' / '512MiB'. Return megabytes."""
|
||||
match = re.match(r'^([0-9.]+)\s*([KMGT]?i?B)$', text.strip())
|
||||
if not match:
|
||||
return 0.0
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2).upper().replace('I', '')
|
||||
factors = {'B': 1.0 / (1024 * 1024), 'KB': 1.0 / 1024, 'MB': 1.0,
|
||||
'GB': 1024.0, 'TB': 1024.0 * 1024.0}
|
||||
return value * factors.get(unit, 1.0)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--container', required=True)
|
||||
parser.add_argument('--seconds', type=int, default=120)
|
||||
parser.add_argument('--interval', type=float, default=2.0)
|
||||
arguments = parser.parse_args()
|
||||
|
||||
cpu_samples = []
|
||||
memory_samples = []
|
||||
deadline = time.monotonic() + arguments.seconds
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
['docker', 'stats', '--no-stream', '--format',
|
||||
'{{.CPUPerc}}|{{.MemUsage}}', arguments.container],
|
||||
stderr=subprocess.DEVNULL, text=True).strip()
|
||||
except subprocess.CalledProcessError:
|
||||
break
|
||||
if not output:
|
||||
break
|
||||
cpu_text, memory_text = output.split('|', 1)
|
||||
cpu_samples.append(float(cpu_text.replace('%', '').strip()))
|
||||
used = memory_text.split('/')[0]
|
||||
memory_samples.append(parse_size(used))
|
||||
time.sleep(arguments.interval)
|
||||
|
||||
def summarize(values):
|
||||
if not values:
|
||||
return {'samples': 0}
|
||||
ordered = sorted(values)
|
||||
index = min(int(len(ordered) * 0.95), len(ordered) - 1)
|
||||
return {
|
||||
'samples': len(values),
|
||||
'median': round(statistics.median(values), 1),
|
||||
'p95': round(ordered[index], 1),
|
||||
'max': round(max(values), 1),
|
||||
}
|
||||
|
||||
print(json.dumps({
|
||||
'container': arguments.container,
|
||||
'cpu_percent': summarize(cpu_samples),
|
||||
'memory_mb': summarize(memory_samples),
|
||||
}, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user