Vindle tối ưu hóa việc nhập CSV quy mô lớn bằng cách thay thế PHP bằng Rust
Vindle đã giảm thời gian xử lý cho 27 GB dữ liệu sản phẩm bằng cách chuyển việc lọc CSV từ PHP sang một công cụ Rust tùy chỉnh, loại bỏ các bước phân tích dư thừa.
Bài được dịch tự động từ bản gốc tiếng Anh.
Vindle, một nền tảng so sánh giá, gần đây đã cải tổ toàn bộ quy trình tiếp nhận dữ liệu để xử lý một tích hợp mới với Bol.com. Trước tình trạng khối lượng sản phẩm tăng gấp ba bậc độ lớn, đội ngũ kỹ thuật đã thay thế trình phân tích ban đầu dựa trên PHP bằng một ứng dụng Rust tùy chỉnh. Sự chuyển đổi này cho phép họ xử lý hiệu quả 27 GB nguồn cấp dữ liệu CSV nén (gzipped) trong khi vẫn duy trì lịch cập nhật hàng giờ nghiêm ngặt.
Chuyện gì đã xảy ra
Thách thức bắt đầu khi Vindle tích hợp với Bol.com, một nhà bán lẻ tổng hợp cung cấp hơn 73 triệu sản phẩm. Dữ liệu được gửi dưới dạng 26 tệp CSV nén gzipped với tổng dung lượng khoảng 27 GB, kích thước mỗi tệp dao động từ 1,5 MB đến 4,8 GB. Vì Vindle cập nhật giá theo giờ, hệ thống cần phân tích, lọc và lưu trữ các mục này một cách nhanh chóng. Cách triển khai ban đầu sử dụng hàm fgetcsv() của PHP trong một ứng dụng Symfony. Mặc dù phương pháp này nhanh chóng để xây dựng và đủ tốt cho các nguồn cấp dữ liệu nhỏ, nó trở thành nút thắt cổ chai khi xử lý hàng triệu dòng dữ liệu mà hơn 99% bị loại bỏ do danh mục không liên quan hoặc thiếu dữ liệu.
Để giải quyết khoảng trống về hiệu suất, nhóm phát triển trước tiên đã giới thiệu Xan, một công cụ CSV dòng lệnh viết bằng Rust. Điều này cho phép họ chuyển việc lọc dòng và chọn cột khỏi PHP, chỉ truyền dữ liệu liên quan vào ứng dụng chính. Tuy nhiên, Vindle cũng yêu cầu thống kê chi tiết về lý do các dòng bị từ chối, chẳng hạn như thương hiệu không hợp lệ hoặc khu vực vận chuyển không được hỗ trợ. Xan không thể cung cấp các số liệu này trong một lần quét duy nhất, buộc nhóm phải chạy công cụ hai lần: một lần cho thống kê và một lần cho dữ liệu đã lọc. Cách tiếp cận quét kép này thêm 20–40 giây cho mỗi nguồn cấp dữ liệu và nhân đôi công việc phân tích CSV tốn kém.
Nỗ lực song song hóa hai quy trình Xan này bằng cách sử dụng đường ống Unix (Unix pipes) và tee đã thất bại do các vấn đề về áp suất ngược (backpressure). Nhánh chậm hơn chặn toàn bộ quy trình, và độ phức tạp của việc xử lý lỗi tăng lên mà không mang lại lợi ích về tốc độ. Cuối cùng, nhóm đã viết một chương trình Rust nhỏ, chuyên dụng thực hiện cả việc lọc và thu thập thống kê trong một lần quét duy nhất. Công cụ này đọc từ đầu vào chuẩn (standard input), áp dụng các quy tắc loại trừ, ghi các dòng hợp lệ vào đầu ra chuẩn (standard output) và gửi thống kê tới đầu ra lỗi chuẩn (standard error). Giải pháp này khôi phục hiệu quả của việc quét đơn lẻ đồng thời cung cấp những thông tin chi tiết cần thiết, tất cả mà không cần viết lại hoàn toàn lõi Symfony.
Chi tiết chính
- Bol.com cung cấp hơn 73 triệu sản phẩm trải rộng trên 26 tệp CSV nén gzipped với tổng dung lượng khoảng 27 GB.
- Triển khai PHP ban đầu sử dụng
fgetcsv()nhưng gặp khó khăn với khối lượng dữ liệu đòi hỏi việc lọc nặng nề. - Việc lọc loại bỏ từ 90% đến 99,9977% số dòng dựa trên các tiêu chí như danh mục, tính hợp lệ của giá và vị trí vận chuyển.
- Sử dụng Xan để tiền xử lý giúp cải thiện tốc độ nhưng đòi hỏi hai lần quét riêng biệt để thu thập cả dữ liệu đã lọc và thống kê từ chối.
- Song song hóa các quy trình Xan qua đường ống Unix gây ra các nút thắt cổ chai do áp suất ngược và không cải thiện thông lượng tổng thể.
- Một công cụ CLI Rust tùy chỉnh sử dụng thư viện
simd-csvhiện xử lý việc lọc và thống kê trong một lần quét, truyền trực tiếp kết quả sang PHP.
Bối cảnh
Việc phân tích CSV thường được coi là đơn giản, nhưng ở quy mô lớn, nó trở nên nặng về CPU và I/O. Trong các kịch bản khối lượng cao, chi phí đọc, giải nén và tách token từng ký tự trong tệp cộng dồn rất nhanh. Khi hầu hết các dòng bị loại bỏ, chi phí phân tích đầy đủ chúng trước khi từ chối là sự lãng phí tài nguyên tính toán. Các công cụ như Xan tận dụng các tối ưu hóa cấp thấp, chẳng hạn như chỉ thị SIMD (Single Instruction, Multiple Data - Một lệnh, nhiều dữ liệu), để đẩy nhanh quá trình phân tích này. Tuy nhiên, các công cụ chung có thể thiếu logic kinh doanh cụ thể cần thiết cho việc lọc phức tạp hoặc theo dõi thống kê. Viết một nhị phân nhỏ, tập trung bằng ngôn ngữ hệ thống như Rust cho phép các nhà phát triển kết hợp khả năng phân tích hiệu suất cao với logic tùy chỉnh, tránh chi phí phát sinh của các trình thông dịch đa năng như PHP trong các vòng lặp chặt chẽ.
Tại sao điều này quan trọng
Đối với các nhóm vận hành ứng dụng tự chủ (self-hosted), trường hợp này nhấn mạnh tầm quan trọng của việc xác định ranh giới đúng đắn giữa mã ứng dụng cấp cao và xử lý dữ liệu cấp thấp. Chuyển toàn bộ quy trình tiếp nhận sang Rust sẽ gây ra nợ bảo trì và độ phức tạp đáng kể. Bằng cách để Symfony chịu trách nhiệm điều phối, quản lý thông tin đăng nhập và lưu trữ cơ sở dữ liệu, nhóm vẫn giữ được lợi ích năng suất của PHP trong khi giải quyết nút thắt cổ chai hiệu suất cụ thể bằng một công cụ chuyên dụng. Cách tiếp cận lai này cho phép các kỹ sư tối ưu hóa các đường dẫn quan trọng mà không hy sinh sự dễ dàng trong phát triển mà framework chính của họ mang lại.
Ngoài ra, xu hướng chuyển sang dữ liệu luồng (streaming data) làm giảm sự phụ thuộc vào đĩa cứng. Quy trình ban đầu yêu cầu ghi các tệp tạm thời lớn vào đĩa, tiêu tốn đáng kể không gian lưu trữ và băng thông I/O. Công cụ Rust mới xử lý dữ liệu từ đầu vào chuẩn sang đầu ra chuẩn, cho phép một quy trình không dùng đĩa (diskless workflow). Điều này cực kỳ quan trọng đối với các môi trường có hạn chế về lưu trữ hoặc nơi mà sự cạnh tranh I/O có thể ảnh hưởng đến các dịch vụ khác. Nó chứng minh rằng các cải tiến hiệu suất thường đến từ những thay đổi kiến trúc, chẳng hạn như loại bỏ các lần quét dư thừa và giảm lưu trữ trung gian, chứ không chỉ đơn thuần là tối ưu hóa cú pháp mã.
Bạn có thể làm gì
- Hồ sơ hóa (Profile) các quy trình tiếp nhận dữ liệu của bạn để xác định xem phân tích hay lọc là nút thắt cổ chai chính trước khi viết lại mã.
- Cân nhắc chuyển giao việc xử lý nặng theo từng dòng sang các nhị phân bên ngoài được viết bằng các ngôn ngữ hệ thống như Rust hoặc Go.
- Tránh song song hóa các tác vụ phân tích giống hệt nhau qua đường ống Unix nếu các nhánh có khối lượng công việc không cân bằng, vì áp suất ngược sẽ giới hạn thông lượng.
- Thiết kế các công cụ CLI để chấp nhận đầu vào từ đầu vào chuẩn và ghi vào đầu ra chuẩn nhằm cho phép truyền dữ liệu theo luồng và giảm mức sử dụng đĩa.



