o2o-negosium-original/lps/loadtest/README.md
민헌 a63793daf0 test(lps): Locust API 부하 테스트 + 멀티코어 벤치 (풀 병목 발견·증명)
API(enqueue/조회, asyncpg I/O 바운드) 부하 테스트로 멀티코어 활용·한계 측정.

- loadtest/locustfile.py: enqueue(고유코드 write)+조회 가중 부하.
- loadtest/bench_multicore.sh: PROCESS_COUNT 1→N 자동 비교(--processes 로 부하생성기도 멀티프로세스).
- server_configs: PROCESS_COUNT / DB_POOL_SIZE / DB_MAX_OVERFLOW env override(코드·toml 수정 없이 튜닝).
- loadtest/README.md: 발견 문서화.

발견(11코어·1500users): 기본 풀(10/20)로 워커 늘리면 실패 폭증(1w=0 → 4w=10336) —
(pool+overflow)×2엔진×workers=240 > PG max_connections=100 커넥션 고갈(SQLAlchemy pool checkout 실패).
증명: 풀 8/4(96<100)로 PC=4 재실행 → RPS 1336→2705(2배), 실패 0. 코드는 멀티코어 활용 가능,
막는 건 풀 오버서브스크립션. 규칙: (pool+overflow)×2×process_count ≤ max_connections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:45:25 +09:00

55 lines
2.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LPS 부하 테스트 (Locust)
**대상 = API 서버**(web_main). 워커(브라우저 크롤)는 프록시/브라우저에 처리량이 묶여 Locust 대상이 아니다.
API 는 요청을 받아 `job` 테이블에 적재만 하고 즉시 응답한다(**asyncpg I/O 바운드**, bcrypt 없음).
## 파일
- `locustfile.py` — enqueue(POST /search, 고유코드 write) + 조회(jobs/stats/ops/readyz) 가중 부하
- `bench_multicore.sh` — **PROCESS_COUNT 1→N 자동 비교**(멀티코어 스케일링 측정, 대화형)
## 실행
```bash
# 웹 UI (:8089)
.venv/bin/locust -f loadtest/locustfile.py --host http://localhost:9600
# 헤드리스
.venv/bin/locust -f loadtest/locustfile.py --host http://localhost:9600 --headless --processes 4 -u 1500 -r 150 -t 1m
# 멀티코어 자동 벤치 (PROCESS_COUNT 1/2/4)
LOCUST_PROCESSES=4 ./loadtest/bench_multicore.sh
```
> ⚠️ **워커는 끄고** 실행(부하 중 실제 크롤=프록시/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';`
## 핵심 발견 (2026-07-09, 11코어 맥 · 1500 users · 20s)
**1) 멀티코어는 되지만 DB 커넥션 풀이 발목을 잡는다.**
API 는 asyncio(스레드 1개)라 단일 프로세스=단일 코어. uvicorn `workers`(=`process_count`)를 늘리면
코어만큼 스케일해야 하는데, **기본 풀(pool_size=10, max_overflow=20)에선 워커를 늘릴수록 오히려 실패 폭증**:
| workers | RPS | 실패 | 원인 |
|:---:|---:|---:|---|
| 1 | 1,137 | 0 | 정상(단일 코어) |
| 2 | 1,390 | 2,997 | 커넥션 초과 시작 |
| 4 | 1,336 | 10,336 | **커넥션 고갈**(SQLAlchemy pool checkout 실패) |
원인: **`(pool_size+max_overflow) × 2엔진(R/W) × workers` 가 PG `max_connections`(기본 100)를 초과**.
4워커면 (10+20)×2×4 = **240 > 100** → 워커 3~4가 커넥션 못 받아 요청 실패.
**2) 풀을 올바르게 잡으면 멀티코어가 제대로 작동한다.**
| PC=4 설정 | RPS | 실패 |
|---|---:|---:|
| 풀 10/20 (240 요구) | 1,336 | 10,336 |
| **풀 8/4 (96 요구)** | **2,705** | **0** |
풀만 줄이니 **RPS 2배 + 실패 0**. 즉 코드는 멀티코어를 활용할 수 있고, **막는 건 풀 오버서브스크립션**이다.
## 튜닝 규칙 (프로덕션)
```
(pool_size + max_overflow) × 2 × process_count ≤ PG max_connections
```
- 예) max_connections=100, process_count=4 → (pool+overflow) ≤ 12 → **pool_size=8, max_overflow=4**
- env 로 조절(코드 수정 없이): `DB_POOL_SIZE`, `DB_MAX_OVERFLOW`, `PROCESS_COUNT`
- 더 큰 처리량이 필요하면: **PG `max_connections` 상향** 또는 **pgbouncer**(커넥션 풀러) 도입
- **부하 한계(이 머신)**: 풀 정상화 시 4워커 ~2,700 RPS, p95 ~900ms, 실패 0