Dynein: Building a Distributed Delayed Job Queueing System

Bài tóm tắt Airbnb 2019 3. Quy mô lớn Độ khó: Trung bình

Airbnb · 2019 · 3. Quy mô lớn · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Job nền ở Airbnb gồm cả job hẹn giờ xa (nhắc check-in trước nhiều tháng, thậm chí nhiều năm). Yêu cầu: at-least-once, scale ngang, cô lập theo service, p95 lệch giờ dưới 10 giây, ack từng message, dead letter queue, mỗi service một worker pool riêng, huỷ được job.
  • Hệ cũ: một cụm Resque trung tâm của monolith Rails, chạy trên một instance Redis: at-most-once (có thể mất job), giới hạn bởi RAM và mạng một máy, một job xấu làm đứng phần lớn worker; Resque Scheduler giữ mọi job hẹn giờ trong Redis, huỷ job phải quét cả backlog. Thêm một hệ tự viết trên MySQL cho hẹn giờ dài, nhất quán nhưng không scale.

Thay đổi chính

  • Hàng đợi = SQS, mỗi service một queue (tạo bằng một PR Terraform): đơn giản, không cần thứ tự hay lưu lâu như Kafka, at-least-once, có sẵn DLQ, ack từng message, phân quyền. Job chạy trong 15 phút: Dynein chỉ bọc API SQS (để có metric và gắn hệ rate limit/backpressure nội bộ).
  • Job hẹn giờ: vào một SQS "inbound" làm bộ đệm ghi chống đỉnh (và là tín hiệu khi có sự cố), service không trạng thái lấy ra theo tốc độ đều, lưu trigger vào scheduler; tới giờ thì đẩy sang queue của service đích.
  • Scheduler, phiên bản 1 = Quartz trên MySQL: chỉ đạt khoảng 1.000 QPS mỗi instance RDS r4.8xlarge dù tắt phần lớn khoá; nút thắt là khuếch đại query: một lần enqueue thành 4 SELECT + 3 INSERT cho những tính năng (lịch lặp...) không dùng. Danh sách scheduler tĩnh, không hợp Pod Kubernetes hay xoay vòng.
  • Phiên bản 2 = DynamoDB tự viết: việc của scheduler chỉ là "mỗi tick tìm job quá hạn rồi đẩy đi". Partition key = ID ngẫu nhiên (tải đều, tránh hot partition), sort key = giờ hẹn + UUID (range query hiệu quả). Chống giao trùng bằng conditional update trạng thái Scheduled → Acquired (optimistic locking) trước khi đẩy, rồi xoá. Pod scheduler theo dõi ReplicaSet của mình và tự tính tập partition theo danh sách pod, nên scale bằng Deployment thường hoặc autoscaler.
  • Kết quả theo bài: 1.000 QPS với chưa tới 1/3 chi phí RDS, scale gần như tuyến tính không cần shard tay, chỉnh throughput theo mùa; interface với người dùng không đổi khi thay scheduler.

Vì sao

  • Queue và scheduler cần đánh đổi khác nhau; gộp chung (Quartz làm queue, Resque Scheduler) thì thừa tính năng ở chỗ này, thiếu ở chỗ kia. Tách thành phần nhỏ giữ interface cho phép thay ruột.

Bài học cho SWE

  • Đo khuếch đại query khi một thư viện tổng quát chạy trên workload hẹp: phần lớn chi phí là tính năng không dùng.
  • Mô hình "index theo thời gian + quét quá hạn" shard được tầm thường nếu khoá phân vùng ngẫu nhiên.
  • Mỗi service một queue là cô lập lỗi rẻ nhất khi hạ tầng tạo queue gần như miễn phí.

Liên quan