저는 모든 게재된 주장이 가져온 증거(출처 페이지, 제출 문서, 데이터셋)로 추적되어야 하는 정보 플랫폼 AiSeek을 운영하고 있습니다. 이 때문에 데이터 수집 계층(fetch layer)이 전체 제품의 공급망 역할을 합니다. 만약 이 계층이 느리거나, 낭비가 심하거나, 조용히 잘못된다면, 하위 모든 과정에 그 문제가 전파됩니다.
10월 21일까지 이 계층은 Crawl4AI였습니다. 이는 페이지 렌더링이 필요한 경우 Chromium을 구동하는 유능하고 인기 있는 Python 프레임워크였습니다. 저는 이것이 무겁다는 느낌을 받았습니다. 단순히 느낌에 의존하기보다, 제가 직접 감사(audit)를 진행했습니다. 운영 환경에서 수행된 3일간의 작업(총 8,563건, 10월 18일~20일), 운영 데이터베이스, 그리고 운영 박스와 동일한 제한 조건(코어 6개, 10GB) 하에 실행된 로컬 재현 테스트를 진행했습니다. 아래 제시된 모든 수치는 별도로 언급하지 않는 한 모두 운영 데이터를 기반으로 합니다.
이 감사에서 밝혀진 내용, 그로 인해 제가 구축한 것들, 그리고 사람들이 보통 출시 글에서 빠뜨리는 부분을 공유합니다.
크롤러가 하루 동안 실제로 수행한 작업
평소에 AiSeek은 일반 HTTP를 통해 약 2,000개의 기사를 수집했으며, 이 중 600~900개를 브라우저로 보냈습니다. 그 결과, 기사 수집의 약 28%만이 실제로 렌더링되었습니다. 두 경로는 서로 다른 지연 시간(latency) 환경에서 작동했습니다:
| 경로 | 작업 건수 | p50 | p95 | p99 |
|---|
| HTTP 전용 | 2,879 | 1.6 s | 5.8 s | 10.9 s |
| 렌더링 (Crawl4AI) | 1,249 | 7.0 s | 24.4 s | 40.2 s |
실제로 보니, 렌더링된 페이지들은 대부분 굳이 그렇게 할 필요가 없었던 페이지들이었습니다. 하지만 먼저 더 큰 놀라움부터 말씀드리겠습니다.
발견 1: 저는 프레임워크를 브라우저 드라이버로 사용하고 있었다
제가 로컬에서 재현한 174건의 성공적인 렌더링 중, 166건은 최종 텍스트 추출을 위해 제가 직접 만든 trafilatura를 사용했습니다. Markdown Crawl4AI가 생성한 것을 사용한 경우는 단 8건에 불과했습니다. 이 Markdown은 모든 페이지에서 계산되었지만, 그중 95%는 버려졌습니다.
이 거대한 프레임워크로부터 제가 실제로 소비한 것은 최소한의 정보였습니다:
- 렌더링된 HTML,
- 리다이렉트 후 최종 URL,
- 상태 코드(status code),
- 링크 목록,
- 제목 대체값(title fallback).
아이러니는 더욱 커진다. 파이프라인에서 가장 무거운 단일 파싱 단계는 Crawl4AI의 것도 아니었다. 나 자신의 trafilatura 호출은 페이지당 중앙값 82ms가 소요되었고, Crawl4AI의 스크래핑과 Markdown 생성을 합친 비용은 약 40ms였다. 느리다고 의심했던 라이브러리는 그 자체로는 대부분 무해했다.
나는 Crawl4AI를 사용하고 있지 않았다. 프레임워크가 부착된 브라우저를 임대하여 사용했고, 매 페이지마다 그 프레임워크의 비용을 지불하고 있었다.
발견 2: 렌더링 시간의 55%가 순수한 낭비였다
렌더링을 위한 결정 규칙은 하나의 논리적 줄이었다. HTTP 가져오기(fetch)가 1,200자 미만의 텍스트를 생성하면 브라우저에서 렌더링하라는 것이었다. 이 규칙은 JavaScript로 막힌 페이지와 짧은 기사를 구별할 수 없었고, AiSeek의 많은 소스는 중국어와 일본어로 된 짧은 뉴스 요약본이 많아서, 여기서 800자는 완전한 이야기 분량이다.
프로덕션 결정 로직을 로컬에서 재현해 보니: HTTP가 이미 사용 가능한 페이지를 가져왔음에도 불구하고 규칙이 어쨌든 브라우저로 보낸 59건의 경우 중, 37개의 렌더링(63%)은 HTTP와 동일한 양의 텍스트를 반환했다. NHK는 렌더링 전 849자였고, 후에도 849자였다. 이 낭비된 렌더링 과정은 총 154.6초 중 85초를 소모했다: **이미 가지고 있던 페이지를 재가져오는 데 사용된 전체 렌더링 시간의 55%**에 해당한다.
이것이 감사에서 가장 가치 있는 발견이었으며, 언어나 엔진과는 아무 관련이 없었다. 가장 큰 속도 향상은 언제 렌더링할지에 대한 더 나은 결정이었다. 그 규칙을 재작성하는 것이 어떤 엔진 교체보다 더 가치가 있었다.
발견 3: 실제 비용은 Chromium과 공유 이벤트 루프였다
실제로 기기를 소모한 것은 다음과 같았다:
- Chromium 자체만으로도. 각 동시 렌더링되는 페이지는 약 0.7 CPU 코어와 수백 MB, 그리고 약 1.7초의 콜드 스타트(cold start)를 소모합니다. 드라이버 언어 변경으로는 이 계산을 바꿀 수 없습니다.
- 동기식 작업을 수행하는 단일 이벤트 루프 (single event loop doing synchronous work). Crawl4AI는 HTML 정리 및 Markdown 변환을 Python 이벤트 루프에서 동기적으로 실행합니다. 4개의 동시 페이지를 사용했을 때, 제 측정한 프로브(probe) 결과 p99 지연 시간은 154–172ms였고 최악의 경우 2.2초에 달했습니다. 이 시간 동안 해당 프로세스 내의 모든 것—단순 HTTP 페치와 공통 프로세스 및 10 GB 컨테이너를 공유하는 GLiNER 및 MiniCheck 모델까지—이 대기했습니다. 제가 10월 9일에 작성한 오래된 코드 주석에는 "페이지가 렌더링되는 동안 단순 페치는 중앙값으로 8초가 걸렸다"고 적혀 있습니다. 이제 저는 그 이유를 알게 되었습니다.
- 낮은 동시성 상한선 (low concurrency ceiling). 6코어 환경에서 페이지 제한을 테스트해 본 결과: 4페이지는 1.87 req/s를 기록했고, 8페이지는 2.19 req/s로 최고치를 찍었지만, 16페이지에서는 p50 지연 시간이 18초까지 증가하며 오히려 1.72 req/s로 감소했습니다. 특정 지점을 넘어서면 페이지가 많아질수록 단순히 대기열만 길어지게 됩니다.
- 용량 (Weight). 서비스 이미지는 6.64 GB였습니다. 페치 경로만으로도 약 1 GB에 달하는 115개의 패키지를 불러왔는데, 여기에는 우리가 한 번도 호출하지 않은 여러 라이브러리들이 포함되어 있었습니다.
발견 4: 실패 원인이 크롤러의 잘못이 아닌 경우도 많다
운영 환경에서 약 25%의 페치가 실패했습니다. 저는 실패 그룹에서 70개의 URL을 가져와 집 네트워크 연결과 운영 서버에서 동시에 페치해 보았습니다. 집에서는 70개 중 39개가 전체 텍스트를 반환했지만, 운영 서버에서는 제로였습니다. 나머지는 거부(403/451)되거나 속도 제한(429)되었습니다. 전체 출판사 그룹이 저희 데이터센터의 아웃바운드를 차단하고 있으며, 일부는 IP 평판 문제가 아니라 지리적 문제입니다 (451은 "법적 이유로 이용 불가"; 운영 서버는 Türkiye에 위치합니다).
어떤 엔진도 이를 고칠 수 없으며, AiSeek의 규칙에 따라 저는 거부된 요청을 우회하지 않습니다. 정직한 사용자 에이전트(user agent)를 사용하고 robots 규정을 준수하며, 거부는 최종적입니다. 이 내용을 포함하는 이유는 크롤러 벤치마크에서는 절대 언급되지 않기 때문입니다—운영 환경에서 발생하는 실패의 4분의 1은 귀하의 스택과는 아무 관련이 없을 수 있습니다.
제가 복사하지 않겠다고 약속한 버그들
감사 과정에서는 대체 솔루션이 물려받지 않기를 바라는 결함들까지도 목록화했습니다:
- 첫 리디렉션 단계의 상태 코드. 174건 중 136건의 성공적인 렌더링 보고서가 시작된 301/302/308 코드를 보였습니다. 따라서 '301 → 404'와 같은 상황이 성공으로 저장될 수 있었습니다.
- 페이지당 중첩되는 SSRF 경로 가드. 페이지를 열 때마다 공유 브라우저 컨텍스트에 핸들러가 추가되었습니다. 재활용 시간 동안 최대 200개까지 쌓였습니다.
- DNS 실패가 SSRF 거부로 보고됨. '이 호스트는 금지됨'과 '이 호스트가 일시적으로 해결되지 않음'이라는 두 가지 상황에 동일한 400 에러 코드가 사용되어, 호출자가 정반대로 처리해야 하는 상황을 만들었습니다.
- 네트워크 오류가 단순한 500 에러로 통합됨. 이는 우리 자체 코드의 버그와 구별할 수 없는 수준이었습니다.
대체 솔루션: Rust를 사용하여 최소 표면 구축하기
그래서 저는 기능 체크리스트가 아닌, 감사 과정에 맞춰 PageSift를 구축했습니다:
- 최소한의 정보만 추출. 렌더링된 HTML, 최종 URL, 최종 상태 코드와 리디렉션 체인, 링크, 그리고 Rust 측 추출 기능만을 포함합니다. 버려질 목적으로 마크다운을 생성하거나 불필요한 패키지(115개)를 사용하는 일이 없습니다.
- 별도의 프로세스. 자체 CPU 및 메모리 제한이 있으며, 모델에 영향을 주지 않고 재시작할 수 있습니다. 다른 사람의 이벤트 루프 위에는 동기적으로 작동하는 것이 전혀 없습니다.
- 더 스마트한 렌더 결정 후, 엔진 순서로 진행. HTTP가 우선이며, 렌더링은 가치를 창출하는 곳에만 사용됩니다.
- 브라우저 대신 브라우저 체인(chain)을 사용. Lightpanda가 먼저 렌더링하고, Lightpanda가 처리할 수 없는 페이지는 Chrome의 헤드리스 셸이 담당합니다. 둘 다 샌드박스 처리되어 실행되며, 절대
--no-sandbox를 사용하지 않습니다. 이는 저희 호스트 환경에서 AppArmor 및 seccomp 프로파일을 작성해야 함을 의미했습니다.
- 정직한 실패 보고. 블록(block)은 블록으로 보고되며, 절대로 가짜 콘텐츠로 대체되지 않습니다. 증거 플랫폼에게는 선언된 공백이 조작된 페이지보다 낫습니다.
- 동일한 API 계약 유지. 워커는 계속해서
POST /v1/fetch를 호출하고 동일한 응답 스키마를 검증했습니다. 이 교체 작업은 사이드카(sidecar) 한 곳만 건드렸을 뿐, 다른 것은 전혀 만지지 않았습니다.
10월 21일 사이드카(sidecar)가 도입되었고, Crawl4AI는 같은 날 코드와 이미지를 남겼습니다. 그날의 배포 노트에는 결정 사항이 소괄호 안에 기록되어 있습니다: (owner: 관찰 주간 없음). 감사가 바로 관찰이었습니다.
출시 게시물에서 빠진 부분: 프로덕션은 여전히 v0.01을 실행 중이다
여기 고백합니다. AiSeek의 프로덕션 빌드는 PageSift를 고정된 커밋(pinned commit)으로부터 구축하는데, 이는 최초로 작동하는 빌드이며 0.01이라고 부릅니다. 그리고 오늘도 여전히 그 빌드를 사용하고 있습니다.
그 이후로 오픈 소스 프로젝트는 스프린트했습니다: 0.50은 Tranco Top 100k를 크롤링했습니다, 0.61이 이어졌고, 이번 주에는 NUMA 인식 데이터 평면(NUMA-aware data plane)을 재작성하여 (192 vCPU 박스에서 로컬 테스트 오리진 대비 초당 약 95만 페이지 처리), 세 가지 엔진의 적응형 체인, 그리고 전체 파이프라인을 888 MiB 이하로 압축하는 메모리 엔벨로프를 갖춘 0.70 버전이 출시되었습니다. 하지만 프로덕션은 아무것도 가지고 있지 않습니다. 데이터 평면도 없고, 세 번째 엔진도 없고, 수정된 여섯 가지 버전도 없습니다.
업그레이드는 의도적으로 지루합니다: 고정된 커밋 하나를 변경하고, 두 개의 엔진 체크섬(checksums)을 업데이트하며, 서버에서 검증한 다음 푸시하는 과정입니다. 이는 감사가 진행되었던 것과 같은 이유로 일어나지 않았습니다. 하루 약 2,800개의 페치(fetch) 수준에서는 처리량(throughput)이 AiSeek의 제약 조건이 결코 아니었습니다. 감사는 문제가 판단 규칙과 공유 프로세스에 있다는 것을 보여주었고, 0.01은 이미 이 두 가지를 모두 해결했습니다. 프로덕션이 정체 상태로 얻는 것은 사이트의 모든 주장이 내가 정확히 알고 있는 동작 방식을 가진 바이너리에 의해 가져온 증거와 연결된다는 점입니다.
업그레이드가 이루어지면, 이번 교체 과정과 마찬가지로 감사받게 될 것입니다: 프로덕션 수치가 먼저, 발표가 나중입니다. 그것이 Part 3가 될 것입니다.
교훈을 얻자면, 만약 이와 유사한 것을 운영한다면: 교체하기 전에 크롤러를 프로파일링하십시오. 프레임워크에 비용을 지불하고 브라우저를 사용하고 있을 뿐이며, 가장 빠른 최적화는 새로운 엔진이 아니라 한 줄의 결정 규칙일 수 있다는 것을 알게 될 수도 있습니다.
PageSift는 오픈 소스(AGPL-3.0, 상업용 라이선스 이용 가능)이며, github.com/YOLOW92/pagesift에서 확인할 수 있습니다. 이를 실제 제품으로 활용하는 곳은 aiseek.dev입니다.