설정이 .env(compose 주입)·config.toml·코드 곳곳의 os.environ 직독 3계층에 흩어져 관리가 어려웠다. TOML 하나로 통합한다(협의 결정). - 신설 [WorkerConfig](동시성·폴백·프로필·데드라인·유예·Chrome·하트비트), [AlertConfig](웹훅·쿨다운·임계 10종). [WebServerConfig].api_keys(guard), [DecodoConfig].ip_request_budget/port_cooldown_sec 추가 — 흩어져 있던 LPS_* env 20여 개를 섹션으로 흡수. - server_configs 의 env override 계층(DB_*·시크릿·NAVER_KEYS 등) 삭제. 남는 env 는 APP_ENV(부트스트랩)·PROCESS_COUNT/WORKER_CONCURRENCY(실행 스크립트 대화형 입력 전용)·LPS_LIVE(테스트 옵트인)뿐. - Docker: env 주입 → config.docker.toml 마운트 + APP_ENV=docker. 이미지 무시크릿 유지, 마운트 누락 시 FileNotFoundError 즉시 실패. .env.example 삭제, config.docker.toml.example 신설. - negodata 호출부: guard 키를 env 직독에서 [WebServerConfig].lps_api_key (+기존 관례대로 env override)로 이동. - 실행 스크립트: 프로필·폴백·예산 프롬프트 제거(toml 소스 안내), 동시성/프로세스 수만 임시 override 로 유지. - docs 7종·example toml 의 env 표기를 toml 키로 일괄 갱신. - 전체 145 passed + APP_ENV=docker 로딩·API 기동 스모크 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
4.1 KiB
Markdown
72 lines
4.1 KiB
Markdown
# 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 권장(`[MainDBConfig].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
|