o2o-negosium-original/lps/loadtest.py
민헌 fbd2cec1da test(lps): 부하테스트 상품셋 100종 카탈로그 분리 — 실제 입력 분포 재현
- loadtest/catalog.json 신설(실존 상품 100종): 규격만 54 · 모델포함 26 ·
  이름만 20 · 기준가 포함 7 (price→가격밴드 필터 경로 포함)
- loadtest.py 는 인라인 6종 대신 카탈로그 로드, 실행 시 구성 요약 출력

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:50:28 +09:00

125 lines
5.5 KiB
Python

"""LPS 부하 테스트 — 여러 상품을 한 번에 제출하고 처리량·지연·비용을 집계한다.
여러 상품 동시 검색을 재현한다. 워커 동시성(WORKER_CONCURRENCY)만큼 상품이 병렬 처리된다.
python loadtest.py # 카탈로그 앞 6개
N=100 python loadtest.py # 상품 100개(loadtest/catalog.json 전체)
BASE=http://localhost:9600 python loadtest.py
상품셋 = loadtest/catalog.json (100종, 실존 상품): 규격만(식품·생활용품) / 규격+모델(가전·디지털)
/ 이름만 / 회사 포함 / 일부 기준가(price→가격밴드 필터 경로)를 섞어 실제 입력 분포를 재현한다.
N > 카탈로그 크기면 반복 확장. product_code 는 부하테스트 전용 프리픽스(LT###, 매회 유니크라
dedupe·네거티브캐시에 안 걸림).
측정: 벽시계 총시간, 처리량(상품/분), 상품별·집계 지연(p50/p95), AI·DECODO·총비용.
워커가 떠 있어야 하고, 워커 동시성이 낮으면 상품들이 큐에서 순차 대기한다(그게 부하의 핵심 관측).
"""
import asyncio
import json
import os
import pathlib
import time
import httpx
BASE = os.environ.get("BASE", "http://localhost:9600")
N = int(os.environ.get("N", "6"))
_CATALOG = json.loads(
(pathlib.Path(__file__).parent / "loadtest" / "catalog.json").read_text(encoding="utf-8")
)
def _products(n):
out = []
for i in range(n):
base = _CATALOG[i % len(_CATALOG)]
out.append({"product_code": f"LT{i:03d}", "job_type": "batch", **base})
return out
def _composition(products) -> str:
"""상품셋 구성 요약 — 어떤 입력 분포로 테스트하는지 리포트에 남긴다."""
model = sum(1 for p in products if p.get("model"))
spec_only = sum(1 for p in products if p.get("specification") and not p.get("model"))
name_only = sum(1 for p in products if not p.get("specification") and not p.get("model"))
priced = sum(1 for p in products if p.get("price"))
return f"모델포함 {model} · 규격만 {spec_only} · 이름만 {name_only} · 기준가 포함 {priced}"
def _pct(values, p):
if not values:
return 0
s = sorted(values)
k = min(len(s) - 1, int(round((p / 100) * (len(s) - 1))))
return s[k]
async def main():
products = _products(N)
print(f"■ 부하 테스트: 상품 {N}개 → {BASE} (워커 동시성만큼 병렬 처리)")
print(f" 구성: {_composition(products)}\n")
async with httpx.AsyncClient(timeout=30) as c:
# 큐/헬스 사전 확인
try:
stats = (await c.get(f"{BASE}/v1/lps/queue/stats")).json()["counts"]
print(f"시작 큐 상태: {stats}")
except Exception as e:
print(f"API 접속 실패({e}). 서버가 떠 있나요?"); return
t0 = time.monotonic()
r = (await c.post(f"{BASE}/v1/lps/search", json={"data": products})).json()
jobs = [it["job_id"] for it in r.get("items", []) if it.get("job_id")]
print(f"접수: {len(jobs)}건 (중복 스킵 {N - len(jobs)})\n폴링 중…\n")
results = {}
while len(results) < len(jobs):
await asyncio.sleep(2)
pend = [j for j in jobs if j not in results]
got = await asyncio.gather(*[c.get(f"{BASE}/v1/lps/jobs/{j}") for j in pend])
for j, resp in zip(pend, got):
d = resp.json()
if d.get("status") in ("DONE", "DEAD"):
results[j] = d
done = len(results)
print(f"\r 진행 {done}/{len(jobs)} ({time.monotonic()-t0:.0f}s 경과)", end="", flush=True)
wall = time.monotonic() - t0
print("\n")
# 집계
durs, ai_costs, proxy_costs, totals = [], [], [], []
found = dead = 0
print(f"{'상품코드':<8} {'상태':<5} {'결과':<10} {'소요':>6} {'AI$':>9} {'DECODO$':>9} {'총$':>9}")
print("-" * 66)
for j in jobs:
d = results[j]
o = d.get("output") or {}
m = o.get("metrics") or {}
cost = m.get("cost") or {}
code = (o.get("query") or "")[:8]
st = d.get("status", "?")
dur = (m.get("duration_ms") or 0) / 1000
if st == "DONE":
found += 1 if o.get("outcome") == "found" else 0
durs.append(dur); ai_costs.append(cost.get("ai_usd", 0))
proxy_costs.append(cost.get("proxy_usd", 0)); totals.append(cost.get("total_usd", 0))
else:
dead += 1
print(f"{code:<8} {st:<5} {o.get('outcome','-'):<10} {dur:>5.1f}s "
f"{cost.get('ai_usd',0):>9.6f} {cost.get('proxy_usd',0):>9.6f} {cost.get('total_usd',0):>9.6f}")
print("-" * 66)
print(f"\n■ 집계 ({len(jobs)}건, DEAD {dead})")
print(f" 벽시계 총시간 : {wall:.1f}s 처리량: {len(jobs)/wall*60:.1f} 상품/분")
print(f" 상품 지연 : p50 {_pct(durs,50):.1f}s · p95 {_pct(durs,95):.1f}s · max {max(durs) if durs else 0:.1f}s")
print(f" ※ 순차합 대비 병렬: 상품별 소요 합 {sum(durs):.0f}s → 벽시계 {wall:.0f}s (동시성 효과)")
print(f" 비용 합계 : AI ${sum(ai_costs):.5f} + DECODO ${sum(proxy_costs):.5f} = ${sum(totals):.5f}")
print(f" 상품당 평균 비용 : ${(sum(totals)/len(totals)) if totals else 0:.6f}")
print(f" 1,000건 추정 비용: ${(sum(totals)/len(totals)*1000) if totals else 0:.2f}")
if __name__ == "__main__":
asyncio.run(main())