o2o-negosium-original/lps/loadtest/README.md
민헌 89fe2c9805 feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률
loadtest/monitor.py (:9700, 단일 파일·외부 인프라 없음) + run_monitor.sh(대화형).
e2e 부하(N=100 loadtest.py) 중 "프로세스가 잘 진행되는지, 코어가 전부 도는지,
병목이 어느 층인지"를 브라우저에서 2초 간격으로 본다:

- 큐 추이: /v1/lps/ops 폴링 — PENDING/RUNNING/DONE/DEAD 라인 + 처리량(개/분) 타일
- 프로세스 그룹 CPU: worker/api/chrome/postgres — 병목 층 판독
  (chrome 은 워커 자손만 집계해 사용자 브라우저와 분리, uvicorn spawn 자식은 부모로 api 귀속)
- 코어별 사용률 막대: 멀티코어 활용 확인
- 현재 스냅샷 표 + 호버 툴팁 + 라인 끝 직접 라벨(dataviz 팔레트 검증 통과, 다크 서피스)
- psutil 의존성 추가(로컬 관측 전용)

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

72 lines
4.1 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 자동 비교**(멀티코어 스케일링 측정, 대화형)
- `monitor.py` — **실시간 관측 대시보드**(:9700, `../run_monitor.sh`) — 큐 추이·처리량(개/분)·코어별 CPU·
프로세스 그룹(worker/api/chrome/postgres) 사용률. e2e(`loadtest.py` N=100 등)와 같이 띄워
"코어가 다 도는지 / 병목이 어느 층인지"를 본다. Grafana 대체가 아닌 로컬 경량 도구(외부 인프라 없음).
## 실행
```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
```
**이 규칙은 이제 config 가 자동으로 지킨다** — `MainDBConfig.connection_budget`(기본 40)을 두면,
기동 시 `process_count` 에 맞춰 `pool_size/max_overflow` 를 역산한다:
`(pool+overflow)×2×process_count ≤ connection_budget`. 워커를 늘려도 커넥션이 예산을 넘지 않는다.
(`server_configs._autosize_pool`. 기동 로그 `DB Pool : ... = N conns (budget=…)` 로 실효값 확인)
| process_count | 자동 산정(예산 40) | 총 커넥션 |
|:---:|:---:|---:|
| 1 | pool 12 / overflow 8 | 40 |
| 2 | pool 6 / overflow 4 | 40 |
| 4 | pool 3 / overflow 2 | 40 |
| 8 | pool 1 / overflow 1 | 32 |
- **예산 설정**: 전용 PG(max_connections=100)면 `connection_budget≈90`, 공유 PG면 40 권장. `env DB_CONNECTION_BUDGET`.
(공유 PG 기본 40 은 API + worker + 타 서비스가 100 안에 공존하도록 잡은 안전값 → 처리량보다 안정 우선)
- **override 우선순위**: 명시 `DB_POOL_SIZE`/`DB_MAX_OVERFLOW` > 자동 산정(budget>0) > toml `pool_size/max_overflow`(budget=0)
- 더 큰 처리량이 필요하면: 예산 상향 + **PG `max_connections` 상향** 또는 **pgbouncer**(커넥션 풀러) 도입
- **부하 한계(이 머신)**: 예산 96(≈max_connections)·4워커 ~2,700 RPS, p95 ~900ms, 실패 0