feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형)

기존 run_local_server.sh 스타일에 맞춘 대화형 .sh 두 개.

- run_local_worker.sh: 동시성(WORKER_CONCURRENCY)·Chrome 프로필(LPS_PROFILE_DIR)
  선택 후 워커 기동. 실행 중 워커 감지 시 교체 여부 확인. venv/config 자동 보장.
- run_loadtest_gui.sh: Locust 웹 UI(:8089) 기동. 워커 실행 중이면 경고(크롤 비용),
  API 미기동 시 PROCESS_COUNT 받아 백그라운드 기동(종료 시 함께 정리). 부하생성기 --processes.
- .gitignore(lps): .profiles/(Chrome 쿠키·cf_clearance 세션) 커밋 방지
- README·operations: 새 스크립트 안내로 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-10 08:33:11 +09:00
parent 9c2432c818
commit 0e30354ba5
5 changed files with 164 additions and 10 deletions

5
lps/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Chrome 프로필(쿠키·cf_clearance 세션) — 로컬 워커 영속 볼륨. 절대 커밋하지 않는다.
.profiles/
# 로컬 워커 하트비트/부하 테스트 로그(기본은 /tmp 지만 상대경로로 뜰 때 대비)
lps_worker_heartbeat

View File

@ -72,7 +72,10 @@ cp config/config.local.toml.example config/config.local.toml # 값 채우기
./run_local_server.sh # → http://localhost:9600/docs ./run_local_server.sh # → http://localhost:9600/docs
# 3) 워커 실행 (실제 검색 수행) — 별도 터미널 # 3) 워커 실행 (실제 검색 수행) — 별도 터미널
python worker_main.py ./run_local_worker.sh # 대화형: 동시성·프로필 선택 (또는 python worker_main.py)
# (선택) 부하 테스트 GUI — Locust 웹 UI(:8089)
./run_loadtest_gui.sh # 브라우저에서 users/spawn 조절하며 RPS/지연 관측
``` ```
간단 테스트: 간단 테스트:
@ -104,7 +107,9 @@ lps/
├── web_main.py # API 서버 진입점 (요청 접수) ├── web_main.py # API 서버 진입점 (요청 접수)
├── worker_main.py # 워커 진입점 (검색+웜업+유휴정리+ops모니터) ├── worker_main.py # 워커 진입점 (검색+웜업+유휴정리+ops모니터)
├── Dockerfile # API 이미지(lean) · Dockerfile.worker # 워커(Chromium+Xvfb) ├── Dockerfile # API 이미지(lean) · Dockerfile.worker # 워커(Chromium+Xvfb)
├── run_local_server.sh # 로컬 API 실행 스크립트 ├── run_local_server.sh # 로컬 API 실행 (대화형)
├── run_local_worker.sh # 로컬 워커 실행 (대화형: 동시성·프로필)
├── run_loadtest_gui.sh # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형)
├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입) ├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입)
├── common/ # 공통(enums, DB 세션, 모델, 로거) ├── common/ # 공통(enums, DB 세션, 모델, 로거)
│ └── database/model/models.py # DB 테이블 정의 │ └── database/model/models.py # DB 테이블 정의

View File

@ -35,20 +35,21 @@ psql -h 127.0.0.1 -U postgres -d lps_db -c "CREATE EXTENSION IF NOT EXISTS pgcry
**워커** (실제 검색 수행) — 별도 터미널 **워커** (실제 검색 수행) — 별도 터미널
```bash ```bash
python worker_main.py ./run_local_worker.sh # 대화형: 동시성(WORKER_CONCURRENCY)·Chrome 프로필 선택
# 로그를 실시간으로 보려면: # 또는 직접:
PYTHONUNBUFFERED=1 python worker_main.py PYTHONUNBUFFERED=1 python worker_main.py # 로그 실시간
# 상품 여러 개를 동시에 검색(워커별 브라우저 세트 · 다른 IP): WORKER_CONCURRENCY=3 python worker_main.py # 동시성 2~3(로컬). Chrome 최대 4×N개
WORKER_CONCURRENCY=3 python worker_main.py # 권장 2~3(로컬). Chrome 최대 4×N개
``` ```
> 워커 실행 시 쿠팡 크롤링용 **Chrome 창이 뜹니다**(정상). 기동 로그에 `DECODO 프리플라이트 OK — egress IP ...`, `AI: ON/OFF`가 표시됩니다. > 워커 실행 시 쿠팡 크롤링용 **Chrome 창이 뜹니다**(정상). 기동 로그에 `DECODO 프리플라이트 OK — egress IP ...`, `AI: ON/OFF`가 표시됩니다.
> 동시성 N이면 상품 N개가 진짜 병렬 처리됩니다(각 워커가 자기 프로필·프록시 IP 사용). > 동시성 N이면 상품 N개가 진짜 병렬 처리됩니다(각 워커가 자기 프로필·프록시 IP 사용).
**부하 테스트** (여러 상품 동시 검색 측정) **부하 테스트**
```bash ```bash
N=8 python loadtest.py # 상품 8개 제출→처리량·지연(p50/p95)·AI/DECODO/총비용 집계 N=8 python loadtest.py # e2e: 상품 8개 제출→처리량·지연(p50/p95)·AI/DECODO/총비용 집계 (워커 필요)
./run_loadtest_gui.sh # API 부하: Locust 웹 UI(:8089)에서 RPS/지연 실시간 관측 (워커 OFF)
``` ```
> 워커 동시성만큼 병렬 처리됩니다(동시성 낮으면 큐에서 순차 대기 — 그게 부하 관측 포인트). > e2e(loadtest.py)는 워커 동시성만큼 병렬 처리됩니다(동시성 낮으면 큐에서 순차 대기 — 그게 부하 관측 포인트).
> API 부하(GUI)는 enqueue/조회 경로만 측정하므로 **워커를 끄고** 실행합니다(실제 크롤 비용 회피).
## 2-1. 멀티코어 스케일 & 커넥션 풀 (자동) ## 2-1. 멀티코어 스케일 & 커넥션 풀 (자동)

