
이전 포스트: [대규모 시스템 설계 스터디] 8장 정리
다음 포스트: [ 대규모 시스템 설계 스터디 ] 10장 정리
웹 크롤러(Web Crawler)는 웹 페이지를 자동으로 방문하고, 페이지 안의 콘텐츠와 링크를 수집하는 프로그램이다.
검색 엔진의 Googlebot처럼 웹의 새로운 페이지를 찾아 검색 인덱스를 만드는 것이 대표적인 예다.
크롤러는 보통 몇 개의 시작 URL에서 출발한다.
Seed URL
↓
페이지 다운로드
↓
링크 추출
↓
새 URL 발견
↓
다시 다운로드
↓
반복
겉으로 보면 단순한 반복 작업처럼 보인다.
하지만 수십억 개의 페이지를 대상으로 한다면
등을 모두 고려해야 한다.
결국 웹 크롤러는 단순한 링크 수집 프로그램이 아니라 대규모 분산 시스템으로 바라봐야 한다.
웹 크롤러의 가장 대표적인 활용 사례다.
웹 페이지 수집
↓
페이지 분석
↓
검색 인덱스 생성
↓
검색 결과 제공
크롤러가 웹 페이지를 미리 찾아 수집해두기 때문에 사용자가 검색할 때마다 전체 웹을 실시간으로 확인할 필요가 없다.
웹 페이지는 언제든 수정되거나 삭제될 수 있다.
따라서 특정 시점의 웹 콘텐츠를 장기간 보존하기 위해 웹 크롤러를 사용할 수도 있다.
웹 페이지 수집
↓
특정 시점 상태 저장
↓
장기간 보관
웹에서 데이터를 수집한 뒤 분석하여 새로운 정보를 찾아내는 방식이다.
Web Crawling
→ 데이터 수집
Web Mining
→ 수집한 데이터에서 의미 발견
예를 들어 기업 관련 보고서와 공시 자료 등을 수집하고 분석하는 데 활용할 수 있다.
특정 콘텐츠가 새롭게 나타났거나 기존 내용이 변경되었는지 지속적으로 확인하는 방식이다.
예를 들면
등에 활용할 수 있다.
즉 웹 마이닝이 수집된 데이터에서 의미를 찾는 것이라면, 웹 모니터링은 특정 조건이나 변화를 계속 추적하는 것에 가깝다.
웹 크롤러를 설계하기 전에 먼저 어떤 크롤러를 만들어야 하는지 범위를 확정해야 한다.
예를 들어 다음을 확인할 수 있다.
이러한 조건에 따라 설계 방식이 달라진다.
웹의 규모가 매우 크기 때문에 한 서버에서 URL을 하나씩 처리하는 방식으로는 부족하다.
단일 Worker
URL1 → URL2 → URL3 → URL4
대신 여러 작업자가 동시에 URL을 처리할 수 있어야 한다.
Worker 1 → URL1
Worker 2 → URL2
Worker 3 → URL3
Worker 4 → URL4
이를 통해 전체 크롤링 처리량을 높인다.
웹에는 정상적인 페이지뿐 아니라 다양한 문제가 존재한다.
특정 URL 하나 때문에 전체 크롤러가 멈춰서는 안 된다.
URL 요청
↓
응답 없음
↓
Timeout
↓
해당 작업 종료
↓
다음 URL 처리
웹 크롤러에서 말하는 예의는 수집 대상 서버에 지나친 부담을 주지 않는 것이다.
예를 들어 한 사이트에 수백 개의 요청을 동시에 보내면 해당 사이트의 정상 사용자에게까지 영향을 줄 수 있다.
따라서 같은 호스트에는 적절한 간격을 두고 요청해야 한다.
Request
↓
Wait
↓
Request
↓
Wait
또한 해당 사이트가 제공하는 robots.txt 정책도 확인해야 한다.
처음에는 HTML만 수집하더라도 이후 다른 콘텐츠까지 지원해야 할 수 있다.
Crawler
├─ HTML Parser
├─ Image Downloader
├─ PDF Processor
└─ Video Processor
따라서 새로운 콘텐츠 형식을 추가할 때 전체 시스템을 수정하기보다 필요한 모듈만 추가할 수 있도록 설계하는 것이 좋다.
매달 10억 페이지를 다운로드한다고 가정한다.
평균 QPS는
1,000,000,000
÷ 30일
÷ 24시간
÷ 3,600초
≈ 400 Page / sec
정도다.
Peak QPS를 평균의 2배로 가정하면
약 800 Page / sec
수준을 처리해야 한다.
웹 페이지 평균 크기를 500KB로 가정하면
10억 × 500KB
≈ 500TB / 월
이 된다.
5년 동안 저장하면
500TB × 12 × 5
≈ 30PB
정도의 저장 공간이 필요하다.
즉 대규모 웹 크롤러에서는 URL 처리뿐 아니라 저장소 자체도 대규모 시스템으로 설계해야 한다.
전체적인 흐름은 다음과 같이 볼 수 있다.
Seed URLs
↓
URL Frontier
↓
HTML Downloader
↓
Content Parser
↓
중복 콘텐츠 검사
↓
Content Storage
↓
URL Extractor
↓
URL Filter
↓
방문 여부 확인
↓
URL Frontier
새롭게 발견된 URL이 다시 URL Frontier로 들어가면서 이 과정이 계속 반복된다.
Seed URL은 크롤링을 시작하는 출발점이다.
예를 들어 특정 대학 사이트만 수집한다면 해당 대학 홈페이지의 주요 URL을 시작점으로 사용할 수 있다.
전체 웹을 대상으로 한다면 여러 종류의 Seed URL이 필요하다.
예를 들면
지역별 주요 사이트
한국
미국
일본
...
또는
주제별 주요 사이트
쇼핑
뉴스
스포츠
건강
...
처럼 나눌 수 있다.
Seed URL을 정하는 완벽한 방법은 없으며 크롤러의 목적에 따라 달라진다.
앞으로 방문해야 할 URL을 저장하는 공간이다.
아직 방문하지 않은 URL
↓
URL Frontier
↓
Downloader
단순하게 생각하면 Queue와 비슷하다.
하지만 실제 대규모 크롤러에서는 단순 FIFO Queue 이상의 역할을 담당한다.
대표적으로
가 필요하다.
URL Frontier에서 URL을 가져와 실제 웹 페이지를 다운로드한다.
URL Frontier
↓
HTML Downloader
↓
HTTP Request
↓
Website
다운로더는 URL에 포함된 도메인을 실제 서버의 IP 주소로 변환하기 위해 DNS도 사용한다.
다운로드했다고 해서 곧바로 저장하는 것은 아니다.
HTML이 정상적인지 확인하고 필요한 데이터를 추출해야 한다.
Downloaded HTML
↓
Content Parser
↓
Validation
예를 들어
등을 확인할 수 있다.
파싱 작업을 별도 컴포넌트로 분리하면 다운로더가 페이지 다운로드에 집중할 수 있다.
웹에는 동일한 콘텐츠가 서로 다른 URL로 존재할 수 있다.
모든 문서를 문자열 전체로 비교하면 비용이 너무 크다.
따라서 콘텐츠의 Hash 또는 Checksum을 계산할 수 있다.
Content
↓
Hash
↓
기존 Hash와 비교
이미 동일한 값이 존재한다면 중복 콘텐츠로 판단하여 저장하지 않는다.
이를 통해
을 줄일 수 있다.
중복 검사를 통과한 콘텐츠는 저장소에 보관한다.
크롤링 데이터는 규모가 매우 크기 때문에 모든 데이터를 메모리에 보관할 수는 없다.
따라서
대부분의 콘텐츠
→ Disk
자주 접근하는 콘텐츠
→ Memory / Cache
와 같은 구조를 사용할 수 있다.
저장소를 결정할 때는
등을 고려한다.
HTML 페이지에는 다른 페이지로 연결되는 링크가 포함되어 있다.
URL Extractor는 이 링크들을 찾아낸다.
예를 들어
/wiki/Cong_Weixi
라는 상대 경로가 있다면 현재 도메인과 결합한다.
https://en.wikipedia.org
+
/wiki/Cong_Weixi
↓
https://en.wikipedia.org/wiki/Cong_Weixi
즉 추출된 링크를 다시 크롤링할 수 있도록 완전한 URL 형태로 만든다.
추출했다고 해서 모든 URL을 방문할 필요는 없다.
예를 들어
등은 미리 제거할 수 있다.
Extracted URL
↓
URL Filter
↙ ↘
Allow Drop
동일한 URL을 계속 추가하면 같은 페이지를 반복적으로 수집하게 된다.
따라서 URL이
이미 방문했는가?
또는
이미 Frontier에 들어가 있는가?
를 확인해야 한다.
중복 확인에는
등을 활용할 수 있다.
처음 발견한 URL만 Frontier로 다시 전달한다.
정리하면 다음과 같다.
1. Seed URL을 Frontier에 저장
↓
2. Downloader가 URL 조회
↓
3. DNS를 통해 서버 확인
↓
4. 페이지 다운로드
↓
5. Content Parsing
↓
6. 중복 콘텐츠 검사
↓
7. 콘텐츠 저장
↓
8. URL 추출
↓
9. URL Filtering
↓
10. 방문 여부 확인
↓
11. 새로운 URL만 Frontier에 추가
↓
12. 반복
웹 페이지는 그래프로 바라볼 수 있다.
Page = Node
Hyperlink = Edge
따라서 웹 크롤링은 그래프 탐색 문제이기도 하다.
하나의 경로를 가능한 깊게 따라가는 방식이다.
A
└─ B
└─ C
└─ D
└─ ...
웹은 사실상 끝을 예측하기 어려울 정도로 큰 그래프이기 때문에 특정 경로에 지나치게 깊게 들어갈 수 있다.
현재 발견된 페이지 주변부터 넓게 탐색한다.
A
/ | \
B C D
/ \
E F
일반적으로 FIFO Queue를 사용한다.
웹 크롤링에서는 하나의 경로를 깊게 따라가기보다 여러 페이지를 넓게 탐색하는 것이 유리하기 때문에 BFS 기반 접근을 사용할 수 있다.
하지만 단순 BFS에도 문제가 있다.
한 페이지에서 추출되는 링크 대부분이 같은 사이트 내부 링크일 수 있다.
예를 들어
wikipedia.org/page1
wikipedia.org/page2
wikipedia.org/page3
...
가 Frontier에 연속해서 들어간다면 FIFO 순서대로 처리할 때 Wikipedia 서버에 요청이 집중될 수 있다.
Wikipedia
↑ ↑ ↑ ↑ ↑ ↑
Crawler Request
이는 크롤러의 Politeness 문제로 이어진다.
FIFO 방식은 먼저 발견한 URL을 먼저 처리할 뿐 페이지의 중요도는 고려하지 않는다.
하지만 실제 크롤링에서는 중요한 페이지를 먼저 방문하는 것이 효율적일 수 있다.
우선순위의 기준으로는
등을 사용할 수 있다.
따라서 URL Frontier는
Priority
+
Politeness
두 가지를 동시에 처리해야 한다.
URL Frontier를 두 종류의 Queue로 나누어 볼 수 있다.
Front Queue
→ Priority 담당
Back Queue
→ Politeness 담당
즉
Front Queue는 무엇을 먼저 수집할지, Back Queue는 어느 사이트를 지금 요청해도 되는지를 결정한다.
URL의 중요도에 따라 서로 다른 Queue에 넣는다.
f1 → 낮은 Priority
f2 → 중간 Priority
f3 → 높은 Priority
Prioritizer가 URL의 중요도를 계산한다.
그리고 Queue Selector는 높은 Priority Queue를 더 자주 선택한다.
High Priority
→ 자주 처리
Low Priority
→ 상대적으로 적게 처리
다만 낮은 Priority URL이 영원히 처리되지 않는 상황은 방지해야 한다.
Back Queue는 Host별 요청 속도를 관리한다.
예를 들어
wikipedia.org → Queue B1
apple.com → Queue B2
nike.com → Queue B3
처럼 같은 Host에 속한 URL을 동일한 Queue에 넣는다.
URL에서 Host를 추출한다.
https://www.apple.com/mac
↓
apple.com
그리고 Mapping Table을 이용하여 해당 Host가 어느 Queue를 사용하는지 찾는다.
Host
↓
Mapping Table
↓
Back Queue
이 구조를 통해 같은 웹사이트의 URL을 같은 Queue에서 관리할 수 있다.
Queue Selector는 각 Host의 마지막 요청 시간을 확인한다.
현재 요청을 보내도 되는 Host의 Queue에서 URL 하나를 꺼내 Worker에게 전달한다.
Back Queues
↓
Queue Selector
↓
Worker
↓
Download
Worker가 페이지를 내려받은 뒤 같은 사이트에는 일정 시간 후 다시 요청한다.
apple.com/page1
↓
Wait
↓
apple.com/page2
이를 통해 특정 서버에 대한 요청 폭주를 막을 수 있다.
웹 페이지는 계속 변경된다.
따라서 한 번 수집한 페이지라고 해서 영원히 다시 방문하지 않는 것은 아니다.
Page Crawled
↓
시간 경과
↓
Page 변경
↓
Recrawl 필요
하지만 모든 페이지를 같은 주기로 다시 수집하면 비용이 너무 커진다.
따라서
변경이 잦은 Page
→ 자주 Recrawl
중요한 Page
→ 자주 Recrawl
거의 바뀌지 않는 Page
→ 긴 주기로 Recrawl
처럼 재수집 주기를 다르게 가져갈 수 있다.
대규모 크롤러는 수억~수십억 개의 URL을 관리할 수 있다.
모든 URL을 메모리에 올리면 공간이 부족하다.
하지만 모든 작업을 Disk에서 처리하면 I/O 때문에 느려질 수 있다.
따라서
Disk
→ 대부분의 URL 저장
Memory
→ 현재 처리에 필요한 URL Buffer
를 함께 사용하는 방식이 가능하다.
그리고 메모리 상태는 주기적으로 디스크에 기록하여 장애 발생 시 복구할 수 있도록 한다.
웹사이트에는 크롤러가 어떤 경로를 수집해도 되는지 알려주는 robots.txt가 존재할 수 있다.
일반적으로
https://example.com/robots.txt
에서 확인한다.
대표적인 규칙으로는
User-agent
Disallow
등이 있다.
크롤러는 이를 확인하여 사이트가 제외하도록 요청한 경로를 수집하지 않도록 한다.
다만 robots.txt는 보안 장치가 아니라 크롤러가 따라야 하는 정책을 알리는 방식이다.
페이지를 요청할 때마다 robots.txt를 다시 다운로드하면 비효율적이다.
따라서
robots.txt 조회
↓
Cache 저장
↓
재사용
↓
일정 시간 후 갱신
하는 방식으로 처리할 수 있다.
하나의 Downloader가 모든 페이지를 처리하지 않는다.
URL Frontier
├─ Downloader 1
├─ Downloader 2
├─ Downloader 3
└─ Downloader N
각 서버 내부에서도 여러 Worker Thread를 사용할 수 있다.
이를 통해 전체 처리량을 높인다.
매번 DNS를 조회하면 그만큼 지연이 발생한다.
따라서
example.com
→ IP
결과를 일정 시간 동안 캐시한다.
가능하다면 크롤링 서버와 대상 서버 사이의 물리적 거리를 줄여 네트워크 Latency를 감소시킬 수 있다.
캐시, Queue, 저장소 역시 지역적으로 적절하게 배치할 수 있다.
특정 서버가 응답하지 않는다고 Worker 하나가 오랫동안 기다리면 처리량이 떨어진다.
Request
↓
Timeout 초과
↓
Abort
↓
다음 URL
따라서 적절한 Timeout 정책이 필요하다.
대규모 크롤러는 언제든 중단될 수 있다.
따라서
등을 지속성 저장소에 기록해둘 수 있다.
Crawler 장애
↓
재시작
↓
저장된 상태 조회
↓
중단 지점부터 재개
Downloader 서버를 여러 대 운영한다면 URL을 여러 서버로 분배해야 한다.
이때 이전 장에서 배운 Consistent Hashing을 적용할 수 있다.
URL
↓ Hash
Downloader Server
서버가 추가되거나 제거되어도 일부 URL만 재배치되므로 확장에 유리하다.
인터넷에는 예측할 수 없는 상황이 많다.
따라서 하나의 URL에서 예외가 발생해도 전체 크롤링 작업은 계속되어야 한다.
URL A → Error
URL B → 계속 처리
URL C → 계속 처리
다운로드한 데이터 역시 검증해야 한다.
예를 들어
등을 확인할 수 있다.
이 단계에서 비정상적인 콘텐츠를 걸러내면 이후 시스템의 안정성을 높일 수 있다.
웹에는 HTML뿐 아니라 다양한 콘텐츠가 존재한다.
따라서 크롤러를 모듈화하여 새로운 처리 방식을 추가할 수 있도록 한다.
Crawler Core
├─ HTML Parser
├─ URL Extractor
├─ PNG Downloader
├─ PDF Processor
└─ Web Monitor
새로운 요구사항이 생길 때 전체 크롤러를 수정하지 않고 새로운 모듈만 추가하는 것이 목표다.
웹에는 크롤러의 리소스를 낭비하게 만드는 콘텐츠도 많다.
대표적으로
중복 콘텐츠
Spider Trap
데이터 노이즈
가 있다.
서로 다른 URL이 동일한 내용을 제공할 수 있다.
URL A ─┐
├→ 동일 Content
URL B ─┘
이를 모두 저장하면 공간이 낭비된다.
따라서 콘텐츠의 Hash나 Checksum을 비교하여 중복 여부를 확인한다.
크롤러가 사실상 끝없이 새로운 URL을 발견하도록 만들어진 구조다.
예를 들어
/foo/bar
/foo/bar/foo/bar
/foo/bar/foo/bar/foo/bar
...
처럼 URL이 계속 늘어날 수 있다.
크롤러 입장에서는 모두 서로 다른 URL처럼 보일 수 있다.
이런 상황에서는
등을 활용할 수 있다.
다만 모든 Spider Trap을 자동으로 완벽하게 찾아내기는 어렵다.
크롤링 목적과 크게 관련 없는 데이터도 존재한다.
예를 들어
등이다.
이런 데이터까지 전부 저장하면 저장 공간과 후속 처리 비용이 증가한다.
따라서 URL Filter와 Content Parser를 이용해 가능한 한 제거한다.
현대 웹사이트는 HTML 안에 모든 콘텐츠가 처음부터 들어 있지 않을 수 있다.
JavaScript가 실행된 뒤 콘텐츠나 링크가 생성될 수 있다.
HTML 다운로드
↓
JavaScript 실행
↓
추가 Content 생성
단순 HTTP 다운로드만 수행하면 이런 데이터를 확인하지 못할 수 있다.
필요한 경우 실제 렌더링 과정까지 수행하는 구조를 고려해야 한다.
크롤러가 수집하는 데이터가 계속 증가하면 저장 계층도 확장해야 한다.
동일한 데이터를 여러 서버에 복제한다.
Data
├→ DB1
└→ DB2
장애 대응과 읽기 가용성을 높일 수 있다.
데이터 자체를 여러 서버로 나눈다.
전체 데이터
↓
Shard 1
Shard 2
Shard 3
저장 공간과 처리량을 수평으로 확장할 수 있다.
Downloader는 가능하면 Stateless하게 구성한다.
Downloader 1
Downloader 2
Downloader 3
...
현재 크롤링 상태는 외부 저장소가 관리하고 Downloader는 전달받은 URL의 다운로드에 집중한다.
그러면 트래픽에 따라 서버를 쉽게 추가하거나 제거할 수 있다.
웹 크롤러도 실제로 어떻게 동작하는지 지속적으로 확인해야 한다.
예를 들어
Crawling Success Rate
Response Time
Duplicate Ratio
Error Rate
Page Update Frequency
Host Request Rate
Crawler Throughput
등을 수집할 수 있다.
이 데이터를 분석하여
등을 조정할 수 있다.
이번 장의 전체 흐름을 연결하면 다음과 같다.
Seed URL 선정
↓
URL Frontier 저장
↓
Priority에 따라 URL 선택
↓
Host별 요청 간격 확인
↓
HTML Downloader
↓
DNS 조회
↓
페이지 다운로드
↓
Content Parsing
↓
중복 Content 검사
↓
Content Storage
↓
URL Extract
↓
URL Filtering
↓
방문 여부 검사
↓
새로운 URL을 Frontier에 추가
↓
반복
대규모 환경에서는 여기에
분산 Downloader
+
Consistent Hashing
+
Cache
+
Persistent Storage
+
Replication / Sharding
+
Failure Handling
+
Monitoring
등이 추가된다.
결국 웹 크롤러 설계의 핵심은
가능한 많은 페이지를 빠르게 수집하는 것이 아니라, 가치 있는 페이지를 우선적으로 선택하고 대상 사이트에 부담을 주지 않으면서 안정적으로 수집하는 것
이라고 이해할 수 있다.
처음에는 웹 크롤러를
페이지 방문
→ 링크 추출
→ 다음 페이지 방문
정도의 단순한 반복 프로그램으로 생각했다.
하지만 대규모 환경에서는 URL을 발견하는 것보다 발견한 URL을 어떻게 관리할 것인가가 훨씬 중요한 문제였다.
특히 URL Frontier가 단순한 FIFO Queue 하나가 아니라
Front Queue
→ 중요한 페이지 우선
Back Queue
→ 동일 Host 요청 속도 제어
처럼 두 가지 문제를 함께 해결한다는 점이 인상 깊었다.
또 웹 크롤러는 내 시스템의 성능만 생각하면 되는 것이 아니라 크롤링 대상 서버에 피해를 주지 않는 Politeness도 시스템 요구사항으로 고려해야 한다는 점도 새로웠다.
결국 이번 장은
BFS
→ Priority
→ Politeness
→ Recrawl
→ Distributed Crawling
→ Failure Handling
처럼 단순한 그래프 탐색 알고리즘이 실제 대규모 웹 수집 시스템으로 확장되는 과정을 배운 장이었다.