Pub/Sub là tính năng hay bị dùng sai nhất của Redis, vì nó trông giống một hàng đợi. Bài này đo ba tình huống cho thấy nó không phải.

Ba phép thử mất tin, hành vi với thuê bao chậm, và chi phí phát tán

Ba phép thử

1. Gửi khi chưa ai nghe:

gửi 100 bản tin rồi mới có thuê bao
thuê bao nhận được: KHÔNG GÌ CẢ

2. Gửi khi đã có thuê bao:

gửi 5 bản tin
nhận được: 5 / 5

3. Thuê bao ngắt kết nối rồi nối lại:

gửi trong lúc ngắt   : 0 / 50
gửi sau khi nối lại  : 3 / 3

Kết quả rõ ràng: Pub/Sub giao đúng những bản tin gửi trong lúc thuê bao đang kết nối, và không giao gì khác.

Đây là phát sóng, không phải hàng đợi

Không bộ đệm, không lịch sử, không xác nhận. PUBLISH ghi bản tin vào bộ đệm đầu ra của những kết nối đang đăng ký ngay tại thời điểm đó, rồi quên nó đi.

PUBLISH trả về số thuê bao nhận được — nếu là 0, bản tin vừa biến mất và không có gì báo cho bạn biết ngoài con số đó.

Điều này biến mọi lần khởi động lại ứng dụng, mọi lần triển khai, mọi sự cố mạng thành một khoảng mất tin. Với kết nối TCP có thể im lặng đứt hàng chục giây trước khi phát hiện, khoảng đó không nhỏ.

Nên nếu bạn đang dùng Pub/Sub cho thứ không được mất, hãy đọc lại phần về Stream: nhóm tiêu thụ giữ bản tin trong danh sách chờ cho tới khi có ai xác nhận. Đó là công cụ đúng cho việc đó.

Thuê bao đọc chậm thì bị ngắt

Tôi đăng ký một thuê bao rồi cố ý không đọc gì, và cho publisher phát liên tục:

publisher gửi 315.890 bản tin × 10 KB = 3.012 MB
pubsub_channels                            ->  0
client_output_buffer_limit_disconnections  ->  1

Redis đóng kết nối của thuê bao đó.

Giới hạn mặc định cho khách Pub/Sub:

client-output-buffer-limit pubsub 33554432 8388608 60

Cứng 32 MB, mềm 8 MB kéo dài 60 giây. Vượt cái nào cũng bị ngắt.

Đây là hành vi tự bảo vệ đúng đắn — không có nó, một thuê bao chậm sẽ làm Redis phình bộ nhớ vô hạn. Nhưng hệ quả với ứng dụng thì nghiêm trọng: thuê bao bị ngắt im lặng và mất mọi bản tin cho tới khi nó phát hiện và nối lại.

Và nó xảy ra đúng lúc tệ nhất: khi hệ thống đang tải cao, khi thuê bao đang xử lý chậm, khi lượng bản tin lớn nhất.

Mọi mã Pub/Sub trong production cần: đọc trong một luồng riêng không bao giờ chặn, xử lý ở luồng khác, và tự động nối lại khi mất kết nối.

Nhắc lại từ phần 12: khách thường không có giới hạn bộ đệm (normal 0 0 0), nhưng khách Pub/Sub thì . Hai loại khách, hai chính sách khác nhau.

PUBLISH chậm dần theo số thuê bao

 0 thuê bao   21.955 PUBLISH/s
 1 thuê bao   22.687 PUBLISH/s
10 thuê bao    3.766 PUBLISH/s     chậm 6 lần

Mỗi bản tin phải được ghi vào bộ đệm của từng thuê bao, và việc đó chạy trên luồng duy nhất.

Tổng số bản tin giao vẫn tăng — 10 thuê bao × 3.766 = 37.657 bản tin mỗi giây — nhưng người gửi thì chậm hẳn lại, và trong lúc đó không lệnh nào khác được phục vụ.

Điều đáng chú ý: từ 0 lên 1 thuê bao gần như miễn phí, nhưng từ 1 lên 10 thì mất 6 lần. Chi phí không nằm ở việc "có phát tán hay không" mà ở số lần ghi.

Với kênh có hàng trăm thuê bao — ví dụ mỗi kết nối WebSocket là một thuê bao — PUBLISH trở thành một lệnh nặng. Đó là lúc nên gom: một tiến trình trung gian đăng ký một lần rồi tự phân phát cho các kết nối phía dưới.

Khi nào Pub/Sub là công cụ đúng

Nó tốt cho tín hiệu, những thứ mất một cái cũng không sao vì cái sau sẽ tới:

  • Bảo mọi instance làm mới bộ đệm cấu hình.
  • Gỡ một mục khỏi bộ đệm cục bộ trên mọi máy.
  • Thông báo trạng thái thời gian thực mà bản mới nhất thay thế bản cũ.
  • Chỉ số và nhật ký để hiển thị, không để tính toán.

Điểm chung: nếu bỏ lỡ một bản tin, hệ thống vẫn đúng — muộn nhất là tới lần sau.

không tốt cho: xử lý đơn hàng, gửi email, bất cứ việc gì chạy đúng một lần.

Thông báo không gian khoá

Redis có một nguồn Pub/Sub sẵn: sự kiện thay đổi khoá.

redis-cli config set notify-keyspace-events KEA
redis-cli psubscribe '__keyevent@0__:expired'

Rất tiện để phản ứng khi khoá hết hạn. Nhưng nó thừa hưởng mọi hạn chế ở trên: ứng dụng không chạy lúc khoá hết hạn thì sự kiện đó mất vĩnh viễn.

Và nó còn một hạn chế riêng: sự kiện expired chỉ phát khi Redis thực sự xoá khoá — như đo ở phần 8, chuyện đó có thể xảy ra chậm hơn thời điểm hết hạn về mặt logic. Đừng dùng nó cho việc cần đúng thời điểm.

SSUBSCRIBE cho Cluster

Redis 7 thêm Pub/Sub theo phân mảnh: SPUBLISHSSUBSCRIBE. Bản tin chỉ đi tới nút giữ phân mảnh của kênh đó, thay vì phát tới mọi nút như Pub/Sub thường.

Trên cụm lớn, đó là khác biệt giữa một lần ghi và N lần ghi. Nếu bạn dùng Cluster và Pub/Sub cùng lúc, đây là thứ đáng chuyển sang — sê-ri sẽ quay lại ở phần về Cluster.

Thử ba mươi giây

Xem Pub/Sub trên hệ thống của bạn có đang mất tin không:

redis-cli info stats | grep -E 'pubsub|client_output_buffer_limit_disconnections'
redis-cli pubsub channels
redis-cli pubsub numsub $(redis-cli pubsub channels | tr '\n' ' ')

Hai dấu hiệu:

  • client_output_buffer_limit_disconnections lớn hơn 0 — đã có thuê bao bị ngắt vì đọc chậm, và mỗi lần ngắt là một khoảng mất tin.
  • Kênh có numsub bằng 0 mà vẫn có ai đó PUBLISH vào — mọi bản tin gửi tới kênh đó đang rơi vào hư không.

Phần sau: nhân bản — đo độ trễ giữa bản chính và bản sao, và cái gì mất khi chuyển đổi.