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

4.1 KiB
Raw Blame History

LPS 부하 테스트 (Locust)

대상 = API 서버(web_main). 워커(브라우저 크롤)는 프록시/브라우저에 처리량이 묶여 Locust 대상이 아니다. API 는 요청을 받아 job 테이블에 적재만 하고 즉시 응답한다(asyncpg I/O 바운드, bcrypt 없음).

파일

  • locustfile.py — enqueue(POST /search, 고유코드 write) + 조회(jobs/stats/ops/readyz) 가중 부하
  • bench_multicore.shPROCESS_COUNT 1→N 자동 비교(멀티코어 스케일링 측정, 대화형)
  • monitor.py실시간 관측 대시보드(:9700, ../run_monitor.sh) — 큐 추이·처리량(개/분)·코어별 CPU· 프로세스 그룹(worker/api/chrome/postgres) 사용률. e2e(loadtest.py N=100 등)와 같이 띄워 "코어가 다 도는지 / 병목이 어느 층인지"를 본다. Grafana 대체가 아닌 로컬 경량 도구(외부 인프라 없음).

실행

# 웹 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