docs(lps): README·docs 최신화 — IP 선제 회전·알림 10룰·guard·ip_session 반영

이번 기능 3종+알림 확장 이후 문서와 코드의 어긋남을 정리한다.

- README: 주요 기능 표에 선제 회전(예산 3회)·임계 알림·API guard 추가,
  차단 대응 설명을 '막히기 전 교체' 순서로 재서술, 폴더 구조에
  alerts/ip_session 반영, 데이터베이스 문서 링크를 5종으로 수정.
- api.md: /v1/lps/ops 운영 스냅샷 섹션 신설(필드 주석 포함),
  /readyz 문서화, HTTP 401(guard) 상태 코드 추가.
- architecture.md: 구성요소 표에 관측·알림/API guard 행 추가.
- database.md: 제목 '테이블 5종'으로 수정.
- operations.md: 테스트 수 96→145, 로그 읽는 법에 예산 선제 회전·
  포트 쿨다운 로그 2행 추가.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-13 20:46:59 +09:00
parent ca7f057e41
commit 993301be8b
5 changed files with 35 additions and 11 deletions

View File

@ -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)

View File

@ -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` 누락/불일치 — 키 주입 확인(상단 인증 참고)

View File

@ -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). 분리하면 요청이 밀리지 않고, 워커만 따로 늘릴 수 있습니다.

View File

@ -6,7 +6,7 @@
- **테이블 정의**: `common/database/model/models.py` (SQLAlchemy) — 이 파일이 스키마의 단일 출처
- **공통 규칙**: 외래키(FK) 안 씀(무결성은 앱에서) · 코드값은 정수(SMALLINT) · 시각은 전부 `TIMESTAMPTZ`(UTC)
## 테이블 4종 한눈에
## 테이블 5종 한눈에
| 테이블 | 용도 |
|--------|------|

View File

@ -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`의 테스트 작업을 가로채기 때문. 테스트 전 워커를 멈추세요: