o2o-negosium-original/lps
민헌 3b345929d4 feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리
한 DECODO 계정을 여러 워커 프로세스가 나눠 쓰는 전제로 전환한다. 인메모리 장부는
프로세스마다 따로라 (1) 같은 IP 를 동시에 잡고 (2) 한쪽이 태운 IP 를 다른 쪽이 곧바로
집으며 (3) 재시작하면 쿨다운이 통째로 사라졌다.

proxy_port 테이블 = 단일 진실. 상태는 세 시각으로만 표현한다(leased/rest/cooldown_until).
- acquire: 한 UPDATE 안에서 FOR UPDATE SKIP LOCKED 로 후보를 잠그고 임대까지 끝낸다
  (잡 큐와 같은 방식 — SELECT 후 UPDATE 로 나누면 그 틈에 다른 프로세스가 같은 행을 집는다)
- 회전은 LRU(last_used_at). 프로세스가 몇 개든 '가장 오래 안 쓴 IP'를 집으므로 전체가
  자연히 한 바퀴씩 돈다 → 프로세스별 seed_offset 계산 제거
- 죽은 프로세스 회수: leased_until 만료로 자동 복귀(별도 reaper 불필요)
- 차단·휴식은 전역이라 재시작해도 유지된다

DB 왕복은 비동기라 검색 루프(동기)에서 곧바로 못 한다 → 회전·차단을 pending 에 적어두고
ensure_port(브라우저 재기동 직전, async)에서 한 번에 flush. _close_ctx 에서도 flush 해
종료 시 유실(=태운 IP 를 남이 그대로 집는 상황)을 막는다.

**프로필 슬롯**(services/search/profile_slot): Chrome 은 user_data_dir 당 1 인스턴스다.
예전엔 워커 인덱스로만 갈라서 프로세스 2개면 같은 경로를 잡아 두 번째가 통째로 죽었다
(실측: 잡 3건 중 2건 DEAD, TargetClosedError). 파일 락으로 슬롯을 선점한다 — PID 경로가
아니라 슬롯이라 재시작 시 재사용돼 웜 쿠키(cf_clearance·Akamai)를 버리지 않는다.

검증: 프로세스 2개 동시 acquire 20회 → 중복 배정 0건. 워커 2프로세스 e2e → 잡 3건 모두
DONE(네이버가 삼다수 최저가 획득 8,960 < 13,200). 테스트 14건 추가, 전체 217 passed.
2026-08-05 11:44:54 +09:00
..
common feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00
config feat(lps): IP 를 태우지 말고 한계 직전까지 쓰고 쉬게 — 네이버 예산 상향 + 휴식 개념 2026-08-05 11:16:37 +09:00
crud feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00
docs feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화 2026-07-16 11:54:32 +09:00
loadtest refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
migrations feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00
router [fix] lps: 네거티브 캐시 히트 시 이력 누락 수정 + force 재검색 지원 2026-07-28 11:55:23 +09:00
services feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00
tests feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00
worker [fix] lps: 네거티브 캐시 히트 시 이력 누락 수정 + force 재검색 지원 2026-07-28 11:55:23 +09:00
.dockerignore fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
.gitignore feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형) 2026-07-10 08:33:11 +09:00
conftest.py feat(lps): 인터넷 최저가 검색 솔루션 프레임워크 골격 추가 2026-07-08 15:18:27 +09:00
Dockerfile fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
Dockerfile.worker fix(lps): 워커 재시작 시 Xvfb 가 stale lock 으로 죽어 모든 크롤이 실패하던 문제 2026-08-04 16:30:38 +09:00
loadtest.py test(lps): 부하테스트 상품셋 100종 카탈로그 분리 — 실제 입력 분포 재현 2026-07-10 10:50:28 +09:00
pytest.ini feat(lps): 인터넷 최저가 검색 솔루션 프레임워크 골격 추가 2026-07-08 15:18:27 +09:00
README.md feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화 2026-07-16 11:54:32 +09:00
requirements-api.txt fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
requirements.txt feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률 2026-07-10 09:50:14 +09:00
run_docker.sh feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화 2026-07-16 11:54:32 +09:00
run_loadtest_gui.sh feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형) 2026-07-10 08:33:11 +09:00
run_local_server.sh refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
run_local_worker.sh refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
run_monitor.sh feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률 2026-07-10 09:50:14 +09:00
web_main.py feat(lps): process_count 기반 커넥션 풀 자동 산정 — 멀티코어 풀 오버서브스크립션 방지 2026-07-09 23:59:05 +09:00
worker_main.py feat(lps): 포트(IP) 장부를 DB 로 올려 멀티 프로세스에서 안전하게 — 프로필 슬롯도 분리 2026-08-05 11:44:54 +09:00

