diff --git a/lps/README.md b/lps/README.md index 9008ff4..0af9c14 100644 --- a/lps/README.md +++ b/lps/README.md @@ -39,7 +39,7 @@ **핵심 포인트** - **즉시 응답 + 나중 처리**: 요청하면 바로 "접수번호(job_id)"를 주고, 실제 검색은 뒤에서 진행됩니다. (검색은 몇 초~수십 초 걸림) - **못 찾으면 검색어를 바꿔 재시도**: "맥심 커피"로 안 나오면 "맥심 모카골드 커피믹스"처럼 **AI가 검색어를 다듬어** 다시 시도하고, 그래도 없으면 "없음"으로 정리합니다. (무한 재시도 안 함) -- **차단 대응**: 쿠팡(Akamai)·G마켓(Cloudflare 사람확인) 등이 봇으로 감지하면 **다른 IP로 바꿔** 재시도하고, 시작 시 챌린지를 미리 풀어(웜업) 실 작업을 빠르게 합니다. +- **차단 대응**: IP당 요청 예산(기본 3회)에 닿으면 **차단당하기 전에 IP를 선제 교체**하고(평판 보존 — 그 IP는 로테이션 복귀 시 재사용), 그래도 감지되면 그 포트를 쿨다운 격리 후 다른 IP로 재시도합니다. 시작 시 챌린지를 미리 풀어(웜업) 실 작업을 빠르게 합니다. - **원가 투명**: 검색 1건이 쓴 AI 비용·프록시 대역폭·시간을 함께 기록합니다. --- @@ -56,7 +56,9 @@ | 검색 원가 계측 | 검색 1건의 AI 토큰·비용 + DECODO 대역폭(실측 CDP) + 시간을 집계 | | 다중 상품 병렬 | 워커별 브라우저 세트로 여러 상품 동시 검색(`WORKER_CONCURRENCY`) | | 안정적 큐 처리 | 작업 유실 없이 순서대로, 실패 시 자동 재시도 | -| 프록시 IP 회전 | 봇 감지·전송오류 시 IP 자동 순환 + 시작 웜업(DECODO) | +| 프록시 IP 선제 회전 | 요청 예산(기본 3회) 도달 시 **차단 전 선제 교체** + 불탄 포트 쿨다운 + 봇 감지·전송오류 즉시 순환 + 시작 웜업(DECODO). 예산 튜닝용 `ip_session` 관측 로그 | +| 임계 알림 | 큐·차단·DB풀·소스별 장기실패·비용 등 10룰 — 쿨다운(스팸 방지)·해소 알림, Slack 웹훅([룰 표](docs/operations.md)) | +| API guard | `LPS_API_KEY` 설정 시 `/v1` 전체 X-API-Key 검증(개발은 미설정=개방 모드) | --- @@ -93,7 +95,7 @@ curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \ | 문서 | 대상 | 내용 | |------|------|------| | **[아키텍처](docs/architecture.md)** | 개발자/기획자 | 구성요소·파이프라인·안티봇(Akamai/Turnstile)·비용계측·동시성 | -| **[데이터베이스](docs/database.md)** | 개발자/기획자 | 테이블 4종 구조와 코드값(+by_mall) | +| **[데이터베이스](docs/database.md)** | 개발자/기획자 | 테이블 5종 구조와 코드값(+by_mall·ip_session) | | **[API 사용법](docs/api.md)** | 연동 개발자 | 엔드포인트·요청/응답·metrics 예시 | | **[운영 가이드](docs/operations.md)** | 운영자/개발자 | 실행·병렬·관측(readyz/ops/알림)·**Docker 배포**·문제 해결 | | **[크롤러 논의](docs/decision-openmarket-crawler.md)** | 팀 | 오픈마켓 크롤러 유지 여부(ROI) 의사결정 메모 | @@ -111,14 +113,14 @@ lps/ ├── run_local_worker.sh # 로컬 워커 실행 (대화형: 동시성·프로필) ├── run_loadtest_gui.sh # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형) ├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입) -├── common/ # 공통(enums, DB 세션, 모델, 로거) +├── common/ # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자) │ └── database/model/models.py # DB 테이블 정의 ├── loadtest.py # 부하 테스트 (N개 상품 → 처리량·지연·비용 집계) -├── crud/ # DB 접근 (job_crud, price_history, negative_cache, bot_detection) +├── crud/ # DB 접근 (job_crud, price_history, negative_cache, bot_detection, ip_session) ├── services/ │ ├── search/ # 소스 어댑터 (coupang, naver, esm=G마켓·옥션, st11=11번가) │ │ ├── browser_base.py # patchright 공통(수명·프록시회전·차단감지·CDP 바이트계측) -│ │ ├── proxy.py # DECODO(IP 회전·프리플라이트) +│ │ ├── proxy.py # DECODO(IP 회전·포트 쿨다운·프리플라이트) │ │ └── card_parser.py # 오픈마켓 공용 카드 파서 │ ├── pipeline/ # 필터·이상치·최저가 정렬(+몰별 분해) │ ├── ai/ # AI 유사도 판정·검색어 생성 (OpenAI) diff --git a/lps/docs/api.md b/lps/docs/api.md index 015fed9..cf1921e 100644 --- a/lps/docs/api.md +++ b/lps/docs/api.md @@ -152,11 +152,29 @@ curl "localhost:9600/v1/lps/products/T1/history?limit=100" { "result": {...}, "counts": { "PENDING": 0, "RUNNING": 1, "DONE": 12, "DEAD": 0 } } ``` -## 5. 헬스체크 — `GET /healthz` -서버 기동 시각을 반환(살아있는지 확인용). +## 5. 운영 스냅샷 — `GET /v1/lps/ops` + +외부 모니터가 스크랩·임계 알림하기 좋은 **플랫 JSON**. 알림 룰·임계는 [운영 가이드](operations.md) 참고. + +```json +{ + "pending": 0, "running": 1, "done": 12, "dead": 0, + "dead_1h": 0, // 최근 1h 재시도 소진 실패 + "stuck_running": 0, // lease 만료/장기 실행 좀비 신호 + "oldest_pending_sec": 3, // 큐 지연(가장 오래된 대기 잡) + "blocks_1h": 0, // 최근 1h 봇 감지 수 + "deadline_1h": 0, // 최근 1h 잡 데드라인 강제종료(크롤 행 신호) + "cost_1h_usd": 0.09, // 최근 1h 완료 잡 검색원가 합($) + "pool_checked_out": 0, "pool_capacity": 40, "pool_pct": 0 // API 프로세스 DB 풀 사용률 +} +``` + +## 6. 헬스체크 — `GET /healthz` · `GET /readyz` +`/healthz`: 서버 기동 시각 반환(liveness — 프로세스 생존만). `/readyz`: DB 도달성까지 확인(실패 시 503) — LB/오케스트레이터용. 둘 다 **guard 대상이 아니라 항상 개방**. --- ## 상태 코드 요약 - **작업 상태**: `PENDING`(대기) · `RUNNING`(처리중) · `DONE`(완료) · `DEAD`(실패-확인필요) - **결과 outcome**: `found`(찾음) · `not_found`(검색했으나 같은 상품 없음) +- **HTTP 401**: guard 활성 환경에서 `X-API-Key` 누락/불일치 — 키 주입 확인(상단 인증 참고) diff --git a/lps/docs/architecture.md b/lps/docs/architecture.md index 52c131d..6fed3e1 100644 --- a/lps/docs/architecture.md +++ b/lps/docs/architecture.md @@ -13,6 +13,8 @@ | **오픈마켓 폴백** | `services/search/{esm,st11}/` | G마켓·옥션·11번가 크롤 — 네이버가 그 몰을 커버 못 했을 때만 (BrowserSearchAdapter 공유). **기본 비활성**(`LPS_FALLBACKS`) | | **파이프라인** | `services/pipeline/` | 수집 결과를 필터·이상치 제거·최저가 정렬 | | **AI** | `services/ai/` | "같은 상품" 판정 + 검색어 생성 (OpenAI) | +| **관측·알림** | `common/alerts.py` + 워커 ops-monitor | 큐·차단·DB풀·비용 등 10룰 임계 알림(쿨다운·해소 알림, Slack 웹훅) + 하트비트. API 도 자기 풀을 자체 감시. [룰 표](operations.md) | +| **API guard** | `router/v1/validator/auth.py` | `LPS_API_KEY` 설정 시 `/v1` 전체 X-API-Key 검증(개발은 미설정=개방) | > **API와 워커를 분리**한 이유: 요청 접수는 즉시(가벼움), 실제 검색은 무거움(브라우저·AI). 분리하면 요청이 밀리지 않고, 워커만 따로 늘릴 수 있습니다. diff --git a/lps/docs/database.md b/lps/docs/database.md index 546f946..e6c87ba 100644 --- a/lps/docs/database.md +++ b/lps/docs/database.md @@ -6,7 +6,7 @@ - **테이블 정의**: `common/database/model/models.py` (SQLAlchemy) — 이 파일이 스키마의 단일 출처 - **공통 규칙**: 외래키(FK) 안 씀(무결성은 앱에서) · 코드값은 정수(SMALLINT) · 시각은 전부 `TIMESTAMPTZ`(UTC) -## 테이블 4종 한눈에 +## 테이블 5종 한눈에 | 테이블 | 용도 | |--------|------| diff --git a/lps/docs/operations.md b/lps/docs/operations.md index 2285b0e..0d4bde1 100644 --- a/lps/docs/operations.md +++ b/lps/docs/operations.md @@ -86,7 +86,9 @@ config 가 보장: 위 값 ≤ connection_budget (기본 40) | `[coupang] query='...' → N건 (ip_req#K)` | 쿠팡 결과 수 / 이 IP로 K번째 요청 | | `[gmarket/auction/st11] query='...' → N건` | 오픈마켓 폴백 크롤 결과 수 | | `[ai] 판정 N건 중 매칭 M건` | AI 같은상품 선별 결과 | -| `[coupang][BOT-DETECTED] ... marker='...'` | 봇 감지(마커별) → IP 회전 | +| `[coupang] IP 회전 — 요청예산 3회 도달` | 예산 선제 회전(정상 동작 — 차단 전 교체, 포트는 재사용됨) | +| `[proxy] 포트 10005 쿨다운 1800s — 활성 N/100` | 차단 감지된 포트 격리(만료까지 로테이션이 건너뜀) | +| `[coupang][BOT-DETECTED] ... marker='...'` | 봇 감지(마커별) → 포트 쿨다운 + IP 회전 | | `[gmarket] IP 회전 — 프록시 전송오류/봇 감지` | 프록시 죽음(407/터널) 또는 차단 → 새 IP | | `[fallback:gmarket] 데드라인 15s 초과 → 스킵` | 폴백 크롤이 시간 상한 초과 → 그 몰만 스킵 | | `[coupang] 유휴 120s 초과 → 브라우저 정리` | 유휴 브라우저 닫아 메모리 회수(다음 검색 때 재기동) | @@ -167,7 +169,7 @@ LPS_ALERT_COOLDOWN_MIN=30 # 같은 룰 재발송 억제 ## 5. 테스트 ```bash -python -m pytest # 단위·통합(96) — 브라우저/네트워크 불필요 +python -m pytest # 단위·통합(145) — 브라우저/네트워크 불필요 LPS_LIVE=1 python -m pytest tests/test_browser_base.py::test_live_smoke # 라이브 스모크(셀렉터·안티봇 드리프트 감지) ``` > ⚠️ **워커가 실행 중이면 테스트가 깨집니다** — 워커가 같은 `lps_db`의 테스트 작업을 가로채기 때문. 테스트 전 워커를 멈추세요: