docs: Chrome CDP 크롤링 장애 인시던트 보고서 추가
2026-05-18 웹크롤링/유튜브 자막 장애의 증상·진단·근본원인 (Chrome 136+ 기본 프로필 CDP 거부)·조치·검증·재발 점검 체크리스트를 docs/incident-2026-05-18-chrome-cdp-crawling.md 로 정리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
203
docs/incident-2026-05-18-chrome-cdp-crawling.md
Normal file
203
docs/incident-2026-05-18-chrome-cdp-crawling.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# 인시던트 보고서: 웹크롤링/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` |
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
Reference in New Issue
Block a user