LPS — 인터넷 최저가 검색 솔루션

상품 정보를 넣으면 네이버·쿠팡을 뒤져 "같은 상품"의 최저가를 찾아 돌려주고, 그 가격을 시간에 따라 기록해 그래프로 볼 수 있는 시스템입니다.


🧭 이게 뭔가요? (비개발자용 3줄 요약)

  1. "맥심 커피 (1박스, 160개입)" 같은 상품 정보를 보내면,
  2. 시스템이 네이버·쿠팡을 실제로 검색하고, AI가 "진짜 같은 상품"만 골라 최저가를 알려줍니다. (빨대·커버 같은 엉뚱한 액세서리는 걸러냅니다)
  3. 같은 상품을 여러 번 조회하면 가격 변화가 쌓여서, 네이버/쿠팡/최종 최저가를 그래프로 볼 수 있습니다.

왜 유용한가? 사람이 일일이 검색·비교하지 않아도, 필요한 상품만(조회할 때만) 자동으로 최저가를 찾고 가격 추이를 남깁니다.


⚙️ 어떻게 동작하나요? (워크플로우)

 [1] 검색 요청           [2] 대기줄(큐)         [3] 일꾼(워커)가 처리
  상품 정보 전송   ─────▶  순서대로 쌓임   ─────▶  네이버 + 쿠팡 동시 검색
 (POST /search)          (즉시 접수번호 반환)         │
                                                     ▼
                                        [4] 걸러내기 + AI 판정
                                        가격 이상치 제거 → "같은 상품"만 선별
                                                     │
                                                     ▼
                                        [4-1] 오픈마켓 폴백 (옵션 · 기본 꺼짐)
                                        네이버가 못 덮은 몰(G마켓·옥션·11번가)만 크롤
                                                     │
                                                     ▼
                                        [5] 최저가 확정 + 기록
                                        네이버/쿠팡/최종 + 몰별 최저가 저장 → 이력 적재
                                                     │
                                                     ▼
                                        [6] 완료(결과 + 검색 원가 조회 가능)

핵심 포인트

  • 즉시 응답 + 나중 처리: 요청하면 바로 "접수번호(job_id)"를 주고, 실제 검색은 뒤에서 진행됩니다. (검색은 몇 초~수십 초 걸림)
  • 못 찾으면 검색어를 바꿔 재시도: "맥심 커피"로 안 나오면 "맥심 모카골드 커피믹스"처럼 AI가 검색어를 다듬어 다시 시도하고, 그래도 없으면 "없음"으로 정리합니다. (무한 재시도 안 함)
  • 차단 대응: IP당 요청 예산(기본 3회)에 닿으면 차단당하기 전에 IP를 선제 교체하고(평판 보존 — 그 IP는 로테이션 복귀 시 재사용), 그래도 감지되면 그 포트를 쿨다운 격리 후 다른 IP로 재시도합니다. 시작 시 챌린지를 미리 풀어(웜업) 실 작업을 빠르게 합니다.
  • 원가 투명: 검색 1건이 쓴 AI 비용·프록시 대역폭·시간을 함께 기록합니다.

✨ 주요 기능

기능 설명
멀티 소스 검색 네이버 쇼핑 API + 쿠팡(Akamai 우회) 동시 검색·병합
오픈마켓 폴백 크롤 네이버가 못 덮은 몰만 G마켓·옥션(Cloudflare Turnstile 우회)·11번가 크롤 → 몰별 가격. 기본 비활성([WorkerConfig].fallbacks, 배경)
AI 같은 상품 판정 "진짜 그 상품"만 선별 (액세서리·다른 규격 제외)
검색어 자동 정제 0건이면 정밀/광역 검색어로 재시도
최저가 이력 그래프 조회 시점마다 네이버/쿠팡/최종 + 몰별(by_mall) 최저가를 시계열로 기록
검색 원가 계측 검색 1건의 AI 토큰·비용 + DECODO 대역폭(실측 CDP) + 시간을 집계
다중 상품 병렬 워커별 브라우저 세트로 여러 상품 동시 검색(WORKER_CONCURRENCY)
안정적 큐 처리 작업 유실 없이 순서대로, 실패 시 자동 재시도
프록시 IP 선제 회전 요청 예산(기본 3회) 도달 시 차단 전 선제 교체 + 불탄 포트 쿨다운 + 봇 감지·전송오류 즉시 순환 + 시작 웜업(DECODO). 예산 튜닝용 ip_session 관측 로그
임계 알림 큐·차단·DB풀·소스별 장기실패·비용 등 10룰 — 쿨다운(스팸 방지)·해소 알림, Slack 웹훅(룰 표)
API guard [WebServerConfig].api_keys 설정 시 /v1 전체 X-API-Key 검증(개발은 빈값=개방 모드)

