Cách chúng tôi xây dựng pipeline dưới 30ms của tracio.ai
Từ thu thập tín hiệu đến visitor ID trong chưa đầy 30ms: kiến trúc của chúng tôi dùng Go, ClickHouse, Redis và xử lý phân tán.
Khi bắt tay xây dựng engine nhận diện thiết bị của tracio.ai, chúng tôi có một yêu cầu không thể thương lượng: toàn bộ pipeline — từ lúc nhận tín hiệu được mã hóa đến lúc trả về visitor ID — phải hoàn tất trong chưa đầy 30 mili-giây ở phân vị thứ 95. Bài viết này trình bày chi tiết kiến trúc mà chúng tôi đã xây dựng để đạt mục tiêu đó.
Tổng quan pipeline
Pipeline nhận diện có năm giai đoạn: giải mã tín hiệu, chuẩn hóa tín hiệu, tính hash, identity resolution và tuần tự hóa phản hồi. Mỗi giai đoạn được tối ưu độc lập, và các giai đoạn có thể chạy song song thì chạy song song. Tổng ngân sách là 30ms, phân bổ đại khái như sau: giải mã 2ms, chuẩn hóa 3ms, tính hash 2ms, identity resolution 20ms, tuần tự hóa 1ms. 2ms còn lại là bộ đệm.
Giải mã tín hiệu đảo ngược lớp truyền tải được mã hóa ở phía client. Chúng tôi dùng các gói crypto của Go với tăng tốc phần cứng, hoàn tất việc giải mã một payload 4KB điển hình trong chưa đầy 1ms. Chuẩn hóa phân tích cú pháp JSON của tín hiệu, kiểm tra kiểu dữ liệu và áp dụng các phép biến đổi theo từng nền tảng — ví dụ, chuẩn hóa chuỗi user agent để loại bỏ nhiễu đặc thù theo phiên bản.
Identity resolution phân tán
Identity resolution — xác định xem thiết bị này đã từng được nhìn thấy hay chưa — là giai đoạn nhạy cảm nhất về độ trễ. Chúng tôi lưu profile thiết bị trong Redis, phân mảnh (shard) trên một cụm bằng lớp định tuyến key phân tán. Việc định tuyến phân phối các key dựa trên fingerprint tầng phần cứng, đảm bảo rằng các lượt tra cứu cho cùng một thiết bị luôn trúng cùng một node Redis.
Cách triển khai sharding của chúng tôi dùng virtual node (150 trên mỗi node vật lý) để đảm bảo phân phối đồng đều. Khi thêm hoặc bớt một node, chỉ 1/N số key cần được ánh xạ lại, với N là số node. Chúng tôi triển khai lớp định tuyến bằng Go với thời gian tra cứu O(log n) và không cấp phát bộ nhớ (zero allocation).
Redis làm kho lưu định danh
Chúng tôi chọn Redis thay vì các lựa chọn khác (Memcached, ScyllaDB, DynamoDB) nhờ thời gian phản hồi ổn định dưới một mili-giây và hỗ trợ các cấu trúc dữ liệu phức tạp. Mỗi profile thiết bị được lưu dưới dạng một hash Redis với các trường cho hash của từng tầng tín hiệu, visitor ID, timestamp lần xuất hiện gần nhất và metadata độ tin cậy.
Truy vấn identity resolution là một lệnh HGETALL duy nhất, tiếp theo là một phép so sánh giữa các hash tín hiệu đến với các hash đã lưu. Nếu tầng phần cứng khớp, chúng tôi trả về visitor ID hiện có với độ tin cậy cao. Nếu chỉ tầng phần mềm khớp, chúng tôi thực hiện một phép so sánh mức độ tương đồng của dữ liệu ở cấp tín hiệu để xác định đây có phải cùng một thiết bị với trình duyệt đã cập nhật hay không. Nếu không có gì khớp, chúng tôi tạo một visitor ID mới.
ClickHouse cho lưu trữ sự kiện
Mọi sự kiện nhận diện đều được ghi vào ClickHouse một cách bất đồng bộ. Chúng tôi dùng một buffered writer gom lô các lệnh insert — thu thập sự kiện trong 100ms hoặc cho đến khi tích lũy đủ 1.000 sự kiện, tùy điều kiện nào đến trước. Việc gom lô này rất quan trọng vì ClickHouse hoạt động tốt nhất với các lệnh insert lớn (hàng nghìn hàng mỗi lần) thay vì insert từng hàng riêng lẻ.
Schema ClickHouse của chúng tôi được tối ưu cho hai mẫu truy vấn phổ biến nhất: tra cứu tất cả sự kiện cho một visitor ID cụ thể, và tổng hợp sự kiện theo các khoảng thời gian. Chúng tôi dùng engine MergeTree với primary key là (visitor_id, timestamp), cho phép tra cứu điểm nhanh và quét khoảng hiệu quả. Các materialized view duy trì các chỉ số được tổng hợp trước theo ngày và theo giờ.
Đạt dưới 30ms ở quy mô lớn
Ba quyết định kiến trúc mang tính then chốt để đạt mục tiêu độ trễ. Thứ nhất, pipeline hoàn toàn theo kiểu streaming — chúng tôi bắt đầu xử lý tín hiệu trước khi nhận xong toàn bộ phần thân của HTTP request. Thứ hai, các lượt tra cứu Redis dùng connection pooling với kết nối bền vững, loại bỏ chi phí bắt tay TCP. Thứ ba, các lệnh ghi ClickHouse hoàn toàn bất đồng bộ và không bao giờ chặn đường phản hồi.
Trong kiểm thử tải với 50K request/giây, độ trễ p50 của chúng tôi là 12ms, p95 là 24ms và p99 là 38ms. Đôi khi p99 vượt mục tiêu 30ms trong lúc cụm Redis tái cân bằng, nhưng p95 vẫn ổn định dưới 30ms. Với các khách hàng có yêu cầu độ trễ khắt khe hơn, chúng tôi cung cấp các cụm Redis riêng biệt để loại bỏ tranh chấp tài nguyên giữa nhiều tenant.