목차7개 항목
출처와 참고
Andrew Chan의 「Crawling a billion web pages in just over 24 hours, in 2025」(2025)를 바탕으로 재구성한 노트다.
무엇을 정하는 문제인가
크롤러가 웹이라는 그래프를 어떻게 탐색하는지(URL 프런티어, 재크롤링, PageRank)는 웹 검색엔진의 동작에 있다. 이 노트는 그 크롤러를 정해진 시간과 예산 안에 실제로 돌리는 시스템 설계를 다룬다. 기준 규모는 HTML만 가져와 <a> 링크를 따라가는 크롤링으로 하루 10억 페이지, 머신 십여 대다.
| 제약 | 내용 |
|---|---|
| 예의(politeness) | robots.txt를 지키고, 연락처를 넣은 User-Agent를 쓰고, 요청이 오면 도메인을 제외 목록에 넣는다. 같은 도메인에는 수십 초 이상 간격을 둔다. 작은 사이트를 보호하려면 시드 도메인 목록 안에서만 돈다 |
| 장애 허용 | 멈췄다가 이어서 돌 수 있어야 한다. 크롤링은 웹의 표본이므로 복구할 때 방문 기록 일부를 잃어도 된다 |
| JavaScript | 실행하지 않으면 훨씬 싸다. 실제로 렌더링해야 하는 크롤링은 같은 규모라도 몇 배 비싸다. 렌더링 방식과 크롤러의 관계는 SEO 기초 |
robots.txt를 무시하거나 다른 에이전트로 위장하거나 한 엔드포인트를 쉼 없이 두드리는 크롤러는 사이트 운영자에게 큰 부담이다. AI 학습용 수집이 늘면서 사이트 쪽 방어도 늘었고, Cloudflare처럼 크롤링에 값을 매기는(pay-per-crawl) 시도도 있다.
설계: 기능별로 나누지 않는다
교과서적 분리 설계는 파싱, 페칭, 저장소, 크롤 상태를 각각 다른 머신 풀로 나눈다. 예산이 작을 때는 반대로, 모든 기능을 담은 독립 노드가 시드 도메인을 나눠 맡는 편이 단순하고 싸다. 노드 사이 통신이 없으므로 노드를 더하는 것만으로 확장된다.
flowchart LR
subgraph node["노드 하나 (예: 16 vCPU, 128GB RAM, 25Gbps, 로컬 NVMe 수 TB)"]
redis["Redis: 크롤 상태<br/>· 도메인별 프런티어 (가져올 URL 목록)<br/>· 준비 큐: 다음에 가져와도 되는 시각순으로 정렬한 도메인<br/>· 방문한 URL + 메타데이터 + 저장 경로<br/>· 본 URL 블룸 필터<br/>· 도메인 메타데이터 (제외 여부, robots.txt와 만료 시각)<br/>· 파싱 큐"]
f["페처 N개"] -->|"가져온 HTML"| pq["파싱 큐"] --> p["파서 M개"]
p -->|"링크 추출"| fr["도메인 프런티어"] --> f
p -->|"본문 저장"| disk["디스크"]
end× 노드 여러 대. 시드 도메인을 노드별로 나눈다
| 부품 | 동작 |
|---|---|
| 준비 큐 | 다음 요청 가능 시각순으로 도메인을 정렬한다. 페처는 준비된 도메인을 꺼내 그 도메인의 프런티어에서 URL 하나를 가져오고, 도메인을 새 시각으로 다시 넣는다. 도메인당 지연이 자료 구조에 들어 있다 |
| 블룸 필터 | URL이 프런티어에 이미 들어갔는지 빠르게 확인한다. 아직 가져오지 않은 URL도 걸러야 하므로 방문 기록과 따로 둔다. 가끔 처음 보는 URL을 봤다고 잘못 판정하지만 표본이라 괜찮다 |
| 페처 | I/O를 기다리는 일이라 비동기(asyncio 등)로 코어 하나에 수천 개의 동시 요청을 돌린다 |
| 파서 | CPU를 쓰는 일이라 프로세스 수로 늘린다. 프로세스당 동시 작업은 수십 개면 CPU가 찬다 |
| 로컬 캐시 | robots.txt 같은 도메인 정보를 페처·파서가 LRU 캐시로 들고 있어 상태 저장소 부하를 줄인다 |
asyncio 같은 사용자 공간 멀티태스킹이 문맥 전환 없이 높은 동시성을 얻는 원리는 비동기 서버의 이벤트 루프에 있다.
| 판단 | 이유 |
|---|---|
| 크롤 상태는 인메모리 저장소(Redis) | 자료 구조는 단순하지만 프런티어 질의가 매우 잦다. SQLite·PostgreSQL로는 빠르게 만들기 어렵다 |
| 샤드를 너무 잘게 나누지 않는다 | 인기 도메인을 맡은 노드만 바빠지는 핫 샤드가 생긴다 |
| 상태 저장소 하나에 붙는 작업자 수에 상한이 있다 | Redis 한 대의 초당 연산 수가 먼저 찬다. 노드당 작업자 수를 그 아래로 맞춘다 |
| 거대한 단일 머신보다 수평 확장 | 큰 머신은 네트워크는 남아도 OS 자원 같은 소프트웨어 한계로 처리량이 비례해 늘지 않는다 |
저장소: 요청당 과금을 계산한다
객체 스토리지(S3)는 용량뿐 아니라 요청마다 값을 받는다. 페이지 10억 개를 하루 저장하면 PUT 요청 비용만으로 크롤링 전체 비용보다 한 자릿수 커진다. 그래서 스토리지 최적화 인스턴스의 로컬 NVMe에 저장하고, 용량은 snappy 같은 빠른 압축(또는 페이지 자르기)으로 맞춘다.
병목은 CPU로 옮겨 갔다
| 병목 | 2010년대 초 | 2025년 무렵 |
|---|---|---|
| 네트워크 대역폭 | 대학·사내망을 다 쓰지 않도록 속도를 늦췄다 | 데이터센터 대역폭이 넘친다. 노드당 25Gbps 중 약 3분의 1만 쓴다 |
| DNS | 조회 처리량이 병목이었다 | 상위 도메인 위주라면 문제가 되지 않는다 |
| 파싱 | 쿼드코어로 충분했다 | 가장 큰 병목. 페이지가 커졌다 |
| TLS | HTTPS가 드물었다 | 핸드셰이크 계산이 CPU 시간의 약 4분의 1을 쓴다 |
- 페이지가 커졌다. 상위 도메인 페이지의 비압축 크기 중앙값은 100KB를 넘고, 평균은 2010년대 초의 몇 배다. 처방은 빠른 파서(HTML5 전용 C++ 파서 Lexbor를 감싼 selectolax 등)로 바꾸고, 파서에 넘기기 전에 평균보다 넉넉한 크기에서 자르는 것이다.
- HTTPS가 기본이 됐다. Firefox에서 HTTPS로 열리는 페이지는 2014년 30%에서 2025년 80% 이상이 됐다. 그래서 페칭도 네트워크가 아니라 CPU에 묶인다. HTTPS 연결 과정은 HTTPS와 TLS에 있다.
메모리는 꼬리에서 터진다
프런티어를 모두 메모리에 두는 설계는 평균이 아니라 꼬리로 계산해야 한다. 방문 기록과 HTTP 클라이언트 메모리는 전체 페이지 수로 어림할 수 있지만, 링크가 아주 많은 인기 도메인 몇 개(대형 포털, 위키백과)의 프런티어는 URL 수억~수십억 개, 수십 GB로 불어난다. 도메인별 프런티어 크기에 상한을 두고, 넘치면 잘라 낸다. 장애 허용 설계가 있으면 쓰러진 노드를 재시작해 이어 가기 쉽다. 이렇게 불어나는 도메인은 대개 크롤러를 붙잡아 두는 함정 사이트(tarpit)가 아니라 정상적인 인기 사이트다.
로그 로테이션처럼 작은 운영 설정도 규모가 열 배로 뛰면 디스크를 채운다. 본 크롤링 전에 목표 규모에 가까운 리허설을 한다.
교과서 어림과 비교
널리 알려진 「페칭 전용 머신 몇 대로 며칠에 100억 페이지」 같은 어림에서 대역폭 사용률 30% 가정 자체는 현실과 맞는다. 다만 그 숫자에 이르려면 파싱과 TLS를 포함한 CPU 최적화가 어림보다 훨씬 많이 필요하다.
정리
- 예의(robots.txt, 도메인당 지연, 시드 제한)가 첫 번째 제약이다. 도메인당 지연은 준비 큐라는 자료 구조로 강제한다.
- 작은 예산에서는 기능별 머신 풀보다 도메인 샤드마다 모든 기능을 담은 독립 노드가 낫다. 크롤 상태는 Redis(프런티어, 준비 큐, 블룸 필터)에 둔다.
- 저장은 요청당 과금을 계산한다. 대량 소형 객체라면 로컬 스토리지가 한 자릿수 싸다.
- 병목은 네트워크·DNS에서 CPU로 옮겨 갔다. 빠른 파서, 페이지 자르기, TLS 비용을 감안한 CPU 배분으로 푼다.
- 메모리 계산은 평균이 아니라 꼬리를 본다. 인기 도메인 몇 개의 프런티어가 전체를 쓰러뜨린다.
관련 노트: 웹 검색엔진의 동작: 크롤링, 색인, 랭킹, 질의 처리, 비동기 서버의 이벤트 루프: 다중 접속 서버에서 Reactor 패턴까지, 웹 확장성의 구성 요소: 한 장으로 보는 웹 아키텍처, SEO 기초: 크롤링부터 메타 태그까지