Worker Backpressure (Part 1): How we taught our queue workers to slow down
Canva · 2026 · 3. Quy mô lớn · Độ khó: Trung bình — bài gốc
Phần 1 (vì sao và thiết kế tổng quát). Phần 2 (thuật toán controller, mô phỏng) chưa đăng khi wiki đọc.
Bối cảnh
- Canva chạy hàng nghìn queue; worker được viết "tham lam": có message và còn sức là lấy xử lý ngay. Lúc dependency khoẻ thì đó là điều mong muốn.
- Khi dependency (datastore, service khác) bắt đầu lỗi, worker tham lam vẫn kéo message với tốc độ tối đa: đổ dầu vào lửa, phí tài nguyên cho message chắc chắn hỏng, message rơi xuống DLQ phải xả tay, và on-call bị gọi.
Những gì các team đã làm trước đó (và vì sao chưa đủ, theo Canva)
| Cách | Hạn chế |
|---|---|
| Scale tay số worker | Scale lên có thể dồn thêm tải vào chính dependency đang hỏng; con số nào cũng là đoán |
| Rate limit cố định trong code | Chỉ đúng khi năng lực không đổi; năng lực dependency dùng chung thay đổi liên tục |
| Circuit breaker | Chỉ có bật/tắt, không có dốc; mở lại đột ngột có thể quật ngã dependency vừa hồi |
| Exponential backoff | Làm mượt retry từng message, không điều tiết tổng tải |
| Adaptive backoff (client-side throttling trong sách SRE của Google) | Một team đã dùng tốt và là nguồn cảm hứng, nhưng nằm ngoài thư viện queue chung và cố định một thuật toán |
Thay đổi chính
Worker Backpressure là một vòng phản hồi dựng sẵn trong thư viện queue:
- Signal: sau mỗi message, ghi kết quả thành công hay thất bại.
- Controller (một interface, thay thuật toán được) giữ một số duy nhất, backoff factor từ 0,0 (tốc độ tối đa) tới 1,0 (lùi hẳn), so với một set point là tỉ lệ lỗi coi như nhiễu nền (mặc định 5%). Dưới set point thì không làm gì.
- Permit: trước mỗi lần poll, worker hỏi controller được kéo bao nhiêu message đồng thời; con số này giảm theo backoff factor.
Mọi thứ chạy cục bộ trên từng instance, không có điều phối viên bên ngoài, không thêm network call; chi phí là hai phép tính số học. Về tên gọi: backpressure thường là tín hiệu ngược lên producer, còn worker tự ghìm gần với concurrency-limits của Netflix hơn; họ giữ tên vì từ chối việc ở worker thì tải nằm lại trong queue, thứ "upstream" duy nhất có thể đẩy ngược.
Kết quả (hai sự cố production, 2026)
| Sự cố cloud provider (~4 giờ, lỗi từng đợt) | Quá quota của queue đích (32,5 giờ liên tục) | |
|---|---|---|
| Thành công | 1.610.173 | 63.285.748 |
| Lỗi | 498 | 1.795.025 lần thử |
| Đỉnh lỗi toàn fleet | 1,42% (từng instance lên tới 50%) | 3,7% (từng instance tới ~19%) |
| DLQ tăng | 1 message | 22 message |
Ở sự cố thứ hai, throughput vẫn khoảng 2 triệu message/giờ, cao hơn trước sự cố. Theo ước tính của Canva, không có backpressure thì DLQ ít nhất khoảng 16.000 message. Không ai bị gọi dậy trong cả hai lần.
Đánh đổi
- Mất throughput: worker chạy sát công suất sẽ cảm thấy rõ.
- Chỉ một tín hiệu (thành công/thất bại) làm đại diện cho sức khoẻ dependency; latency, số message đang bay có thể thêm sau.
Bài học cho SWE
- Với async worker, "quá tải" không chỉ ở phía nhận request: người gây tải chính là worker của mình. Cho nó tự lùi khi dependency lỗi (Load shedding, rate limit và backpressure).
- Dốc liên tục (backoff factor) tốt hơn bật/tắt (circuit breaker) vì tránh được đợt dồn tải khi mở lại (Hot partition và thundering herd).
- Đặt cơ chế vào thư viện chung với mặc định tốt thì hàng nghìn queue được bảo vệ mà không team nào phải tự viết.
- Đo bằng thứ đau thật: số message xuống DLQ và số lần gọi on-call.