prod 영향 점검 중 발견. 직전 커밋(377389f)의 sticky 만료 회전은 **브라우저가 열려 있을 때만** 동작했다 — `_ctx is None` 이면 만료 검사를 건너뛰었다. 그런데 negodata 연동은 수동 트리거 전용이라 검색이 드문드문 들어오고, 그때는 유휴 정리(120s)로 브라우저가 닫힌 채 매 검색이 그 경로로 들어온다. 즉 **실사용 패턴에서만 안 먹는** 반쪽 수정이었다(377389f커밋 메시지·README 의 '고쳤다'는 서술이 부정확했다). 실측(합성 게이트웨이·10포트, sticky 매번 경과): 연속 검색 IP 6개 순환 ✅ 저트래픽 · 예산 3 IP 2개 예산이 대신 회전시켜 가려져 있었음 저트래픽 · 예산 0 **IP 1개 고정** ❌ ([DecodoConfig] 주석의 '0=시간창 회전만'이 거짓) 원인은 시계가 둘이었던 것이다. sticky 만료를 브라우저 기동 시각(_launched_at)으로 쟀는데, 브라우저는 닫혔다 열릴 때마다 시계가 되감긴다. IP 를 쥔 시간과 어긋나는 이 구조가 F1(예산 미발화)과 F4(회전 안 됨)의 공통 원인이었다. → 시계를 _session_started_at 하나로 통일하고 _launched_at 을 제거했다. 만료 판정은 브라우저가 닫혀 있어도 수행한다. 세 시나리오 모두 정상 회전 확인. **prod 템플릿 kr_host 누락**(기존 문제, 이번 변경과 무관): config.prod.toml.example 에 kr_host 가 없어 그대로 복사하면 네이버가 국가 무지정 게이트웨이로 떨어진다. 해외 residential IP 는 '비정상적인 접근'(2.6KB) 하드차단이고 회전으로 회복 불가라 네이버 결과가 통째로 0건이 된다. kr_host + naver_ip_request_budget 을 경고 주석과 함께 추가. 테스트 2건 추가(저트래픽 sticky 만료 회전 / IP 를 쥔 시간이 수명 내면 회전 안 함), 전체 253 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.6 KiB
Plaintext
86 lines
3.6 KiB
Plaintext
# 배포서버용. 복사해서 사용: cp config.prod.toml.example config.prod.toml
|
|
# 실제 config.prod.toml 은 시크릿 포함이라 커밋하지 않는다.
|
|
#
|
|
# ⚠️ 로드 경로 주의 — docker-compose.prod.yml 이 이 파일을 config.local.toml 자리에 마운트한다:
|
|
# ./lps/config/config.prod.toml : /app/config/config.local.toml : ro
|
|
# 컨테이너는 APP_ENV=local 로 뜨므로 코드가 읽는 파일명은 config.local.toml 이지만
|
|
# 내용은 이 파일이다. 파일명만 보고 "prod 설정이 안 읽힌다"고 오해하기 쉽다.
|
|
#
|
|
# 로컬(config.local.toml.example)과 다른 항목만 ★ 로 표시했다.
|
|
|
|
[WebServerConfig]
|
|
server_name = "LpsServer"
|
|
port = 9600
|
|
process_count = 1
|
|
is_ssl = false # 리버스프록시 뒤 — TLS 는 프록시가 종단
|
|
is_test = false # ★ prod
|
|
cors_origins = [] # ★ 브라우저가 직접 호출하지 않으면 비움
|
|
api_keys = ["<API_KEY>"] # ★ prod 는 반드시 채운다(비면 무인증 개방). openssl rand -hex 32
|
|
|
|
[LogConfig]
|
|
print_console = true
|
|
log_level = "info" # ★ prod
|
|
|
|
[MainDBConfig]
|
|
db_type = "postgresql"
|
|
name = "lps_db"
|
|
write_host = "host.docker.internal" # ★ 컨테이너 → 호스트 DB (compose extra_hosts)
|
|
write_port = 5432
|
|
write_id = "<DB_USER>"
|
|
write_pw = "<DB_PASSWORD>"
|
|
read_host = "host.docker.internal" # ★
|
|
read_port = 5432
|
|
read_id = "<DB_USER>"
|
|
read_pw = "<DB_PASSWORD>"
|
|
show_log = false
|
|
pool_size = 10
|
|
max_overflow = 20
|
|
connection_budget = 40
|
|
sslmode = "" # 관리형 DB 면 "require"
|
|
|
|
[WorkerConfig]
|
|
concurrency = 1 # ★ 동시성을 올리면 차단 시 IP 소모도 비례해 늘어난다
|
|
fallbacks = []
|
|
profile_dir = "/profiles" # ★ 필수 — compose 가 lps-profiles 볼륨을 여기에 마운트한다.
|
|
# ".profiles" 로 두면 컨테이너 레이어에 쌓여 재생성마다 쿠키가 날아간다.
|
|
job_deadline_sec = 300
|
|
shutdown_grace_sec = 60
|
|
chrome_channel = "chrome"
|
|
chrome_executable = "/opt/google/chrome/chrome" # ★ 컨테이너 필수(설정 시 --no-sandbox 가 함께 붙음)
|
|
heartbeat_file = "/tmp/lps_worker_heartbeat"
|
|
|
|
[AlertConfig]
|
|
webhook = "" # ★ Slack 호환 웹훅. 비우면 로그로만 알림
|
|
cooldown_min = 30
|
|
dead_1h = 20
|
|
blocks_1h = 80
|
|
queue_lag_sec = 300
|
|
pool_pct = 90
|
|
source_fail_30m = 5
|
|
deadline_1h = 5
|
|
cost_1h_usd = 1.0
|
|
ports_low_pct = 30
|
|
block_sessions_6h = 1
|
|
|
|
# ── 시크릿 ──
|
|
|
|
[OpenAIConfig]
|
|
api_key = "<OPENAI_API_KEY>" # 소진되면 크롤이 성공해도 AI 판정 실패로 잡이 DEAD 된다
|
|
model = "gpt-4o-mini"
|
|
|
|
[DecodoConfig]
|
|
host = "gate.decodo.com"
|
|
# ⚠️ 네이버는 **한국 IP 가 필수**다. 이 줄이 비면 네이버가 host(국가 무지정)로 떨어지고,
|
|
# 해외 residential IP 로는 매 검색이 하드차단('비정상적인 접근', 2.6KB)된다 — IP 를 아무리
|
|
# 회전해도 회복되지 않는 구조적 차단이라 네이버 결과가 통째로 0건이 된다(fatal_block 알림).
|
|
kr_host = "kr.decodo.com" # 한국 타깃 게이트웨이(네이버용). 비우면 host 사용 = 네이버 불가
|
|
username = "<DECODO_USERNAME>"
|
|
password = "<DECODO_PASSWORD>"
|
|
port_start = 10001
|
|
port_end = 10100
|
|
session_minutes = 10
|
|
cost_per_gb = 3.0
|
|
ip_request_budget = 3 # 쿠팡 IP당 요청 예산. 차단이 ip_req#1 에 몰리면 이 값과 무관한 문제다
|
|
naver_ip_request_budget = 10 # 네이버는 IP 당 12회까지 무차단 실측 → 여유 둔 10
|
|
port_cooldown_sec = 0 # 0=자동 max(sticky, 30분)
|