Skip to content

Ordering Keys — Per-Key Guarantee & Regional Scope

Message ordering là một trong những tính năng bị hiểu sai nhiều nhất của Pub/Sub. Pub/Sub không phải là Kafka với partition-based ordering. Pub/Sub dùng ordering keys để route messages có cùng key về cùng một delivery path — nhưng guarantee này đi kèm với những ràng buộc nghiêm ngặt mà nếu vi phạm, ordering guarantee biến mất hoàn toàn.

Tại sao ordering là một vấn đề phân tán

Trước khi đi vào cơ chế, cần hiểu tại sao ordering khó trong distributed messaging system.

Pub/Sub nhận messages từ nhiều publishers, lưu trên nhiều storage servers (sharded), và deliver qua nhiều delivery servers. Trong kiến trúc sharded, hai messages được publish liên tiếp có thể:

  • Đến ở các frontend servers khác nhau
  • Được lưu trên các storage servers khác nhau
  • Được deliver qua các delivery servers khác nhau

Không có global ordering tự nhiên trong kiến trúc này. Mặc định, subscriber nhận messages theo thứ tự "best-effort" — xấp xỉ publish order nhưng không guaranteed.

Ordering keys giải quyết bài toán này bằng cách đảm bảo tất cả messages với cùng key đi qua cùng một path: cùng storage shard, cùng delivery server. Đây là trade-off: đổi scalability của key đó lấy ordering guarantee.

Cơ chế per-key ordering

"Only one batch outstanding per key"

Đây là ràng buộc cốt lõi của ordering mechanism trong Pub/Sub. Theo documentation:

"Only one batch of messages can be outstanding for an ordering key at a time" (với pull subscriptions) "Only one outstanding message for each ordering key" (với push subscriptions)

Điều này có nghĩa: Pub/Sub không gửi batch message tiếp theo cho ordering key "K" cho đến khi subscriber ack tất cả messages trong batch hiện tại của key "K".

Tại sao cơ chế này đảm bảo ordering?

Nếu batch 1 (messages M1, M2, M3 với key K) đang được xử lý và subscriber ack M1, M2, M3 theo thứ tự, Pub/Sub biết chắc rằng sau khi tất cả ack được nhận, batch 2 (M4, M5) sẽ được deliver sau batch 1. Không có cách nào M4 đến trước M1 từ góc độ subscriber — vì M4 không được gửi cho đến khi M1, M2, M3 đã được ack.

Sequential delivery enforcement

Với pull subscription:

Publisher gửi: M1(key=K) → M2(key=K) → M3(key=K)

Pub/Sub gửi batch 1 [M1, M2, M3]:
  Subscriber nhận: M1, M2, M3 (theo thứ tự publish)
  Subscriber ack M1, M2, M3

Pub/Sub gửi batch 2 [M4, M5]:
  Subscriber nhận: M4, M5
  ...

Trong cùng batch, messages được ordered theo publish time. Nếu subscriber ack M2 trước M1, Pub/Sub không gửi batch tiếp theo cho đến khi M1 cũng được ack — vì batch tiếp theo phải chờ tất cả messages trong batch hiện tại được xử lý.

Ảnh hưởng tới ack deadline: Nếu một message trong batch bị stuck (xử lý lâu, hoặc NACK), toàn bộ key đó bị block. Pub/Sub sẽ redeliver message bị stuck và không gửi messages tiếp theo của key đó cho đến khi unblock.

Regional scope — tại sao bắt buộc

Ordering key mechanism đòi hỏi Pub/Sub biết thứ tự publish của messages. Thứ tự này được xác định tại thời điểm publish và lưu trong storage. Nhưng storage là regional — mỗi message được lưu trong region nơi nó được published.

Vấn đề cross-region publishing: Nếu publisher 1 ở us-central1 publish M1(key=K) và publisher 2 ở europe-west1 publish M2(key=K), không có cơ chế global để xác định thứ tự tuyệt đối giữa M1 và M2 (clock drift, network delay). Đây là lý do:

Pub/Sub yêu cầu: "All messages with the same ordering key must be published in the same region."

Nếu vi phạm điều này (publish cùng ordering key từ nhiều regions), ordering guarantee không áp dụng — bạn vẫn có thể nhận được messages nhưng thứ tự không được đảm bảo.

Single-region endpoint requirement cho publisher

Để đảm bảo tất cả messages với cùng ordering key đến cùng một region, publisher phải dùng regional endpoint thay vì global endpoint:

python
# KHÔNG dùng: có thể route tới bất kỳ region nào
publisher = pubsub_v1.PublisherClient()

# DÙNG: chỉ định regional endpoint
from google.api_core.gapic_v1.client_info import ClientInfo
publisher = pubsub_v1.PublisherClient(
    client_options={"api_endpoint": "us-central1-pubsub.googleapis.com:443"}
)

