load_test.py (5879B)
1 #!/usr/bin/env python3 2 """ 3 Load test for mkv or minikeyvalue. 4 5 Usage: 6 python3 load_test.py http://localhost:3000 # test mkv 7 python3 load_test.py http://localhost:3001 # test minikeyvalue 8 9 Options: 10 --keys N Number of keys to test (default: 1000) 11 --concurrency N Number of concurrent requests (default: 50) 12 --size N Value size in bytes (default: 1024) 13 """ 14 15 import argparse 16 import asyncio 17 import os 18 import time 19 import aiohttp 20 21 22 def make_value(size: int) -> bytes: 23 return os.urandom(size) 24 25 26 async def run_puts(session, base_url, keys, value, concurrency): 27 """PUT all keys, return (total_time, errors).""" 28 sem = asyncio.Semaphore(concurrency) 29 errors = 0 30 31 async def put_one(key): 32 nonlocal errors 33 async with sem: 34 try: 35 async with session.put(f"{base_url}/{key}", data=value) as resp: 36 if resp.status not in (200, 201, 204): 37 errors += 1 38 except Exception: 39 errors += 1 40 41 start = time.monotonic() 42 await asyncio.gather(*(put_one(k) for k in keys)) 43 elapsed = time.monotonic() - start 44 return elapsed, errors 45 46 47 async def run_gets(session, base_url, keys, concurrency, follow_redirects): 48 """GET all keys, return (total_time, errors).""" 49 sem = asyncio.Semaphore(concurrency) 50 errors = 0 51 52 async def get_one(key): 53 nonlocal errors 54 async with sem: 55 try: 56 async with session.get( 57 f"{base_url}/{key}", 58 allow_redirects=follow_redirects, 59 ) as resp: 60 if follow_redirects: 61 if resp.status != 200: 62 errors += 1 63 else: 64 await resp.read() 65 else: 66 # For redirect-based (mkv), 302 is success 67 if resp.status not in (200, 302): 68 errors += 1 69 except Exception: 70 errors += 1 71 72 start = time.monotonic() 73 await asyncio.gather(*(get_one(k) for k in keys)) 74 elapsed = time.monotonic() - start 75 return elapsed, errors 76 77 78 async def run_deletes(session, base_url, keys, concurrency): 79 """DELETE all keys, return (total_time, errors).""" 80 sem = asyncio.Semaphore(concurrency) 81 errors = 0 82 83 async def delete_one(key): 84 nonlocal errors 85 async with sem: 86 try: 87 async with session.delete(f"{base_url}/{key}") as resp: 88 if resp.status not in (200, 204): 89 errors += 1 90 except Exception: 91 errors += 1 92 93 start = time.monotonic() 94 await asyncio.gather(*(delete_one(k) for k in keys)) 95 elapsed = time.monotonic() - start 96 return elapsed, errors 97 98 99 def print_result(label, count, elapsed, errors): 100 rps = count / elapsed if elapsed > 0 else 0 101 print(f" {label:12s} {elapsed:7.2f}s {rps:8.0f} req/s {errors} errors") 102 103 104 async def main(): 105 parser = argparse.ArgumentParser(description="Load test mkv or minikeyvalue") 106 parser.add_argument("url", help="Base URL (e.g. http://localhost:3000)") 107 parser.add_argument("--keys", type=int, default=1000, help="Number of keys") 108 parser.add_argument("--concurrency", type=int, default=50, help="Concurrent requests") 109 parser.add_argument("--size", type=int, default=1024, help="Value size in bytes") 110 parser.add_argument( 111 "--follow-redirects", action="store_true", 112 help="Follow GET redirects (use for mkv to measure full round-trip)", 113 ) 114 parser.add_argument( 115 "--prefix", default="loadtest", 116 help="Key prefix (use different prefixes to avoid collisions)", 117 ) 118 args = parser.parse_args() 119 120 base = args.url.rstrip("/") 121 keys = [f"{args.prefix}/key-{i:06d}" for i in range(args.keys)] 122 value = make_value(args.size) 123 124 print(f"Target: {base}") 125 print(f"Keys: {args.keys}") 126 print(f"Concurrency: {args.concurrency}") 127 print(f"Value size: {args.size} bytes") 128 print(f"Follow redir:{args.follow_redirects}") 129 print() 130 131 conn = aiohttp.TCPConnector(limit=args.concurrency + 10) 132 async with aiohttp.ClientSession(connector=conn) as session: 133 # Warmup — check server is reachable 134 try: 135 async with session.get(base) as resp: 136 pass 137 except Exception as e: 138 print(f"ERROR: Cannot reach {base}: {e}") 139 return 140 141 # PUTs 142 put_time, put_err = await run_puts(session, base, keys, value, args.concurrency) 143 print_result("PUT", len(keys), put_time, put_err) 144 145 # GETs 146 get_time, get_err = await run_gets( 147 session, base, keys, args.concurrency, args.follow_redirects 148 ) 149 print_result("GET", len(keys), get_time, get_err) 150 151 # Second GET pass (warm) 152 get2_time, get2_err = await run_gets( 153 session, base, keys, args.concurrency, args.follow_redirects 154 ) 155 print_result("GET (warm)", len(keys), get2_time, get2_err) 156 157 # DELETEs 158 del_time, del_err = await run_deletes(session, base, keys, args.concurrency) 159 print_result("DELETE", len(keys), del_time, del_err) 160 161 print() 162 total = put_time + get_time + get2_time + del_time 163 total_ops = len(keys) * 4 164 print(f"Total: {total_ops} ops in {total:.2f}s ({total_ops / total:.0f} ops/s)") 165 print() 166 print("Note: PUT/DELETE throughput is bottlenecked by HTTP round-trips") 167 print("to volume servers (nginx), not by the index (SQLite/LevelDB).") 168 169 170 if __name__ == "__main__": 171 asyncio.run(main())