gunicorn/benchmarks/run_benchmark.py
2026-01-23 01:20:03 +01:00

239 lines
7.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Benchmark script for gunicorn gthread worker.
This script runs various benchmarks against gunicorn and reports performance metrics.
Requires: gunicorn, requests (for warmup), and wrk or ab for load testing.
"""
import argparse
import json
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
BENCHMARK_DIR = Path(__file__).parent
APP_MODULE = "simple_app:application"
def check_dependencies():
"""Check if required tools are available."""
# Check for wrk (preferred) or ab
for tool in ['wrk', 'ab']:
try:
subprocess.run([tool, '--version'], capture_output=True, check=False)
return tool
except FileNotFoundError:
continue
print("Error: Neither 'wrk' nor 'ab' found. Install one of them.")
print(" macOS: brew install wrk")
print(" Linux: apt-get install wrk (or apache2-utils for ab)")
sys.exit(1)
def start_gunicorn(worker_class, workers, threads, connections, bind, extra_args=None):
"""Start gunicorn server and return the process."""
cmd = [
sys.executable, '-m', 'gunicorn',
'--worker-class', worker_class,
'--workers', str(workers),
'--threads', str(threads),
'--worker-connections', str(connections),
'--bind', bind,
'--access-logfile', '-',
'--error-logfile', '-',
'--log-level', 'warning',
APP_MODULE,
]
if extra_args:
cmd.extend(extra_args)
env = os.environ.copy()
env['PYTHONPATH'] = str(BENCHMARK_DIR.parent)
proc = subprocess.Popen(
cmd,
cwd=BENCHMARK_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# Wait for server to be ready
time.sleep(2)
return proc
def stop_gunicorn(proc):
"""Stop the gunicorn server."""
proc.send_signal(signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
def run_wrk_benchmark(url, duration, threads, connections):
"""Run wrk benchmark and return results."""
cmd = [
'wrk',
'-t', str(threads),
'-c', str(connections),
'-d', f'{duration}s',
'--latency',
url,
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
return parse_wrk_output(result.stdout)
def run_ab_benchmark(url, requests, concurrency):
"""Run Apache Bench benchmark and return results."""
cmd = [
'ab',
'-n', str(requests),
'-c', str(concurrency),
'-k', # keepalive
url,
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
return parse_ab_output(result.stdout)
def parse_wrk_output(output):
"""Parse wrk output to extract metrics."""
metrics = {}
for line in output.split('\n'):
if 'Requests/sec' in line:
metrics['requests_per_sec'] = float(line.split(':')[1].strip())
elif 'Transfer/sec' in line:
metrics['transfer_per_sec'] = line.split(':')[1].strip()
elif 'Latency' in line and 'Distribution' not in line:
parts = line.split()
if len(parts) >= 2:
metrics['latency_avg'] = parts[1]
elif '50%' in line:
metrics['latency_p50'] = line.split()[1]
elif '99%' in line:
metrics['latency_p99'] = line.split()[1]
return metrics
def parse_ab_output(output):
"""Parse ab output to extract metrics."""
metrics = {}
for line in output.split('\n'):
if 'Requests per second' in line:
metrics['requests_per_sec'] = float(line.split(':')[1].split()[0])
elif 'Time per request' in line and 'mean' in line:
metrics['latency_avg'] = line.split(':')[1].strip()
elif 'Transfer rate' in line:
metrics['transfer_per_sec'] = line.split(':')[1].strip()
return metrics
def run_benchmark_suite(tool, bind_addr):
"""Run a suite of benchmarks."""
results = {}
# Test configurations
configs = [
{'name': 'simple', 'path': '/', 'connections': 100},
{'name': 'simple_high_concurrency', 'path': '/', 'connections': 500},
{'name': 'slow_io', 'path': '/slow', 'connections': 50},
{'name': 'large_response', 'path': '/large', 'connections': 100},
]
for config in configs:
url = f'http://{bind_addr}{config["path"]}'
print(f" Running {config['name']}...")
if tool == 'wrk':
metrics = run_wrk_benchmark(
url,
duration=10,
threads=4,
connections=config['connections'],
)
else:
metrics = run_ab_benchmark(
url,
requests=10000,
concurrency=config['connections'],
)
results[config['name']] = metrics
print(f" Requests/sec: {metrics.get('requests_per_sec', 'N/A')}")
return results
def main():
parser = argparse.ArgumentParser(description='Benchmark gunicorn gthread worker')
parser.add_argument('--workers', type=int, default=2, help='Number of workers')
parser.add_argument('--threads', type=int, default=4, help='Threads per worker')
parser.add_argument('--connections', type=int, default=1000, help='Worker connections')
parser.add_argument('--bind', default='127.0.0.1:8000', help='Bind address')
parser.add_argument('--compare', action='store_true', help='Compare sync vs gthread')
parser.add_argument('--output', help='Output JSON file for results')
args = parser.parse_args()
tool = check_dependencies()
print(f"Using benchmark tool: {tool}")
all_results = {}
if args.compare:
# Compare sync and gthread workers
for worker_class in ['sync', 'gthread']:
print(f"\nBenchmarking {worker_class} worker...")
proc = start_gunicorn(
worker_class=worker_class,
workers=args.workers,
threads=args.threads,
connections=args.connections,
bind=args.bind,
)
try:
all_results[worker_class] = run_benchmark_suite(tool, args.bind)
finally:
stop_gunicorn(proc)
else:
# Just benchmark gthread
print("\nBenchmarking gthread worker...")
proc = start_gunicorn(
worker_class='gthread',
workers=args.workers,
threads=args.threads,
connections=args.connections,
bind=args.bind,
)
try:
all_results['gthread'] = run_benchmark_suite(tool, args.bind)
finally:
stop_gunicorn(proc)
# Print summary
print("\n" + "=" * 60)
print("BENCHMARK SUMMARY")
print("=" * 60)
for worker, results in all_results.items():
print(f"\n{worker.upper()} Worker:")
for test, metrics in results.items():
rps = metrics.get('requests_per_sec', 'N/A')
print(f" {test}: {rps} req/s")
if args.output:
with open(args.output, 'w') as f:
json.dump(all_results, f, indent=2)
print(f"\nResults saved to {args.output}")
if __name__ == '__main__':
main()