loadtest_8020.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 8020 接口压测(在 249 本机跑,避免网络变量)。
  5. 阶梯并发(step):每个并发档跑 per-step 秒,请求 multipart 上传同一张图并带 refresh=1
  6. (击穿缓存,逼真实推理);统计每档 RPS / p50 / p90 / p99 / 状态码直方图。
  7. 结尾打一次 /health 拿队列与缓存计数。产出"单机最大可用并发"结论:p99 不超时
  8. (<50s)且 429 少量出现的最高档。
  9. 用法(任意有 requests 的 env):
  10. python tools/loadtest_8020.py --image /path/to/多卡图.jpg --steps 1,2,4,8,16,32 --per-step 60
  11. """
  12. import os
  13. import sys
  14. import time
  15. import argparse
  16. import threading
  17. import statistics
  18. from concurrent.futures import ThreadPoolExecutor, as_completed
  19. sys.stdout.reconfigure(encoding="utf-8")
  20. import requests
  21. def _pctl(sorted_vals, q):
  22. if not sorted_vals:
  23. return None
  24. k = min(len(sorted_vals) - 1, max(0, int(round(q / 100.0 * (len(sorted_vals) - 1)))))
  25. return sorted_vals[k]
  26. def _worker(url, img_path, stop_ts, results, lock):
  27. with open(img_path, "rb") as f:
  28. blob = f.read()
  29. sess = requests.Session()
  30. i = 0
  31. while time.time() < stop_ts:
  32. i += 1
  33. t0 = time.perf_counter()
  34. status = None
  35. try:
  36. r = sess.post(url, files={"image": ("loadtest.jpg", blob)},
  37. data={"refresh": "1"}, timeout=90)
  38. status = r.status_code
  39. except Exception:
  40. status = -1
  41. dt = (time.perf_counter() - t0) * 1000
  42. with lock:
  43. results.append((status, dt))
  44. def _step(name, concurrency, duration, url, img_path):
  45. results = []
  46. lock = threading.Lock()
  47. stop_ts = time.time() + duration
  48. with ThreadPoolExecutor(max_workers=concurrency) as ex:
  49. futs = [ex.submit(_worker, url, img_path, stop_ts, results, lock)
  50. for _ in range(concurrency)]
  51. for f in as_completed(futs):
  52. f.result()
  53. lat = sorted(d for _s, d in results)
  54. codes = {}
  55. for s, _d in results:
  56. codes[s] = codes.get(s, 0) + 1
  57. rps = len(results) / duration
  58. print(f"[{name}] 请求 {len(results)} RPS={rps:.2f} "
  59. f"p50={_pctl(lat, 50):.0f}ms p90={_pctl(lat, 90):.0f}ms p99={_pctl(lat, 99):.0f}ms "
  60. f"max={lat[-1] if lat else 0:.0f}ms 状态码={dict(sorted(codes.items()))}", flush=True)
  61. return {"n": len(results), "rps": rps, "p99": _pctl(lat, 99), "codes": codes}
  62. def main():
  63. ap = argparse.ArgumentParser()
  64. ap.add_argument("--url", default="http://127.0.0.1:8020/match_fields")
  65. ap.add_argument("--image", required=True, help="压测用图(多卡图更能测出真实负载)")
  66. ap.add_argument("--steps", default="1,2,4,8,16,32")
  67. ap.add_argument("--per-step", type=int, default=60, help="每档时长(秒)")
  68. args = ap.parse_args()
  69. print(f"[LoadTest] {args.url} image={args.image} steps={args.steps} "
  70. f"per-step={args.per_step}s", flush=True)
  71. # 预热一发(不带 refresh),确认接口通
  72. with open(args.image, "rb") as f:
  73. r = requests.post(args.url, files={"image": ("warmup.jpg", f.read())}, timeout=120)
  74. print(f"[LoadTest] 预热请求 status={r.status_code} body前80字={r.text[:80]!r}", flush=True)
  75. if r.status_code != 200:
  76. sys.exit("预热失败,终止压测")
  77. summary = []
  78. for c in [int(x) for x in args.steps.split(",") if x.strip()]:
  79. summary.append((c, _step(f"c={c}", c, args.per_step, args.url, args.image)))
  80. print("\n[Summary] 并发 → RPS / p99 / 状态码", flush=True)
  81. for c, s in summary:
  82. print(f" c={c:<3} RPS={s['rps']:.2f} p99={s['p99']:.0f}ms {s['codes']}", flush=True)
  83. try:
  84. h = requests.get(args.url.rsplit("/", 1)[0] + "/health", timeout=5).json()
  85. print(f"\n[Health] queue={h.get('queue')} stats={h.get('stats')} "
  86. f"cache={ {k: h.get('cache', {}).get(k) for k in ('size', 'hits', 'misses')} }", flush=True)
  87. except Exception:
  88. pass
  89. print("\n[结论口径] 取 p99 < 50s 且 429 占比低的最高并发档为可用上限;"
  90. "RPS 进入平台期即 GPU 吞吐上限。", flush=True)
  91. if __name__ == "__main__":
  92. main()