84
lps/run_loadtest_gui.sh Executable file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env bash
#
# LPS 부하 테스트 — Locust 웹 UI(GUI) 실행 (대화형).
# 브라우저(:8089)에서 동시 유저 수·spawn rate 를 조절하며 실시간 그래프로 RPS/지연을 본다.
# 대상 = API 서버(web_main, :9600). enqueue/조회 경로에 부하를 준다.
#
# ⚠️ 워커는 끄고 실행하라 — 부하 중 실제 크롤(프록시/AI 비용)이 발생한다.
# 쌓인 부하 잡 정리: psql -h 127.0.0.1 -U postgres -d lps_db -c "TRUNCATE job;"
#
set -euo pipefail
cd "$(dirname "$0")" # lps/
VENV=".venv"
PY="$VENV/bin/python"
LOCUST="$VENV/bin/locust"
HOST="http://localhost:9600"
PORT=9600
WEBUI_PORT=8089
# 1) venv + locust 보장
if [[ ! -d "$VENV" ]]; then
echo "[setup] venv 생성 + 의존성 설치..."
python3 -m venv "$VENV"
"$PY" -m pip install -q --upgrade pip
"$PY" -m pip install -q -r requirements.txt
fi
if [[ ! -x "$LOCUST" ]]; then
echo "[setup] locust 설치..."; "$PY" -m pip install -q locust
fi
# 2) 워커가 떠 있으면 경고(부하 중 실제 크롤=비용)
if pgrep -f worker_main.py >/dev/null 2>&1; then
echo "[warn] 워커(worker_main.py)가 실행 중입니다 — 부하 중 실제 크롤 비용(프록시/AI)이 발생합니다."
read -rp " 워커를 종료할까요? [Y/n]: " k
[[ "${k:-Y}" =~ ^[Nn] ]] || { pkill -f worker_main.py || true; sleep 1; echo " 워커 종료"; }
fi
# 3) API 기동 확인 — 없으면 띄울지 물어본다
API_STARTED=""
if curl -s "$HOST/healthz" >/dev/null 2>&1; then
echo "[info] API 서버 감지됨 ($HOST)"
else
echo "[info] API 서버($HOST)가 응답하지 않습니다."
read -rp " 이 스크립트가 API 를 띄울까요? [Y/n]: " s
if [[ ! "${s:-Y}" =~ ^[Nn] ]]; then
read -rp " 멀티코어 PROCESS_COUNT [1] (풀은 자동 산정): " PC; PC="${PC:-1}"
echo " [run] web_main.py (PROCESS_COUNT=$PC) 백그라운드 기동..."
PROCESS_COUNT="$PC" APP_ENV=local "$PY" web_main.py > /tmp/lps_api_loadtest.log 2>&1 &
API_STARTED=$!
for _ in $(seq 1 20); do curl -s "$HOST/healthz" >/dev/null 2>&1 && break; sleep 1; done
if curl -s "$HOST/healthz" >/dev/null 2>&1; then
echo " API 기동 완료 (로그: /tmp/lps_api_loadtest.log)"
else
echo " [error] API 기동 실패 — /tmp/lps_api_loadtest.log 확인"; kill "$API_STARTED" 2>/dev/null || true; exit 1
fi
else
echo " [안내] 먼저 다른 터미널에서 ./run_local_server.sh 로 API 를 띄우세요."; exit 1
fi
fi
# 4) 부하 생성기 멀티프로세스(locust 자체가 병목되지 않게). 코어 많으면 늘린다.
CORES=$("$PY" -c "import os;print(os.cpu_count())")
read -rp "부하 생성기 프로세스 수 --processes [4] (CPU=$CORES): " LP; LP="${LP:-4}"
# API 를 이 스크립트가 띄웠으면 종료 시 함께 내린다
cleanup() { [[ -n "$API_STARTED" ]] && { echo ""; echo "[info] 스크립트가 띄운 API 종료"; kill "$API_STARTED" 2>/dev/null || true; }; }
trap cleanup EXIT
echo ""
echo "════════════════════════════════════════════════════════"
echo " Locust 웹 UI: http://localhost:$WEBUI_PORT"
echo " 대상 API : $HOST"
echo " → 브라우저에서 users/spawn rate 입력 후 START"
echo " 중단: Ctrl+C · 끝나면 쌓인 잡 정리:"
echo " psql -h 127.0.0.1 -U postgres -d lps_db -c 'TRUNCATE job;'"
echo "════════════════════════════════════════════════════════"
echo ""
# API 를 이 스크립트가 띄웠으면 exec 하지 않는다(종료 시 trap 으로 API 를 함께 내리기 위해).
if [[ -n "$API_STARTED" ]]; then
"$LOCUST" -f loadtest/locustfile.py --host "$HOST" --web-port "$WEBUI_PORT" --processes "$LP"
else
exec "$LOCUST" -f loadtest/locustfile.py --host "$HOST" --web-port "$WEBUI_PORT" --processes "$LP"
fi

