쿠팡 크롤 IP 를 '막힐 때까지' 쓰던 방식을 '막히기 전에 교체'로 전환한다. - 요청 예산(LPS_IP_REQUEST_BUDGET, 기본 3): IP당 요청 수가 예산에 닿으면 차단 전에 선제 회전. 실측상 5회 부근 차단 이력이 있어 보수적으로 3회. 선제 교체된 포트는 평판이 깨끗해 로테이션 복귀 시 재사용된다. - 포트 쿨다운(LPS_PORT_COOLDOWN_SEC, 기본 max(sticky,30분)): 차단 감지· 전송오류 포트는 격리하고 _port() 가 건너뛴다. 전 포트 쿨다운이면 만료 임박 포트 사용(가용성 우선). 포트 수는 config 범위에서 동적 산출. - 차단 재시도 소진 시에도 회전 예약 — 불탄 포트로 다음 검색을 하지 않음. - ip_session 테이블 신설: 세션마다 요청 수·성공/차단·종료 사유(budget/ block/proxy_error/window/idle/shutdown)를 기록. bot_detection 과 달리 무사 종료도 남아 예산 상한 튜닝의 원천 데이터가 된다(쿼리 database.md). models.py·migrations·init.sql(lps_db 섹션) 동행 갱신, dev DB 적용 완료. - 테스트 17건 추가(쿨다운·예산 판정·세션 기록·CRUD), 전체 126 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
운영 가이드 — 실행 · 로그 · DB · 문제 해결
1. 사전 준비
필요한 것: Python 3.12+ (로컬은 3.14), PostgreSQL, Google Chrome(쿠팡 크롤링용)
설정 파일 (config/config.local.toml, git 미커밋)
cp config/config.local.toml.example config/config.local.toml
채워야 할 값:
| 섹션 | 값 |
|---|---|
[MainDBConfig] |
DB 접속(host/port/id/pw, name=lps_db) |
[NaverConfig].keys |
네이버 쇼핑 API 키(id/secret). 여러 개면 자동 로테이션 |
[OpenAIConfig] |
api_key (AI 판정·검색어 생성용) |
[DecodoConfig] |
프록시 정보(비워두면 프록시 미사용) |
한 파일에 설정+시크릿 통합 관리(로컬). Docker 이미지에는 이 파일이 들어가지 않는다 — 빌드 시
.dockerignore로 제외되고 example(플레이스홀더)이 대신 들어가며, 실값은 compose 의 env 로 주입한다(리포 루트.env, 템플릿.env.example).server_configs의 env override 가 DB 접속·OPENAI_API_KEY·DECODO_*(포트 포함)·NAVER_KEYS를 모두 덮는다.
DB 준비: lps_db 생성 후 최초 실행 시 테이블 자동 생성.
createdb -h 127.0.0.1 -U postgres lps_db
psql -h 127.0.0.1 -U postgres -d lps_db -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
2. 실행
API 서버 (요청 접수)
./run_local_server.sh # → http://localhost:9600/docs
워커 (실제 검색 수행) — 별도 터미널
./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개
LPS_FALLBACKS=gmarket,auction,st11 python worker_main.py # 오픈마켓 폴백 재가동(기본 OFF — decision 문서 참고)
워커 실행 시 쿠팡 크롤링용 Chrome 창이 뜹니다(정상). 기동 로그에
DECODO 프리플라이트 OK — egress IP ...,AI: ON/OFF가 표시됩니다. 동시성 N이면 상품 N개가 진짜 병렬 처리됩니다(각 워커가 자기 프로필·프록시 IP 사용).
워커 종료 (graceful)
Ctrl+C(SIGINT) 또는docker stop(SIGTERM) 1회 → 새 잡은 안 받고, 하던 잡을 마무리한 뒤 리스너·브라우저를 정리하고 종료합니다(LPS 워커 종료 완료로그, 트레이스백 없음).- 유예시간
LPS_SHUTDOWN_GRACE_SEC(기본 60s) 안에 안 끝나면 강제 취소되고, 그 잡은 lease 만료(120s) 후 reaper 가 재큐합니다. 한 번 더 신호를 보내면 즉시 강제 종료입니다. - Docker 는 compose 의
stop_grace_period: 75s(유예 60s + 정리 여유)가 SIGKILL 을 그만큼 미뤄줍니다 — 유예를 늘리면 이 값도 같이 늘리세요.
부하 테스트
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. 멀티코어 스케일 & 커넥션 풀 (자동)
API 서버는 asyncio(스레드 1개)라 1 프로세스 = 1 코어입니다. 처리량을 코어만큼 올리려면
process_count(uvicorn 워커 수)를 늘립니다 — 이때 DB 커넥션 풀은 config 가 자동으로 맞춰줍니다.
실제 동시 커넥션 = (pool_size + max_overflow) × 2엔진(R/W) × process_count
config 가 보장: 위 값 ≤ connection_budget (기본 40)
process_count를 올리면pool_size/max_overflow가 자동으로 축소되어 예산을 넘지 않습니다. (수동 튜닝 불필요 — 예전엔 이걸 안 맞춰서 워커↑ 시 커넥션 고갈→요청 실패가 났음)- 기동 로그에서 실효값 확인:
DB Pool : pool_size=.. max_overflow=.. × 2engine × Nworkers = M conns (budget=..) - 예산 조정: 공유 PG 는 40 유지, 전용 PG(
max_connections≈100)면DB_CONNECTION_BUDGET=90으로 상향. - env 로 조절(코드/toml 수정 없이):
PROCESS_COUNT,DB_CONNECTION_BUDGET, (특수 시)DB_POOL_SIZE/DB_MAX_OVERFLOW. - 부하 한계 측정은
loadtest/README.md참고(Locust 멀티코어 벤치).
3. 로그 보는 법 (워커 터미널)
| 로그 | 의미 |
|---|---|
DECODO 프리플라이트 OK — egress IP ... |
시작 시 살아있는 프록시 포트 선점 성공(egress IP 표시) |
[warmup:gmarket] 챌린지 통과·쿠키 확보 |
시작 웜업 — 챌린지 미리 풀어 쿠키 선점(실 작업 웜) |
[naver] query='...' → N건 |
네이버 검색 결과 수 |
[coupang] query='...' → N건 (ip_req#K) |
쿠팡 결과 수 / 이 IP로 K번째 요청 |
[gmarket/auction/st11] query='...' → N건 |
오픈마켓 폴백 크롤 결과 수 |
[ai] 판정 N건 중 매칭 M건 |
AI 같은상품 선별 결과 |
[coupang][BOT-DETECTED] ... marker='...' |
봇 감지(마커별) → IP 회전 |
[gmarket] IP 회전 — 프록시 전송오류/봇 감지 |
프록시 죽음(407/터널) 또는 차단 → 새 IP |
[fallback:gmarket] 데드라인 15s 초과 → 스킵 |
폴백 크롤이 시간 상한 초과 → 그 몰만 스킵 |
[coupang] 유휴 120s 초과 → 브라우저 정리 |
유휴 브라우저 닫아 메모리 회수(다음 검색 때 재기동) |
[worker-0] done <id> / fail ... → DEAD |
작업 완료 / 실패 |
디버그 로그가 안 보이면
config.local.toml의[LogConfig] log_level = "debug"확인.
4. DB 조회 (유용한 쿼리)
psql -h 127.0.0.1 -U postgres -d lps_db
-- 큐 상태 요약 (1=대기 2=처리중 3=완료 4=실패)
SELECT status, count(*) FROM job GROUP BY status;
-- 최근 작업 결과
SELECT job_id, status, result->>'outcome' AS outcome,
result->'lowest'->>'price' AS lowest, result->'sources' AS sources
FROM job ORDER BY created_at DESC LIMIT 5;
-- 특정 상품의 최저가 이력(그래프 원본) + 몰별 스냅샷
SELECT triggered_at, naver_lowest, coupang_lowest, final_lowest, final_source, by_mall
FROM price_history WHERE product_code='T1' ORDER BY triggered_at;
-- 검색 원가(최근 완료 작업의 metrics)
SELECT job_id,
result->'metrics'->'cost'->>'total_usd' AS 총비용,
result->'metrics'->'cost'->>'proxy_usd' AS DECODO,
result->'metrics'->'crawl'->>'proxy_bytes' AS 전송바이트,
result->'metrics'->>'duration_ms' AS 소요ms
FROM job WHERE status=3 ORDER BY updated_at DESC LIMIT 5;
-- 봇 감지 패턴 (IP당 평균 몇 요청 만에 감지?)
SELECT avg(ip_request_no), count(*) FROM bot_detection;
-- 네거티브 캐시(없음으로 기록된 상품)
SELECT key, until, reason FROM search_negative ORDER BY created_at DESC;
4-1. 관측·알림 (모니터링)
| 엔드포인트/신호 | 용도 |
|---|---|
GET /healthz |
liveness — 프로세스 살아있는지(DB 무관) |
GET /readyz |
readiness — DB 도달성까지 확인(실패 503). LB/오케스트레이터용 |
GET /v1/lps/ops |
운영 스냅샷: 큐 카운트 + oldest_pending_sec(큐 지연) + dead_1h + stuck_running + blocks_1h(최근 차단). 외부 모니터가 스크랩·알림 |
| 워커 하트비트 | /tmp/lps_worker_heartbeat(mtime) — 컨테이너 HEALTHCHECK 가 신선도<120s 로 행/좀비 워커 감지 |
실시간 대시보드(로컬): ./run_monitor.sh → http://localhost:9700 — 큐 추이·처리량(개/분)·
코어별 CPU·프로세스 그룹(worker/api/chrome/postgres) 사용률을 2초 간격으로 시각화.
부하테스트/e2e(N=100 python loadtest.py) 관측용. 상세는 loadtest/README.md.
임계 알림(워커 ops-monitor): 초과 시 WARN 로그 + (env 있으면) Slack 호환 웹훅.
LPS_ALERT_WEBHOOK=https://hooks.slack.com/... # 있으면 알림 전송
LPS_ALERT_DEAD_1H=20 LPS_ALERT_BLOCKS_1H=80 LPS_ALERT_QUEUE_LAG_SEC=300
5. 테스트
python -m pytest # 단위·통합(96) — 브라우저/네트워크 불필요
LPS_LIVE=1 python -m pytest tests/test_browser_base.py::test_live_smoke # 라이브 스모크(셀렉터·안티봇 드리프트 감지)
⚠️ 워커가 실행 중이면 테스트가 깨집니다 — 워커가 같은
lps_db의 테스트 작업을 가로채기 때문. 테스트 전 워커를 멈추세요:pkill -f worker_main.py라이브 스모크는 IP 의존·느려서 기본 skip. 배포 후 셀렉터가 깨졌는지 수동/야간 점검용.
6. 문제 해결
| 증상 | 원인 / 해결 |
|---|---|
| 포트 9600 사용 중 | lsof -ti:9600 | xargs kill 후 재실행 |
| 백그라운드 실행 시 로그 안 보임 | print 버퍼링 → PYTHONUNBUFFERED=1 붙여 실행 |
프리플라이트 실패/모든 크롤 실패 |
DECODO 프록시 문제 — 대시보드에서 잔여 트래픽·플랜·자격증명 확인(407=인증거부). 게이트 다운이면 네이버(직접)만 동작 |
| G마켓 결과 계속 0건 | Cloudflare Turnstile 미통과(나쁜 IP는 인터랙티브 체크박스) — 웜업 IP회전 재시도로 완화. 지연 부담이면 폴백 데드라인이 스킵 |
쿠팡 blocked=True(Access Denied 등) |
Akamai 차단 → 자동 IP 회전(감지 이력 bot_detection). 반복되면 프록시 IP 풀 확대 |
| Chrome이 계속 쌓임 | 유휴 정리(120s)가 닫음. 스파이크/이전 워커 잔여는 pkill -f "user-data-dir=/tmp/lps_" |
| AI 매칭이 0건 자주 발생 | 검색어 모호/스펙 불일치 → product_name/specification을 더 정확히 |
| 검색이 너무 느림/비쌈 | result.metrics로 소스별 시간·DECODO 바이트 확인. 대역폭이 대부분(오픈마켓 크롤) |
result.desc = LPS_JOB_NOT_FOUND |
존재하지 않거나 잘못된 job_id |
7. Docker 배포
# 루트에서 (DB 는 외부 PostgreSQL, host.docker.internal 로 연결)
docker compose build lps-api lps-worker
docker compose up -d lps-api lps-worker
docker logs -f lps-worker # 웜업·검색 로그
docker ps # lps-worker "(healthy)" 확인
- 워커 = 헤드풀 Chromium + Xvfb(
Dockerfile.worker): headless 는 Akamai·Cloudflare Turnstile 에 탐지됨(실측). Xvfb 가상 디스플레이로 headful 실행. - API = lean(
Dockerfile, 브라우저 불필요). - 시크릿은 이미지에 없음(강제): 이미지는 example config 로 빌드된다(
.dockerignore가 config.local.toml·.profiles/제외). 실값은 리포 루트.env(템플릿.env.example)에서 compose env 로 주입..env없이 뜨면 AI OFF·프록시 미사용으로 조용히 동작하니, 기동 로그의AI: ON/OFF·DECODO 프록시: ON/OFF로 주입 성공을 반드시 확인할 것. - Chrome 프로필 영속 볼륨(
lps-profiles:/profiles,LPS_PROFILE_DIR): 재시작해도 cf_clearance 유지 → 재웜업 회피. - 워커 헬스: HEALTHCHECK(하트비트<120s)로 행 워커 감지. compose 의
restart는 unhealthy 를 재시작하지 않으므로 autoheal 컨테이너(라벨autoheal=true감시)가 재시작 담당. k8s 는 liveness probe 로 대체. - 잡 데드라인: 잡 1건 300s 상한(
LPS_JOB_DEADLINE_SEC) — 크롤 행이 워커 슬롯을 영구 점유하지 못하게 함. - IP 선제 회전:
LPS_IP_REQUEST_BUDGET(기본 3) — IP당 요청 예산, 도달 시 차단 전에 회전(0=비활성).LPS_PORT_COOLDOWN_SEC(기본 max(sticky, 1800)) — 차단 감지된 포트 격리 시간. 포트 수를 늘리면 (DECODO_PORT_START/END) 자동 반영 — 코드에 포트 수 하드코딩 없음. 튜닝은ip_session분석 쿼리(database.md) 참고.
남은 배포 과제: API 인증·레이트리밋(비용 남용 방지), 다중 레플리카 시 분산 레이트리밋/프록시 IP 조정. 비용: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 TTL 캐시가 다음 절감 후보.