Nếu dùng global endpoint (pubsub.googleapis.com), DNS có thể resolve tới bất kỳ region nào — và các requests khác nhau có thể land ở regions khác nhau. Dù ít xảy ra trong thực tế (DNS thường sticky), đây là non-deterministic và không nên làm cho ordering workloads.

Throughput limits

Per-key throughput cap

Throughput limit per ordering key: 1 MB/s

Đây là hard limit về throughput cho một ordering key. Nếu bạn cần publish nhiều hơn 1 MB/s cho một key, bạn cần redesign — ví dụ: sharding key thành nhiều keys (key = customer-123-shard-0, customer-123-shard-1), nhưng khi đó ordering across shards không được đảm bảo.

Ordering + exactly-once throughput reduction

Khi enable cả ordering keys và exactly-once delivery, throughput giảm đáng kể:

"When you use ordering with exactly-once delivery, the client throughput is limited to an order of thousand messages per second."

Từ "order of thousand" (hàng nghìn) so với hàng triệu messages/s thông thường — đây là giảm nhiều bậc độ lớn. Lý do là hệ thống phải process ACKs sequentially per key (để verify exactly-once) maintain deduplication state — cả hai operations đều không thể parallel hóa per-key.

Hot key problem

Hot key là tình trạng một (hoặc vài) ordering keys nhận lượng messages không cân xứng so với các keys khác, tạo ra bottleneck ở key đó.

Tại sao hot key là vấn đề

Với ordering keys, tất cả messages của một key phải được process sequentially — không thể parallel. Nếu key "global-event" nhận 100,000 messages/s trong khi các keys khác mỗi cái chỉ nhận 100 messages/s:

  • "global-event" bị giới hạn bởi throughput 1 MB/s (≈ 100,000 messages × 10 bytes = 1 MB, ngay tại ngưỡng)
  • Subscriber phải xử lý tuần tự cho key này
  • Backlog cho key "global-event" tăng nhanh

Ví dụ thực tế: Dùng user ID làm ordering key — nếu có một "bot user" generate 1000 events/s trong khi average user generate 1 event/s, bot user key sẽ tạo hot key.

Phát hiện hot key

Không có Pub/Sub metric trực tiếp cho "hot key". Cách detect:

  • Monitor oldest_unacked_message_age per subscription: nếu tăng đột biến dù throughput tổng không đổi → hot key
  • Log processing time per ordering key tại subscriber side
  • Monitor publish throughput: nếu một key chiếm >80% tổng throughput, đó là hot key candidate

Giải pháp

  1. Key sharding: Thêm suffix random cho hot keys — customer-123-0, customer-123-1, ..., customer-123-N. Downstream phải merge nếu cần.
  2. Redesign: Xem lại liệu ordering thực sự cần thiết cho hot key. Đôi khi chỉ cần idempotency ở subscriber, không cần ordering.
  3. Separate subscription: Hot key có thể được route sang subscription riêng với subscriber dedicated.

Ordering key và redelivery

Khi một message với ordering key bị redeliver (vì ack deadline expire hoặc NACK), behavior quan trọng là: Pub/Sub suspend delivery cho ordering key đó cho đến khi subscriber acknowledge hoặc nack message đang bị stuck.

Batch 1: [M1, M2, M3] với key K
M1 delivered → acked
M2 delivered → ack timeout (subscriber chậm)
M2 redelivered → acked
M3 delivered → acked
Batch 2: [M4, M5] với key K → bây giờ mới được deliver

Trong thời gian M2 bị stuck và chờ redeliver, M3, M4, M5 tất cả phải đợi. Đây là "head-of-line blocking" trong ordering key context.

Hệ quả: Nếu một subscriber instance bị chậm xử lý một message của key K, toàn bộ pipeline cho key K bị chặn — dù các subscriber instances khác đang idle. Đây là trade-off tất yếu của ordering guarantee: để đảm bảo thứ tự, phải accept head-of-line blocking.

Khi nào thực sự cần ordering keys

Ordering keys nên được dùng khi thứ tự xử lý có ý nghĩa business thực sự, không phải vì "cẩn thận hơn":

Nên dùng:

  • Database CDC (Change Data Capture): INSERT → UPDATE → DELETE phải đúng thứ tự per row/entity
  • Financial transactions: các thao tác trên cùng account phải sequential
  • State machine events: transition states phải theo đúng sequence

Không nên dùng:

  • Telemetry/metrics: thứ tự không ảnh hưởng kết quả aggregate
  • Notification delivery: user không quan tâm notification nào đến trước
  • Analytics events: analytics systems tự handle out-of-order data

Chi phí của ordering key: Giảm throughput, tăng latency (head-of-line blocking), tăng complexity. Chỉ dùng khi ordering là hard requirement.

References