9월 14일
우리는 이날을 몇 주 동안 기다려왔다. 7월에 레지덴셜 프록시 공급업체가 결제 장애로 먹통이 된 이후, 두 달 동안 1,375개 소스 중 점점 더 많은 비중이 저하된 상태로 크롤링되거나 전혀 크롤링되지 못하고 있었다 — 가져오기가 타임아웃되고, 세션 중간에 IP가 차단되는, 당장 장애처럼 보이지는 않지만 서서히 줄어드는 숫자로 나타나다가 어느 순간 누구도 눈치채지 못한 채 민망한 수준이 되어버리는 그런 종류의 느린 출혈이었다. 우리는 갱신을 진행했다. 새로운 풀, 2만 개의 로테이팅 레지덴셜 세션 라인, 프로덕션에 붙이기 전에 모든 exit를 프로빙했다. 프로브 통과율 100%. AE, DE, JP — 전부 살아있고, 전부 깨끗했다.
그래서 우리는 감독 크롤링(supervised crawl)을 돌렸다 — 크론을 그냥 믿는 대신 실제로 누군가가 대시보드를 지켜보는 방식으로. 나는 그 반대의 결과, 즉 재앙과는 정반대의 결과를 기대했다.
하지만 실제로 벌어진 일은 단 한 번의 패스에서 데이터베이스가 84,401개의 행을 잃고, 102개 소스가 이전 스냅샷 대비 25% 이상 줄어든 것이었다. 우리가 막 해결한 두 달간의 프록시 고갈보다 더 나쁜 결과였다.
틀렸던 가설, 짧게
내 첫 직감은 — 이건 솔직하게 인정하겠다 — 새 풀이 프로브 결과보다 실제로는 더 나쁘다는 것이었다. 세션 IP들이 크롤링 중간에 속도 제한에 걸리는데, 단발성 프로브로는 그걸 잡아내지 못했을 수 있다는 식이었다. 합리적인 가설이다. 하지만 틀렸고, 그 가설을 반나절만 믿고 있었던 것만으로도 우리에게는 없던 시간을 허비한 셈이었다. 그런 가설을 디버깅하는 동안에는 크롤링 크론을 계속 멈춰둘 수밖에 없으니까 — 아직 추측 단계인데 나쁜 패스가 좋은 스냅샷을 덮어쓰게 둘 수는 없다.
그 가설을 깨뜨린 결정적 증거는 dubizzle이었다. UAE에서 가장 물량이 많은 소스 중 하나인 이 소스는 그 크롤링 중에 실제로 32,595건의 매물을 가져왔다 — 프록시가 명백히 잘 작동하고 있다는 뜻의 완전히 건강한 수치다 — 그런데 그중 거의 전부를 폐기해버렸다. 가져오기에 실패한 게 아니다. 가져온 다음, 버린 것이다.
9월 16일: 실제 버그 찾아내기
바로 이 지점에서 이야기는 프록시 문제가 아니라 '행 교체' 문제로 바뀌었다. 우리의 퍼시스턴스 레이어는 매 패스마다 각 소스를 <site>_cars_for_sale.csv 전체 덮어쓰기 방식으로 스냅샷한다 — 현재 매물만 남기고 테이블 전체를 교체하는 방식이다. 크롤링이 끝까지 완료됐을 때는 이게 맞는 동작이다. 하지만 크롤링이 중간에 타임아웃되고 스크래퍼가 그때까지 모은 부분 배치를 완전한 스냅샷인 것처럼 써버리면 치명적인 결과가 된다. 평소 9,000건의 활성 매물을 보유하던 소스가 타임아웃 전에 긁어온 1,200건으로 덮어써지고, 나머지 7,800건은 사라진다 — 차량이 실제로 팔려서가 아니라, 우리 코드 자체가 불완전한 가져오기를 완전한 가져오기로 판단해버렸기 때문이다.
프록시는 우리에게 거짓말을 한 게 아니었다. 문제는 재시도 로직이었고, 그것은 꽤 오랫동안 조용히 망가져 있었다 — 다만 예전의 더 건강했던 풀이 스윕이 문제가 되기 전에 가져오기를 끝내버렸기 때문에 가려져 있었을 뿐이다.
실제로 계산해보니 그 스윕이 초래한 비용은 다음과 같았다.
| 이전 재시도 스윕 | 수정된 스윕 | |
|---|---|---|
| 프록시당 재시도 횟수 | 3 | 1 |
| 시도당 지연 | 1초 | — |
| 50개 프록시 풀 전체의 실질적 상한 | URL당 약 56분 | 총 120초 예산 |
| 타임아웃 시 동작 | 부분 배치를 전체 스냅샷으로 기록 | 해당 소스는 건너뛰고 이전 스냅샷 유지 |
하나의 URL을 포기하는 데 56분이 걸렸다. 그걸 1,000개가 넘는 소스를 건드리는 크롤링 전체에 곱하면, 우리가 목격한 것과 정확히 같은 종류의 조용하고 불균등하게 분포된 장애가 나온다 — 일부 소스는 괜찮고, 102개는 완전히 망가졌는데, 로그에는 "프록시 장애"를 외치는 기록이 전혀 없다. 왜냐하면 기술적으로는, 프록시 장애가 없었기 때문이다.
9월 25일: 수정, 그리고 여러분에게 중요한 부분
9일 뒤에 배포된 패치는 화려하지 않은 두 가지 일을 했다. 하나는 재시도 계산에 상한을 둬서, 멈춘 가져오기가 거의 한 시간을 잡아먹기 전에 빠르게 실패하도록 만든 것이다. 그리고 — 실제로 데이터 손실을 막는 쪽은 이것인데 — 부분적인 가져오기가 더 이상 전체 행 집합을 교체하지 않게 만든 것이다. 어떤 소스가 평소 9,000건 중 1,200건만 가져왔다면, 그 일부로 덮어쓰는 대신 이전의 완전한 스냅샷을 그대로 유지한다. 같은 날, 두 번째 변경도 함께 들어갔다. 모니터링이 숫자 하락에 대해 누군가를 호출하기 전에 먼저 확인하는, 눈에 보이는 CRAWL_PAUSED 플래그를 기록하는 것이다 — 이로써 의도적인 일시정지가 알림이 아니라 일시정지로 제대로 읽히게 됐다.
중고 Hyundai를 알아보고 있는 독자가 왜 이런 이야기에 신경 써야 할까? 우리가 게시하는 시세 이하(below-market) 플래그 — 이번 주 기준 사이트에 12,823개가 걸려 있다 — 는 어떤 매물을 비교하는 기준이 되는 코호트가 진짜이고 완전한 지역 시장 전체일 때만 정직하기 때문이다. 타임아웃으로 잘려나간 비교 대상의 4분의 3을 잃어버린 스냅샷을 기반으로 만든 딜 배지는 딜 배지가 아니라 녹색 라벨을 두른 잡음일 뿐이다. 우리는 그런 걸 게시하느니 9일 동안 크롤링을 멈추는 쪽을 택하겠다.
오늘 기준 데이터셋은 73개 지역, 1,375개 소스에 걸쳐 1,108,535건의 매물로, 이 모든 일이 시작되기 전보다 오히려 더 커졌다 — 프록시는 갱신됐고 행 교체 버그는 닫혔다. 하지만 내가 계속 돌아가서 떠올리게 되는 숫자는 총량이 아니다. 84,401 — 프록시 계층에서는 건강해 보였던 크롤링과 실제로 건강했던 크롤링 사이의 간극이다.



