Introducing uForwarder: The Consumer Proxy for Kafka Async Queuing

Bài tóm tắt Uber 2026 3. Quy mô lớn Độ khó: Khó

Uber · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bối cảnh

  • Kafka ở Uber xử lý hàng nghìn tỉ message và nhiều PB mỗi ngày. Khoảng ba năm trước bài, Uber dựng Consumer Proxy: một cụm proxy lấy message từ Kafka bằng giao thức nhị phân, đẩy từng message tới consumer service qua gRPC, nhận mã trạng thái, gộp kết quả và commit offset khi an toàn. Lý do ban đầu: giới hạn scale theo partition, head-of-line blocking, hỗ trợ nhiều ngôn ngữ. Lúc viết hơn 1.000 consumer service dùng nó; đã mở mã nguồn là uForwarder.

Thay đổi chính

  • Bốn vấn đề khi đưa lên production:
    • Message "độc" (poison pill) hỏng trên đường đi, không tới được handler nên không vào được dead-letter queue: vượt giới hạn payload 4 MB mặc định của gRPC, instance đích (theo định tuyến cô lập) không hoạt động, interceptor từ chối request.
    • Đội proxy từ vài chục lên hàng nghìn máy; chi phí phần cứng tăng nhanh.
    • Nhiều consumer cần xử lý trễ (nguồn upstream chưa sẵn sàng khi event tới) và tự viết retry hay cơ chế delay riêng.
    • Cần cô lập: message non-production không được làm hỏng service production; message nên ở cùng zone giữa producer và consumer để sự cố một zone không lan.
  • Định tuyến theo ngữ cảnh: không chia topic con (phá tương thích với streaming analytics, ingest dùng chung topic); producer gắn ngữ cảnh vào header message, proxy chuyển thành header gRPC, load balancer định tuyến theo header.
  • Chủ động gỡ head-of-line blocking: dựa trên tracker commit không theo thứ tự (theo dõi trạng thái từng offset mỗi partition). Dấu hiệu bị chặn: tracker đầy trên 90% và tỉ lệ message chưa commit dưới 2% (ít message chặn phần lớn). Xử lý: đánh dấu offset CANCELED, huỷ request gRPC và các lần retry, gửi message vào DLQ, đánh dấu COMMITTED. Không mất dữ liệu.
  • Tự cân lại consumer: proxy gánh việc rebalance nên consumer service thành stateless. Kích thước mỗi workload (CPU theo request rate, bộ nhớ theo concurrency, mạng theo byte rate) được ước thích ứng theo metric traffic, scale lên nhanh, xuống chậm (đỡ lag khi thiếu tài nguyên, đỡ trùng message khi xáo vị trí); gán workload vào worker là dính, chỉ đổi khi vi phạm kích thước hoặc worker không qua liveness; nhiều workload nhỏ xếp chung một worker.
  • Delay trên topic chính: cách cũ dừng cả thread fetch tới khi message đủ trễ nên retry topic bị giới hạn một partition. DelayProcessManager chỉ pause partition chưa tới hạn, giữ message đã poll trong buffer, resume trước lượt poll kế; đảm bảo trễ ít nhất, không phải đúng bằng.
  • Hướng tiếp: khi lag lớn, tua offset về mới nhất và chạy một consumer phụ đuổi phần lag (cân độ tươi với độ đầy đủ); đẩy thẳng object Protobuf thay vì byte thô.

Vì sao

  • Tách việc đọc Kafka (offset, rebalance, retry, DLQ) khỏi consumer service thì mỗi service chỉ còn là một endpoint gRPC stateless, và các tính năng chung (cô lập, delay, gỡ chặn) làm một lần ở proxy.

Bài học cho SWE

  • DLQ chỉ cứu được message tới được handler; cần phát hiện chặn từ phía hàng đợi (tracker gần đầy mà gần hết đã xong).
  • Thêm định tuyến bằng metadata (header) thay vì chia topic, để không phá các consumer khác của cùng dữ liệu.
  • Scale lên nhanh, xuống chậm, và gán dính, để giảm cả lag lẫn xáo trộn.
  • Ghi rõ ngữ nghĩa đảm bảo (trễ ít nhất, không chính xác).

Liên quan