| 질문 | 답변 |
|---|---|
| 이 크롤러의 주된 용도는? | 검색 엔진 인덱스 생성용 |
| 매달 얼마나 많은 웹 페이지를 수집해야 하는가? | 10억 개(1billion)의 웹 페이지를 수집해야 함 |
| 새로 만들어진 웹 페이지나 수정된 웹 페이지도 고려해야 하는가? | YES. 이미 수집한 페이지라도 업데이트되면 다시 크롤링해야 합니다. |
| 수집한 웹 페이지는 저장해야하는가? | YES. 5년간 보관 |
| 중복된 컨텐츠는? | 중복은 무시해도 됩니다. 같은 내용이면 한 번만 저장하면 돼요. |

사용자가 POST 요청:
{
"url": "<https://news.naver.com>"
}
Topic: "urls-to-crawl"
Message:
{
"url": "<https://news.naver.com>",
"depth": 0,
"timestamp": 1696723456,
"request_id": "req-12345"
}
Topic: "urls-to-crawl"
Message:
{
"url": "<https://news.naver.com>",
"depth": 0,
"timestamp": 1696723456,
"request_id": "req-12345"
}
1. PageRank 계산
도메인: "news.naver.com"
Redis 조회:
┌────────────────────────────────┐
│ DB 3: PageRank 점수 │
├────────────────────────────────┤
│ GET pagerank:news.naver.com │
│ → 0.95 (캐시 히트!) │
└────────────────────────────────┘
2. 우선순위 점수 계산
- PageRank: 0.95
- 뉴스 카테고리: +5점
- 업데이트 빈도 높음: +3점
- 최종 점수: 95점
3. URL 분류
95점 → P1 (높음) 큐로 분류
Topic: "priority-p1" (높음)
Message:
{
"url": "<https://news.naver.com>",
"priority_score": 95,
"pagerank": 0.95,
"depth": 0,
"timestamp": 1696723456,
"request_id": "req-12345"
}
Crawler Worker는 4개 토픽 구독:
- priority-p0: 40% 처리
- priority-p1: 30% 처리 ← 여기서 소비!
- priority-p2: 20% 처리
- priority-p3: 10% 처리
Message (priority-p1에서):
{
"url": "<https://news.naver.com>",
"priority_score": 95,
"pagerank": 0.95,
"depth": 0,
"timestamp": 1696723456,
"request_id": "req-12345"
}
┌────────────────────────────────────────┐
│ DB 1: 방문한 URL │
├────────────────────────────────────────┤
│ URL 해시 계산: │
│ hash("<https://news.naver.com>") │
│ → "abc123def456" │
│ │
│ 1차 체크 (Bloom Filter): │
│ BF.EXISTS bloom:visited_urls abc123def │
│ → 0 (없음, 미방문!) │
│ │
│ 결론: 크롤링 진행 OK! │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ DB 5: Rate Limiting │
├────────────────────────────────────────┤
│ GET ratelimit:news.naver.com │
│ → 1696723450 (마지막 요청 시간) │
│ │
│ 현재 시간: 1696723456 │
│ 경과 시간: 6초 │
│ │
│ 1초 이상 지남? YES │
│ 결론: 요청 가능! │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ DB 0: DNS 캐시 │
├────────────────────────────────────────┤
│ GET dns:news.naver.com │
│ → "223.130.195.95" (캐시 히트!) │
│ │
│ DNS 조회 시간: 0ms (캐시) │
│ (조회했다면 10-200ms 걸렸을 것) │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ DB 4: robots.txt 캐시 │
├────────────────────────────────────────┤
│ GET robots:news.naver.com │
│ → { │
│ "user_agent": "*", │
│ "disallow": ["/admin", "/api"], │
│ "crawl_delay": 1 │
│ } │
│ │
│ "/" 경로 허용? YES │
│ 결론: 크롤링 가능! │
└────────────────────────────────────────┘
HTTP 요청:
GET <https://news.naver.com>
Host: news.naver.com
User-Agent: MyBot/1.0
응답:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 524288
<!DOCTYPE html>
<html>
<head>
<title>네이버 뉴스</title>
</head>
<body>
<a href="<https://news.naver.com/article/001>">기사1</a>
<a href="<https://news.naver.com/article/002>">기사2</a>
<a href="<https://sports.naver.com>">스포츠</a>
...
</body>
</html>
다운로드 완료: 512KB (524,288 bytes)
소요 시간: 487ms
S3 업로드:
Bucket: crawler-bucket
Key: raw/2025/10/14/abc123def456.html
동시에 압축 버전도 저장:
Key: compressed/2025/10/14/abc123def456.html.gz
크기: 512KB → 51KB (90% 압축)
┌────────────────────────────────────────┐
│ 방문 기록 저장 │
├────────────────────────────────────────┤
│ 1. Bloom Filter에 추가 │
│ BF.ADD bloom:visited_urls abc123def │
│ │
│ 2. 정확한 기록 저장 │
│ SET visited:abc123def { │
│ "url": "<https://news.naver.com>", │
│ "timestamp": 1696723456, │
│ "status": 200 │
│ } │
│ TTL: 30일 │
│ │
│ 3. Rate Limit 업데이트 │
│ SET ratelimit:news.naver.com 1696723456│
│ TTL: 10초 │
└────────────────────────────────────────┘
Topic: "crawled-pages"
Message:
{
"url": "<https://news.naver.com>",
"s3_key": "raw/2025/10/14/abc123def456.html",
"s3_key_compressed": "compressed/2025/10/14/abc123def456.html.gz",
"original_size": 524288,
"compressed_size": 52428,
"status_code": 200,
"content_type": "text/html",
"headers": {
"server": "nginx",
"date": "Mon, 14 Oct 2025 12:34:56 GMT"
},
"download_time_ms": 487,
"timestamp": 1696723456,
"request_id": "req-12345",
"snippet": "<!DOCTYPE html><html><head><title>네이버 뉴스..." // 첫 500자
}
메시지 크기: 약 2KB
Topic: "crawled-pages"
Message:
{
"url": "<https://news.naver.com>",
"s3_key": "raw/2025/10/14/abc123def456.html",
"original_size": 524288,
"status_code": 200,
"content_type": "text/html",
"timestamp": 1696723456,
"request_id": "req-12345",
"snippet": "<!DOCTYPE html><html><head><title>네이버 뉴스..."
}
S3 GET:
Bucket: crawler-bucket
Key: raw/2025/10/14/abc123def456.html
다운로드:
<!DOCTYPE html>
<html>
<head>
<title>네이버 뉴스</title>
</head>
<body>
<a href="<https://news.naver.com/article/001>">기사1</a>
<a href="<https://news.naver.com/article/002>">기사2</a>
<a href="<https://sports.naver.com>">스포츠</a>
<a href="/entertainment">연예</a>
<a href="malformed-url">잘못된 링크</a>
</body>
</html>
Jsoup.parse(html)
추출된 정보:
- Title: "네이버 뉴스"
- Description: "최신 뉴스를 가장 빠르게"
- Keywords: "뉴스, 속보, 정치"
- Language: "ko"
- Links: 5개 발견
메타데이터:
{
"title": "네이버 뉴스",
"description": "최신 뉴스를 가장 빠르게",
"keywords": ["뉴스", "속보", "정치"],
"author": null,
"publish_date": "2025-10-14",
"language": "ko",
"og_image": "<https://news.naver.com/og_image.jpg>"
}
┌────────────────────────────────────────┐
│ DB 2: 콘텐츠 해시 │
├────────────────────────────────────────┤
│ 1. HTML 해시 계산 │
│ SHA-256(html_content) │
│ → "sha256_xyz789abc" │
│ │
│ 2. Bloom Filter 체크 │
│ BF.EXISTS bloom:content_hashes sha256_ │
│ → 0 (없음, 신규 콘텐츠!) │
│ │
│ 3. 해시 저장 │
│ SET content:sha256_xyz789abc { │
│ "url": "<https://news.naver.com>", │
│ "timestamp": 1696723456 │
│ } │
│ TTL: 30일 │
└────────────────────────────────────────┘
발견한 링크들:
1. "<https://news.naver.com/article/001>" ← 절대 경로
2. "<https://news.naver.com/article/002>" ← 절대 경로
3. "<https://sports.naver.com>" ← 절대 경로
4. "/entertainment" ← 상대 경로!
5. "malformed-url" ← 잘못된 URL
정규화:
1. "<https://news.naver.com/article/001>" ✅
2. "<https://news.naver.com/article/002>" ✅
3. "<https://sports.naver.com>" ✅
4. "/entertainment"
→ "<https://news.naver.com/entertainment>" ✅
5. "malformed-url" ❌ 제거
총 4개 URL 추출
필터 규칙:
- 블랙리스트 도메인: ["spam.com", "malware.net"]
- 제외 파일: [".pdf", ".zip", ".exe"]
- 파라미터 정리: "?utm_source=..." 제거
필터링 결과:
1. "<https://news.naver.com/article/001>" ✅ 통과
2. "<https://news.naver.com/article/002>" ✅ 통과
3. "<https://sports.naver.com>" ✅ 통과
4. "<https://news.naver.com/entertainment>" ✅ 통과
최종: 4개 URL
MongoDB에 메타데이터 저장
Collection: pages
Document:
{
"_id": "abc123def456",
"url": "<https://news.naver.com>",
"title": "네이버 뉴스",
"description": "최신 뉴스를 가장 빠르게",
"keywords": ["뉴스", "속보", "정치"],
"language": "ko",
"content_hash": "sha256_xyz789abc",
"s3_key": "raw/2025/10/14/abc123def456.html",
"size": 524288,
"crawled_at": "2025-10-14T12:34:56Z",
"status": 200,
"extracted_urls": 4
}
S3는 이미 저장됨 (Crawler Worker가 함)
Topic: "urls-to-crawl" (순환!)
4개 메시지 발행:
Message 1:
{
"url": "<https://news.naver.com/article/001>",
"depth": 1, ← depth 증가!
"parent_url": "<https://news.naver.com>",
"timestamp": 1696723460,
"request_id": "req-12346"
}
Message 2:
{
"url": "<https://news.naver.com/article/002>",
"depth": 1,
"parent_url": "<https://news.naver.com>",
"timestamp": 1696723460,
"request_id": "req-12347"
}
Message 3:
{
"url": "<https://sports.naver.com>",
"depth": 1,
"parent_url": "<https://news.naver.com>",
"timestamp": 1696723460,
"request_id": "req-12348"
}
Message 4:
{
"url": "<https://news.naver.com/entertainment>",
"depth": 1,
"parent_url": "<https://news.naver.com>",
"timestamp": 1696723460,
"request_id": "req-12349"
}
이제 4개의 새 URL이 urls-to-crawl 토픽에 들어갔으므로,
Priority Manager가 다시 소비해서 우선순위를 매기고,
이 과정이 무한 반복
00:00.000 사용자가 API Server에 URL 제출
00:00.010 API → Kafka: urls-to-crawl
00:00.020 Priority Manager 소비
00:00.025 Redis에서 PageRank 조회 (5ms)
00:00.030 P1 큐로 분류
00:00.035 Kafka: priority-p1에 발행
00:00.040 Crawler Worker 소비 (4:3:2:1 비율로 P1 처리)
00:00.045 Redis: 방문 체크 (5ms) → 미방문
00:00.050 Redis: Rate Limit 체크 (5ms) → OK
00:00.052 Redis: DNS 조회 (2ms, 캐시 히트!)
00:00.057 Redis: robots.txt 조회 (5ms, 캐시 히트!)
00:00.060 HTTP GET 시작
00:00.547 HTTP 완료 (487ms) ← 가장 오래 걸림!
00:00.570 S3 업로드 (23ms)
00:00.575 Redis 업데이트 (5ms)
00:00.580 Kafka: crawled-pages에 발행
00:00.590 Content Processor 소비
00:00.610 S3 다운로드 (20ms)
00:00.660 HTML 파싱 (50ms)
00:00.670 메타데이터 추출 (10ms)
00:00.675 중복 체크 (5ms) → 신규
00:00.705 URL 추출 (30ms)
00:00.715 URL 필터링 (10ms)
00:00.735 MongoDB 저장 (20ms)
00:00.740 Kafka: urls-to-crawl에 4개 URL 발행
━━━━━━━━━━━━━━━━━━━━━━━━━━━
총 소요 시간: 740ms
병목: HTTP GET (487ms, 66%)
urls-to-crawl: ~200 bytes (URL + 메타)
priority-p0~p3: ~250 bytes (점수 추가)
crawled-pages: ~2KB (메타 + snippet)
urls-to-crawl(순환): ~200 bytes
→ HTML 본문은 Kafka에 없음!
→ S3에만 저장됨
DB 0: DNS 캐시 → 10-200ms를 0ms로!
DB 1: 방문 URL → 중복 크롤링 방지
DB 2: 콘텐츠 해시 → 중복 콘텐츠 방지
DB 3: PageRank → 우선순위 계산
DB 4: robots.txt → 크롤링 규칙
DB 5: Rate Limiting → 예의 지키기 (1초 간격)