Ngày 14 tháng 9
Chúng tôi đã chờ ngày này suốt nhiều tuần. Nhà cung cấp proxy dân cư (residential) của chúng tôi gặp sự cố thanh toán và "tắt điện" từ tháng 7, và trong hai tháng liền, một phần ngày càng lớn trong số 1.375 nguồn dữ liệu của chúng tôi chỉ thu thập được ở trạng thái suy giảm hoặc không chạy được gì cả — các lượt fetch bị timeout, địa chỉ IP bị chặn giữa phiên, kiểu "chảy máu chậm" không hiện ra như một sự cố sập hệ thống, mà chỉ là một con số cứ nhỏ dần đi mà chẳng ai để ý, cho đến khi nó trở nên đáng xấu hổ. Chúng tôi đã gia hạn. Một gói proxy mới, 20.000 dòng session proxy dân cư xoay vòng, được kiểm tra (probe) từng điểm thoát (exit) trước khi cho chạm vào production. 100% lượt probe đạt. AE, DE, JP — tất cả đều hoạt động, đều sạch.
Vậy nên chúng tôi chạy một lượt crawl có giám sát — kiểu mà có người thực sự ngồi theo dõi dashboard thay vì tin tưởng mù quáng vào cron. Tôi đã kỳ vọng điều ngược lại với một thảm họa.
Thứ chúng tôi nhận được là một cơ sở dữ liệu đã mất 84.401 dòng chỉ trong một lượt chạy, với 102 nguồn sụt hơn 25% so với snapshot trước đó. Tệ hơn cả hai tháng "đói" proxy mà chúng tôi vừa khắc phục.
Giả thuyết sai, kể ngắn gọn
Phản ứng đầu tiên của tôi — và tôi sẽ nhận trách nhiệm thẳng thắn về điều này — là nghĩ rằng gói proxy mới, bằng cách nào đó, lại tệ hơn những gì lượt probe cho thấy. Có thể các IP phiên (session IP) bị giới hạn tốc độ (rate-limit) giữa lúc crawl theo cách mà một lượt probe một lần không thể phát hiện ra. Đó là một giả thuyết hợp lý. Nhưng nó cũng sai, và việc tin vào nó dù chỉ nửa ngày đã khiến chúng tôi mất thời gian mà chúng tôi không có dư, vì cron crawl phải tạm dừng suốt thời gian bạn đang gỡ lỗi một giả thuyết như vậy — bạn không muốn một lượt chạy tồi ghi đè lên một snapshot tốt trong khi bạn vẫn còn đang đoán.
Dấu hiệu phá vỡ giả thuyết đó là dubizzle. Đây là một trong những nguồn có khối lượng dữ liệu cao nhất của chúng tôi tại UAE, và thực tế nó đã fetch được 32.595 tin đăng trong lượt chạy đó — một con số hoàn toàn khỏe mạnh, proxy rõ ràng vẫn hoạt động tốt — rồi sau đó lại loại bỏ gần như toàn bộ. Không phải fetch thất bại. Mà là fetch được, rồi vứt đi.
Ngày 16 tháng 9: tìm ra lỗi thật
Đó là lúc câu chuyện không còn là chuyện proxy nữa mà trở thành chuyện ghi đè dòng dữ liệu (row-replacement). Tầng lưu trữ của chúng tôi, ở mỗi lượt chạy, snapshot từng nguồn bằng cách ghi đè toàn bộ file <site>_cars_for_sale.csv — chỉ giữ các tin đăng hiện tại, thay thế toàn bộ bảng. Đó là hành vi đúng khi một lượt crawl hoàn tất. Nhưng nó là thảm họa khi một lượt crawl bị timeout giữa chừng và scraper ghi bất cứ lô dữ liệu từng phần nào nó thu thập được như thể đó là snapshot hoàn chỉnh. Một nguồn thường có 9.000 tin đăng đang hoạt động sẽ bị ghi đè bằng 1.200 tin nó thu thập được trước khi hết thời gian, và 7.800 tin còn lại biến mất — không phải vì những chiếc xe đó đã được bán, mà vì chính code của chúng tôi quyết định rằng một lượt fetch chưa hoàn chỉnh là một lượt fetch hoàn chỉnh.
Không phải proxy đang nói dối chúng tôi. Vấn đề nằm ở logic retry, và nó đã âm thầm tồi tệ trong một thời gian dài, chỉ bị che lấp bởi một gói proxy khỏe mạnh hơn, trước đây thường hoàn tất các lượt fetch trước khi đợt quét (sweep) kịp gây hại.
Đây là cái giá thực sự của đợt sweep đó, khi chúng tôi làm phép tính:
| Đợt sweep retry cũ | Đợt sweep đã sửa | |
|---|---|---|
| Số lần retry mỗi proxy | 3 | 1 |
| Độ trễ mỗi lần thử | 1 giây | — |
| Ngưỡng tối đa trên toàn pool 50 proxy | ~56 phút mỗi URL | Tổng ngân sách 120 giây |
| Hành vi khi timeout | ghi lô dữ liệu từng phần như snapshot đầy đủ | bỏ qua nguồn, giữ snapshot trước đó |
56 phút để bỏ cuộc với một URL duy nhất. Nhân con số đó lên trên một lượt crawl chạm tới hơn một nghìn nguồn, và bạn sẽ có đúng kiểu thất bại âm thầm, phân bố không đồng đều mà chúng tôi đã chứng kiến — một số nguồn vẫn ổn, 102 nguồn bị tàn phá, không có gì trong log hét lên "proxy sập" vì, về mặt kỹ thuật, chẳng có vụ sập nào cả.
Ngày 25 tháng 9: bản vá, và phần quan trọng với bạn
Bản vá được triển khai chín ngày sau đó làm hai việc không hào nhoáng. Nó giới hạn lại phép toán retry để một lượt fetch bị kẹt thất bại nhanh thay vì "ăn" gần hết một giờ, và — đây là phần thực sự ngăn chặn mất dữ liệu — một lượt fetch từng phần không còn được phép thay thế toàn bộ tập dòng dữ liệu nữa. Nếu một nguồn chỉ lấy được 1.200 trong số 9.000 tin đăng thường có, chúng tôi giữ lại snapshot hoàn chỉnh trước đó thay vì ghi đè nó bằng một phần nhỏ. Cùng ngày, một thay đổi thứ hai được triển khai: tạo một cờ CRAWL_PAUSED hiển thị rõ ràng mà hệ thống giám sát của chúng tôi sẽ kiểm tra trước khi báo động cho ai đó về một sự sụt giảm — để một lượt tạm dừng có chủ đích được đọc là một lượt tạm dừng, không phải một cảnh báo sự cố.
Vì sao người đọc đang tìm mua một chiếc Hyundai cũ lại nên quan tâm đến bất kỳ điều gì trong chuyện này? Vì các nhãn "giá dưới thị trường" mà chúng tôi công bố — 12.823 nhãn đang hiển thị trên trang web tính đến tuần này — chỉ trung thực khi nhóm so sánh (cohort) mà chúng tôi dùng để đối chiếu một tin đăng là thị trường khu vực thực sự, đầy đủ, không phải một mảnh vỡ bị cắt cụt vì timeout. Một nhãn "deal" được xây dựng dựa trên một snapshot đã mất ba phần tư tập dữ liệu so sánh thì không còn là nhãn "deal" nữa, mà chỉ là nhiễu khoác một cái mác màu xanh. Chúng tôi thà tạm dừng một lượt crawl trong chín ngày còn hơn công bố thứ đó.
Hiện tại, bộ dữ liệu của chúng tôi có 1.108.535 tin đăng trên 73 khu vực và 1.375 nguồn — lớn hơn trước khi mọi chuyện này bắt đầu, với proxy đã được gia hạn và lỗi ghi đè dòng dữ liệu đã được khắc phục. Nhưng con số tôi vẫn cứ nhớ đến không phải là tổng số đó. Mà là 84.401 — khoảng cách giữa một lượt crawl trông có vẻ khỏe mạnh ở tầng proxy, và một lượt crawl thực sự khỏe mạnh.



