"""LPS **API 서버** 부하 테스트 (enqueue/조회 경로). 부하 대상은 API(web_main) 다 — 워커(브라우저 크롤)는 프록시/브라우저에 묶여 처리량이 결정되므로 Locust 대상이 아니다. API 는 요청을 받아 job 테이블에 적재만 하고 즉시 응답한다(asyncpg I/O 바운드). 측정 목적 1) **멀티코어 활용**: API 는 asyncio(스레드 1개)라 단일 프로세스=단일 코어. uvicorn workers(=process_count) 를 1→N 으로 올리며 RPS 가 스케일하는지 본다. (bench_multicore.sh 가 자동 비교) 2) **부하 한계**: enqueue 는 job write(+dedupe unique index). 한계는 대개 DB(커넥션 풀·write 경합). (pool_size+max_overflow)×2엔진×workers 가 PG max_connections 를 넘으면 거기서 막힌다. ⚠️ **워커는 끄고** 실행하라(부하 중 실제 크롤=프록시/AI 비용). 잡은 PENDING 으로 쌓였다가 끝나면 정리: psql -h 127.0.0.1 -U postgres -d lps_db -c "TRUNCATE job;" -- 부하 중 커넥션 관측: SELECT count(*) FROM pg_stat_activity WHERE datname='lps_db'; 실행 locust -f loadtest/locustfile.py --host http://localhost:9600 # 웹 UI(:8089) locust -f loadtest/locustfile.py --host http://localhost:9600 --headless -u 200 -r 20 -t 2m """ import random from locust import HttpUser, between, task class LpsApiUser(HttpUser): # 실제 클라이언트처럼 짧게 쉬며 반복(과도한 wait 없이 API 한계를 본다) wait_time = between(0.05, 0.3) def on_start(self): self.last_job = None @task(6) def enqueue_search(self): # 고유 product_code → 실제 INSERT(활성 중복 dedupe 회피). 부하의 핵심 write 경로. code = f"LOAD-{random.randint(0, 2_000_000_000)}" body = {"data": [{"product_code": code, "product_name": "부하테스트 상품", "specification": "1박스", "job_type": "batch"}]} with self.client.post("/v1/lps/search", json=body, name="POST /search", catch_response=True) as r: if r.status_code == 200 and r.json().get("accepted", 0) == 1: self.last_job = (r.json().get("items") or [{}])[0].get("job_id") r.success() else: r.failure(f"{r.status_code} {r.text[:120]}") @task(3) def poll_job(self): # 접수 후 상태 폴링(DB read) if not self.last_job: return with self.client.get(f"/v1/lps/jobs/{self.last_job}", name="GET /jobs/{id}", catch_response=True) as r: r.success() if r.status_code == 200 else r.failure(f"{r.status_code}") @task(1) def queue_stats(self): self.client.get("/v1/lps/queue/stats", name="GET /queue/stats") @task(1) def ops(self): self.client.get("/v1/lps/ops", name="GET /ops") @task(1) def readyz(self): self.client.get("/readyz", name="GET /readyz")