mkv

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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())