# 인시던트 보고서: 웹크롤링/YouTube 자막 장애 (Chrome CDP) - **발생 인지일**: 2026-05-18 - **장애 시작 추정일**: 2026-04-13 이후 (Chrome 자동 업데이트 시점) - **영향 범위**: 웹크롤링 3차 폴백(Playwright), YouTube 자막 추출 전면 실패 - **상태**: 해결 완료 (배포 커밋 `9569309`) --- ## 1. 증상 웹크롤링이 예전에는 정상 동작했으나 어느 시점부터 "이상"해짐. - 일반 웹페이지(Jsoup / Jina Reader로 처리 가능한 사이트)는 **정상** 동작 → "되긴 되는데" - 봇 차단 사이트 및 **YouTube 자막 추출**은 **전부 실패** → "이상하네" - 백엔드 자체는 정상 기동 (PM2 online, 장애 인지 시점 기준 21h+ uptime) --- ## 2. 시스템 구조 (배경) 웹크롤링은 3단계 폴백 구조입니다. ``` crawl(url) ├─ 1차: Jsoup (정적 HTML 파싱) ├─ 2차: Jina Reader API (https://r.jina.ai/) └─ 3차: Playwright → PlaywrightBrowserService → 사용자 Chrome (CDP, 9222) ``` - `WebCrawlerService.crawl()` : 3단계 폴백 오케스트레이션 - `PlaywrightBrowserService` : `connectOverCDP("http://...:9222")`로 사용자 Chrome에 연결 - `YouTubeTranscriptService.fetchWithPlaywright()` : 자막 추출 시 동일 CDP 경로 사용 3차 폴백(Playwright)은 봇 판정 우회를 위해 **사용자가 로그인한 Chrome 세션**에 CDP로 붙는 것이 핵심 설계입니다. 따라서 Chrome이 `--remote-debugging-port=9222`로 떠 있고, 거기에 로그인 세션(쿠키)이 살아 있어야 합니다. --- ## 3. 진단 과정 | 확인 항목 | 결과 | |---|---| | `curl http://localhost:9222/json/version` | 연결 실패 | | `pgrep -af remote-debugging-port` | 디버깅 포트로 뜬 Chrome 없음 | | 백엔드 프로세스 | 정상 (PM2 online) | | 백엔드 로그 | `connect ECONNREFUSED ::1:9222`, `Chrome CDP 재연결 실패` 반복 | | 마지막 정상 CDP 연결 로그 | **2026-04-13** 이후 없음 | | 실제 Chrome 프로세스 | **떠 있음** (단, `--remote-debugging-port` 인자 **없이** 일반 실행) | | 재기동 시도 시 Chrome 출력 | `DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir.` | 진단 중 1차 가설(Chrome 프로세스 죽음, IPv6 `::1` resolve 문제)을 거쳐, 최종적으로 Chrome 콘솔 출력에서 **결정적 메시지**를 확인. --- ## 4. 근본 원인 **Chrome 136+ (장애 시점 147) 의 보안 정책**: 기본 프로필 디렉토리(`~/.config/google-chrome`)에서는 원격 디버깅(CDP)을 거부한다. - `--user-data-dir`에 **기본 경로를 그대로 지정해도** 거부됨 → 반드시 **non-default(별도)** 디렉토리여야 함 - 2026-04-13 마지막 정상 동작 이후 **Chrome 자동 업데이트**로 이 정책이 적용됨 - Chrome 프로세스가 죽은 것이 아니라, **버전업으로 기본 프로필 + 원격 디버깅 조합이 영구 차단**된 것 - `--remote-debugging-pipe`는 Playwright `connectOverCDP`(HTTP/WS endpoint 필요)와 호환되지 않아 우회 불가 **부가 발견** 1. **자동 복구 불가 구조**: Chrome을 PM2가 관리하지 않아, 한번 죽거나 잘못 뜨면 영구 장애가 됨 2. **IPv6 함정**: 에러가 `::1:9222` (IPv6 localhost). `localhost`가 IPv6로 resolve되는데 Chrome은 기본적으로 IPv4 `127.0.0.1`에만 바인딩 → 표기 불일치 잠재 위험 --- ## 5. 조치 내역 세 가지를 모두 적용했습니다. ### 5-1. 프로필 이동 + 기동 스크립트 (`start-chrome.sh` 신규) 봇 우회용 로그인 세션을 보존하기 위해 기존 프로필을 **이동(mv)**: ```bash mv ~/.config/google-chrome ~/.config/google-chrome-cdp ``` `/home/opc/sundol/start-chrome.sh` 신규 작성: - `DISPLAY=:1` (VNC 세션) - 기존 동일 프로필 Chrome 종료 (graceful → 강제) - 비정상 종료로 남은 stale 싱글톤 락(`SingletonLock` / `SingletonCookie` / `SingletonSocket`) 정리 - `exec`로 foreground 유지 → PM2 fork 모드가 프로세스 추적 - 기동 인자: `--user-data-dir=/home/opc/.config/google-chrome-cdp --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 --no-first-run --no-default-browser-check --start-maximized` ### 5-2. PM2 상시화 (`ecosystem.config.cjs`) `sundol-chrome` 앱 추가하여 PM2가 관리·자동 재기동: ```js { name: "sundol-chrome", script: "./start-chrome.sh", interpreter: "/bin/bash", cwd: "/home/opc/sundol", env: { DISPLAY: ":1" }, } ``` - `pm2 start ecosystem.config.cjs --only sundol-chrome` → `pm2 save` - `pm2-opc` systemd unit **enabled** 확인 → 재부팅 시 자동 복원 ### 5-3. IPv6 함정 제거 (`PlaywrightBrowserService.java`) ```diff - private static final String CDP_URL = "http://localhost:9222"; + private static final String CDP_URL = "http://127.0.0.1:9222"; ``` Chrome 측도 `--remote-debugging-address=127.0.0.1`로 IPv4 명시 바인딩 → 양쪽을 IPv4로 통일하여 `localhost`의 IPv6 해석 변수 자체를 제거. > 비고: Chrome `--remote-debugging-address`는 단일 주소만 지원하여 IPv4/IPv6 동시 바인딩 불가. 따라서 "양쪽 통일"은 IPv4(`127.0.0.1`)로 일치시키는 방식으로 달성. --- ## 6. 배포 (CLAUDE.md 배포 전 필수 절차 준수) | 단계 | 결과 | |---|---| | 1. 컴파일 | 통과 (종료코드 0) | | 2. PMD 정적 분석 | 6개 위반 검출, **전부 기존 코드** (변경 파일 `PlaywrightBrowserService.java` 위반 0). 규칙상 기존 위반은 보고만, 미수정 | | 3. 코드 리뷰 | 변경은 상수 1줄. 재시도/중복적재/null/카운터 등 로직 영향 없음 | | 4. 사용자 승인 | 승인 후 배포 진행 | - 백엔드 재시작 (`pm2 restart sundol-backend`) → 부팅 로그 `사용자 Chrome에 CDP 연결 완료` 확인 - 커밋 `9569309` → `git push origin main` 완료 - 커밋 범위: `start-chrome.sh`, `ecosystem.config.cjs`, `PlaywrightBrowserService.java` 3개 파일만 (무관한 프론트엔드 변경 제외) > `ecosystem.config.cjs`에는 이전 작업분(`frontend script: node → /usr/local/bin/node`)이 한 파일에 섞여 분리 불가하여 함께 커밋, 커밋 메시지에 명시함. --- ## 7. 검증 결과 | 검증 | 결과 | |---|---| | `curl http://127.0.0.1:9222/json/version` | `Chrome/147.0.7727.55` 정상 응답 | | 포트 바인딩 | `LISTEN 127.0.0.1:9222` — IPv4 명시 바인딩 확인 | | 백엔드 부팅 로그 | `CDP 연결: 1 contexts, 1 pages` / `사용자 Chrome에 CDP 연결 완료` | | PM2 `sundol-chrome` | `status=online, restarts=0` (재시작 루프 없음), PPID=PM2 데몬 | | CDP 페이지 제어 스모크 | `example.com` 새 탭 생성(type=page) → 제어 → 종료 성공 (Playwright `openPage`와 동일 메커니즘) | | `/api/knowledge/youtube-transcript` (무인증) | HTTP 401 → 엔드포인트 정상 가동(인증만 필요) | **미완 검증 (사용자 액션 필요)**: 실제 end-to-end(로그인 인증이 필요한 유튜브 자막/봇차단 페이지 본문 추출)는 인증 장벽으로 자동 수행 불가. 프론트엔드에서 1건 시도 시 백엔드 로그로 최종 확정 가능. --- ## 8. 운영 규칙 (재발 방지) > **Chrome은 PM2 `sundol-chrome`만 관리한다. 수동으로 `google-chrome`를 실행하지 말 것.** 수동 인스턴스가 `~/.config/google-chrome-cdp` 프로필 락을 먼저 잡으면, PM2가 띄우는 Chrome이 CDP 포트를 열지 못해 **동일 장애가 재발**한다. 사용자가 평소 VNC에서 사용하는 Chrome도 **PM2가 띄운 그 창을 그대로 사용**한다. --- ## 9. 재발 시 점검 체크리스트 ```bash # 1) CDP 포트 응답 확인 curl -s http://127.0.0.1:9222/json/version # 2) 디버깅 포트로 뜬 Chrome 프로세스 확인 pgrep -af 'remote-debugging-port=9222' # 3) PM2 sundol-chrome 상태 (재시작 루프 여부) npx pm2 list | grep sundol-chrome # status=online, ↺(restart) 비정상 증가 여부 # 4) 백엔드 로그에서 CDP 연결 상태 grep -aE 'CDP 연결 완료|CDP 연결 실패|ECONNREFUSED.*9222' ~/.pm2/logs/sundol-backend-out.log | tail # 5) Chrome 콘솔 출력 (프로필 정책 위반 메시지 확인) grep -a 'non-default data directory' ~/.pm2/logs/sundol-chrome-*.log # 6) 수동 Chrome 인스턴스가 프로필 락을 잡고 있지 않은지 ls -la ~/.config/google-chrome-cdp/SingletonLock pgrep -af '/opt/google/chrome/chrome' | grep -v -E 'crashpad|--type=' ``` **복구 절차**: 수동 Chrome 전부 종료 → `npx pm2 restart sundol-chrome` → 위 1~4번 재확인. --- ## 10. 관련 파일·위치 | 항목 | 경로 | |---|---| | Chrome 기동 스크립트 | `/home/opc/sundol/start-chrome.sh` | | PM2 설정 | `/home/opc/sundol/ecosystem.config.cjs` (`sundol-chrome` 앱) | | CDP 연결 코드 | `sundol-backend/src/main/java/com/sundol/service/PlaywrightBrowserService.java` (`CDP_URL`) | | 크롤링 폴백 | `sundol-backend/.../service/WebCrawlerService.java` | | YouTube 자막 | `sundol-backend/.../service/YouTubeTranscriptService.java` | | CDP 전용 프로필 | `/home/opc/.config/google-chrome-cdp` (로그인 세션 포함, 824MB) | | 백엔드 로그 | `~/.pm2/logs/sundol-backend-out.log` | | Chrome 로그 | `~/.pm2/logs/sundol-chrome-out.log` / `-error.log` | | 백엔드 포트 | `8080` | | VNC 디스플레이 | `:1` |