59
lps/run_local_worker.sh Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
#
# 로컬 LPS 워커 실행 (대화형). 실행하면 동시성 등을 골라 입력한다.
# 최초 실행 시 venv 생성 + 의존성 설치까지 자동으로 한다.
# 워커 = 실제 검색 수행(네이버/쿠팡/오픈마켓 크롤). 실행 시 쿠팡용 Chrome 창이 뜬다(정상).
#
set -euo pipefail
cd "$(dirname "$0")" # lps/
VENV=".venv"
PY="$VENV/bin/python"
# 1) venv + 의존성 보장
if [[ ! -d "$VENV" ]]; then
echo "[setup] venv 생성 + 의존성 설치..."
python3 -m venv "$VENV"
"$PY" -m pip install -q --upgrade pip
"$PY" -m pip install -q -r requirements.txt
fi
# 2) config 보장
if [[ ! -f config/config.local.toml ]]; then
echo "[error] config/config.local.toml 이 없습니다. 아래로 생성 후 값을 채우세요:"
echo " cp config/config.local.toml.example config/config.local.toml"
exit 1
fi
# 3) 이미 워커가 떠 있으면 안내
if pgrep -f worker_main.py >/dev/null 2>&1; then
echo "[info] 이미 워커(worker_main.py)가 실행 중입니다."
read -rp "기존 워커를 종료하고 새로 띄울까요? [y/N]: " kill_old
if [[ "${kill_old:-N}" =~ ^[Yy] ]]; then
pkill -f worker_main.py || true; sleep 1; echo "[info] 기존 워커 종료"
else
echo "취소합니다."; exit 0
fi
fi
# 4) 동시성 입력 (상품 동시 검색 수 · 워커별 브라우저 세트 · Chrome 최대 4×N개)
echo "── 워커 설정 ──"
read -rp "동시 검색 수 WORKER_CONCURRENCY [3] (로컬 권장 2~3): " CONC
CONC="${CONC:-3}"
# 5) Chrome 프로필 영속 디렉터리 (재시작해도 cf_clearance 유지 → 재웜업 회피). 비우면 기본(/tmp)
read -rp "Chrome 프로필 디렉터리 LPS_PROFILE_DIR [.profiles] (엔터=유지): " PROFILE
PROFILE="${PROFILE:-.profiles}"
mkdir -p "$PROFILE"
export APP_ENV=local
export WORKER_CONCURRENCY="$CONC"
export LPS_PROFILE_DIR="$PROFILE"
export PYTHONUNBUFFERED=1 # 로그 실시간 출력
echo ""
echo "[run] worker_main.py (동시성=$CONC · 프로필=$PROFILE)"
echo " 기동 로그의 'DECODO 프리플라이트 OK — egress IP ...' / 'AI: ON/OFF' 확인"
echo " 중단: Ctrl+C"
echo ""
exec "$PY" worker_main.py