Taking our Ruby & Kafka stack to the next level
Bài tóm tắt Doximity 2022 2. Tăng trưởng Độ khó: Trung bình
Doximity · 2022 · 2. Tăng trưởng · Độ khó: Trung bình — bài gốc
Bối cảnh
- Mọi app Rails ở Doximity nói chuyện với Kafka qua gem nội bộ
dox-messaging, vốn bọc ruby-kafka: API thân thiện hơn, ép best practice, mặc định hợp với use case của họ. - Khi dùng Kafka nhiều hơn: nút thắt ở service phía sau consumer, ruby-kafka chậm ra tính năng, và nhất là heartbeat chạy chung thread với xử lý. Heartbeat chỉ gửi khi consumer poll, tức là khi mọi message trước đã xử lý xong; quá hạn heartbeat thì cả consumer group rebalance. Xử lý chậm làm rebalance lặp liên tục: "rebalancing storm", mọi xử lý đứng lại.
- Thêm consumer (tới bằng số partition) gần như không giúp, thường còn tệ hơn: càng nhiều consumer càng dễ có một cái quá hạn heartbeat.
Thay đổi chính
- Chuyển sang rdkafka-ruby, lớp bọc thư viện C librdkafka (phổ biến nhất hệ sinh thái): nhanh hơn, có tính năng của librdkafka ngay, heartbeat ở thread nền riêng.
- Nhờ gem có factory pattern cho producer/consumer và một object cấu hình chung, họ thay toàn bộ phần ruby-kafka bên dưới mà giữ nguyên interface; PoC xong dưới hai tuần. Benchmark 10.000 message: producer nhanh hơn 1,5 giây, consumer nhanh hơn 5,3 giây. Sau PoC là đề xuất viết, được cả R&D duyệt.
- Phải tự làm những gì rdkafka thiếu: pause partition, metric statsd, tracing, xử lý lỗi theo namespace mới, cấu hình riêng theo topic khi tạo topic; batched consumer thì được merge upstream đúng lúc.
- Rollout từ app ít rủi ro nhất trước.
Segfault trong production
- Lúc khởi động: Passenger preload rồi fork worker; rdkafka có một thread C mà vùng nhớ của nó không được thiết kế để fork. Sửa tạm bằng hook sau fork (Passenger, Spring, Puma mỗi cái một kiểu) rồi nhận ra đang eager-load producer qua một biến global
$producers. Sửa gốc: producer registry lazy-load, chỉ tạo client rdkafka ở lần gửi đầu tiên (sau khi app đã boot), cache lại. - Lúc chạy (Puma trên Kubernetes, không fork): ngẫu nhiên trên nhiều pod. Tăng memory limit giảm tần suất nhưng không hết; giả thuyết dữ liệu hỏng từ Segment bị loại (dữ liệu mã hoá Avro, không có lỗi encoder). Nâng rdkafka-ruby lên 0.11.1 (librdkafka 1.4 → 1.8) thì hết hẳn.
- Testing trước production không bắt được vì traffic mẫu khác traffic thật, và họ deploy hơn 100 lần/ngày (mỗi lần là một lần khởi động). Có lúc bị đề nghị rollback hoặc bỏ dự án.
Kết quả (theo Doximity, 2022)
- Hết rebalancing storm; on-call hết bị ping liên tục.
- Một consumer từ đỉnh khoảng 150.000 (trung bình dưới 100.000) message/phút lên tối thiểu 400.000; lag hàng trăm nghìn tới hàng triệu message mỗi ngày gần như biến mất. Họ cũng phân tán tải phía sau consumer, nhưng theo Doximity chỉ vậy thì không đủ nếu giữ ruby-kafka.
- Dùng được tính năng mới như static membership (giảm rebalance khi deploy).
Bài học cho SWE
- Heartbeat chung thread với xử lý biến xử lý chậm thành rebalance, rồi rebalance thành bão. Thêm consumer làm tệ hơn (Event-driven, message queue và Kafka).
- Thư viện client nội bộ có interface ổn định (factory, config object) cho phép thay ruột mà không đụng app (Nền tảng nội bộ cho service (đường ray chung)).
- Thư viện bọc C mang theo rủi ro mới (fork, bộ nhớ, segfault); rollout từ app ít quan trọng nhất và lazy-load tài nguyên có thread.
- Cùng bài toán "lag không giảm khi thêm pod" với trivago 2026 (trivago 2026).
Liên quan
Được nhắc tới trong
Công ty (1)
Bài tóm tắt (1)