🚀 빠른 시작

cd lps

# 1) 설정 파일 준비 (DB·API 키 등)
cp config/config.local.toml.example config/config.local.toml   # 값 채우기

# 2) API 서버 실행 (요청 접수)
./run_local_server.sh              # → http://localhost:9600/docs

# 3) 워커 실행 (실제 검색 수행) — 별도 터미널
./run_local_worker.sh              # 대화형: 동시성·프로필 선택 (또는 python worker_main.py)

# (선택) 부하 테스트 GUI — Locust 웹 UI(:8089)
./run_loadtest_gui.sh              # 브라우저에서 users/spawn 조절하며 RPS/지연 관측

간단 테스트:

curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
  -d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'

서버 실행 — Docker

# 리포 루트에서 전체 스택과 함께 (권장)
docker compose up -d                # negosium 스택 + lps-api·lps-worker·lps-admin 모두 기동

# lps 서브셋만 (대화형: 설정 검증·guard 키 안전장치 + admin 포함)
./run_docker.sh

환경 구분이 없습니다(도메인 backend·negodata·agent 와 동일) — 항상 config.local.toml. prod 서버도 그 서버의 config.local.toml 에 prod 값(시크릿·guard 키·스케일)을 채우고 그냥 docker compose up -d. DB 는 컨테이너에서 host.docker.internal(호스트 DB)로 접속하고, 관리형 DB 면 LPS_DB_HOST= 로 override 를 끄고 toml 의 호스트를 씁니다. 운영에서 API 를 외부에 열지 않으려면 export LPS_API_BIND=127.0.0.1(리버스프록시 뒤).

자세한 실행/설정은 운영 가이드 참고.


📚 문서

문서 대상 내용
아키텍처 개발자/기획자 구성요소·파이프라인·안티봇(Akamai/Turnstile)·비용계측·동시성
데이터베이스 개발자/기획자 테이블 5종 구조와 코드값(+by_mall·ip_session)
API 사용법 연동 개발자 엔드포인트·요청/응답·metrics 예시
운영 가이드 운영자/개발자 실행·병렬·관측(readyz/ops/알림)·Docker 배포·문제 해결
크롤러 논의 팀 오픈마켓 크롤러 유지 여부(ROI) 의사결정 메모

📁 폴더 구조

lps/
├── web_main.py            # API 서버 진입점 (요청 접수)
├── worker_main.py         # 워커 진입점 (검색+웜업+유휴정리+ops모니터)
├── Dockerfile             # API 이미지(lean) · Dockerfile.worker  # 워커(Chromium+Xvfb)
├── run_local_server.sh    # 로컬 API 실행 (대화형)
├── run_local_worker.sh    # 로컬 워커 실행 (대화형: 동시성·프로필)
├── run_docker.sh          # lps 서브셋 Docker 실행 (대화형: 설정 검증·guard 안전장치 + admin 포함)
├── run_loadtest_gui.sh    # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형)
├── config/                # 설정(config.local.toml — 포트/DB/API키, 미커밋; 배포는 env 주입)
├── common/                # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자)
│   └── database/model/models.py   # DB 테이블 정의
├── loadtest.py            # 부하 테스트 (N개 상품 → 처리량·지연·비용 집계)
├── 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 회전·포트 쿨다운·프리플라이트)
│   │   └── card_parser.py     # 오픈마켓 공용 카드 파서
│   ├── pipeline/          # 필터·이상치·최저가 정렬(+몰별 분해)
│   ├── ai/                # AI 유사도 판정·검색어 생성 (OpenAI)
│   └── metrics.py         # 검색 원가 계측(AI/DECODO 비용·시간)
├── worker/                # 워커 루프·핸들러(폴백·데드라인)·알림(NOTIFY)
├── router/v1/lps/         # API 라우터
└── tests/                 # 테스트

포트

backend 9300 / negodata 9400 / agent 9500 과 겹치지 않게 LPS는 9600.