Scaling Slack's Job Queue

Bài tóm tắt Slack 2017 2. Tăng trưởng Độ khó: Trung bình

Slack · 2017 · 2. Tăng trưởng · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Job queue chạy mọi việc không làm kịp trong request web: mỗi tin nhắn, push notification, unfurl URL, nhắc lịch, tính tiền. Ngày bận nhất hơn 1,4 tỉ job, đỉnh 33.000 job/giây.
  • Kiến trúc từ những ngày đầu: web app băm ID job chọn host Redis, khử trùng có giới hạn, worker poll Redis, chuyển job sang danh sách đang chạy, lỗi thì sang hàng thử lại.
  • Sự cố khoảng một năm trước: DB tranh chấp làm job chạy chậm, Redis đầy bộ nhớ tối đa nên không enqueue được, mọi thao tác dựa vào queue đều lỗi; tệ hơn, dequeue cũng cần chút bộ nhớ trống (để chuyển job sang danh sách đang chạy), nên khi DB đã ổn queue vẫn kẹt, phải can thiệp tay nhiều.
  • Postmortem chỉ ra: Redis ít headroom; mọi client nối tới mọi Redis (đồ thị hai phía đầy đủ); thêm worker là thêm poll lên Redis đang quá tải (muốn tăng capacity xử lý lại làm Redis tệ hơn); dequeue tốn công tỉ lệ với độ dài queue (queue càng dài càng khó rút); ngữ nghĩa và QoS mơ hồ nên engineer ngại dùng.

Thay đổi chính

  • Thay đổi tối thiểu: đặt Kafka trước Redis thay vì thay Redis bằng Kafka (thay hẳn sẽ phải viết lại logic lập lịch, chạy, khử trùng); interface enqueue/dequeue của app giữ nguyên.
  • Kafkagate (Go, không trạng thái): app PHP/Hack POST HTTP, Kafkagate giữ kết nối tới broker. Ưu tiên sẵn sàng hơn nhất quán: chỉ chờ leader ack (rủi ro nhỏ mất job nếu broker chết trước khi replicate); ghi đồng bộ để trả lỗi rõ ràng; định tuyến ưu tiên Kafkagate cùng AZ (giảm latency và phí giữa AZ, vẫn failover được).
  • JQRelay (Go, không trạng thái): mỗi topic một relay nhờ khoá Consul; chỉ tiến offset Kafka khi đã ghi được vào Redis, Redis lỗi thì thử mãi; lỗi riêng của job thì đẩy lại Kafka thay vì vứt hay chặn cả queue; rate limit ghi vào Redis cấu hình qua Consul watch. Bẫy: bộ mã hoá JSON của Go (escape <, >, &) và PHP (escape /) cho chuỗi khác nhau.
  • Cụm Kafka: 16 broker, mỗi topic 32 partition, RF 3, giữ 2 ngày, replication theo AZ, bật unclean leader election. Load test để sizing có headroom; test lỗi (kill một, hai broker cùng AZ, cả ba để ép unclean leader, restart cụm).
  • Rollout: ghi kép vào Redis và Kafka, JQRelay ở chế độ bóng (đọc rồi bỏ); đếm và so số job qua từng chặng; heartbeat canary mỗi phút cho mọi partition (50 cụm Redis, 1.600 partition) có alert theo luồng và thời gian; bật nội bộ vài tuần rồi bật dần theo loại job.
  • Kết quả: khi enqueue nhanh hơn dequeue, job dồn vào Kafka bền thay vì làm Redis cạn bộ nhớ; chỉnh rate limit của JQRelay theo tốc độ dequeue hoặc tạm dừng đẩy vào Redis.

Vì sao

  • Kho trong bộ nhớ làm hàng đợi thì dồn ứ biến thành sự cố; thêm một bộ đệm bền ở phía trước và một van điều tiết là đủ để giải quyết điểm nghẽn nghiêm trọng nhất mà không viết lại.

Bài học cho SWE

  • Kiểm xem thao tác phục hồi (dequeue) có cần chính tài nguyên đang cạn không; nếu cần thì hệ không tự thoát được.
  • Thêm worker để tăng capacity có thể làm nghẽn nặng hơn nếu worker tạo tải lên chính chỗ nghẽn (poll).
  • Đổi kiến trúc hàng đợi bằng một lớp chèn vào giữa, ghi kép, chế độ bóng và canary nhịp tim.

Liên quan