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.
81 lines
2.6 KiB
Python
81 lines
2.6 KiB
Python
"""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()
|