diff --git a/lps/.gitignore b/lps/.gitignore new file mode 100644 index 0000000..6aaa448 --- /dev/null +++ b/lps/.gitignore @@ -0,0 +1,5 @@ +# Chrome 프로필(쿠키·cf_clearance 세션) — 로컬 워커 영속 볼륨. 절대 커밋하지 않는다. +.profiles/ + +# 로컬 워커 하트비트/부하 테스트 로그(기본은 /tmp 지만 상대경로로 뜰 때 대비) +lps_worker_heartbeat diff --git a/lps/README.md b/lps/README.md index 9da9abe..b2085eb 100644 --- a/lps/README.md +++ b/lps/README.md @@ -72,7 +72,10 @@ cp config/config.local.toml.example config/config.local.toml # 값 채우기 ./run_local_server.sh # → http://localhost:9600/docs # 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 서버 진입점 (요청 접수) ├── worker_main.py # 워커 진입점 (검색+웜업+유휴정리+ops모니터) ├── 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 주입) ├── common/ # 공통(enums, DB 세션, 모델, 로거) │ └── database/model/models.py # DB 테이블 정의 diff --git a/lps/docs/operations.md b/lps/docs/operations.md index 39e894e..e2dc610 100644 --- a/lps/docs/operations.md +++ b/lps/docs/operations.md @@ -35,20 +35,21 @@ psql -h 127.0.0.1 -U postgres -d lps_db -c "CREATE EXTENSION IF NOT EXISTS pgcry **워커** (실제 검색 수행) — 별도 터미널 ```bash -python worker_main.py -# 로그를 실시간으로 보려면: -PYTHONUNBUFFERED=1 python worker_main.py -# 상품 여러 개를 동시에 검색(워커별 브라우저 세트 · 다른 IP): -WORKER_CONCURRENCY=3 python worker_main.py # 권장 2~3(로컬). Chrome 최대 4×N개 +./run_local_worker.sh # 대화형: 동시성(WORKER_CONCURRENCY)·Chrome 프로필 선택 +# 또는 직접: +PYTHONUNBUFFERED=1 python worker_main.py # 로그 실시간 +WORKER_CONCURRENCY=3 python worker_main.py # 동시성 2~3(로컬). Chrome 최대 4×N개 ``` > 워커 실행 시 쿠팡 크롤링용 **Chrome 창이 뜹니다**(정상). 기동 로그에 `DECODO 프리플라이트 OK — egress IP ...`, `AI: ON/OFF`가 표시됩니다. > 동시성 N이면 상품 N개가 진짜 병렬 처리됩니다(각 워커가 자기 프로필·프록시 IP 사용). -**부하 테스트** (여러 상품 동시 검색 측정) +**부하 테스트** ```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. 멀티코어 스케일 & 커넥션 풀 (자동) diff --git a/lps/run_loadtest_gui.sh b/lps/run_loadtest_gui.sh new file mode 100755 index 0000000..443d7e1 --- /dev/null +++ b/lps/run_loadtest_gui.sh @@ -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 diff --git a/lps/run_local_worker.sh b/lps/run_local_worker.sh new file mode 100755 index 0000000..b5df08f --- /dev/null +++ b/lps/run_local_worker.sh @@ -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