Skip to content

Pull Subscriptions — Unary Pull & StreamingPull

Pull subscription là mô hình mà subscriber chủ động yêu cầu message từ Pub/Sub. Có hai API hoàn toàn khác nhau về cơ chế: Unary Pull (request-response đơn giản) và StreamingPull (persistent gRPC stream). Hầu hết developer nghĩ chỉ có một "pull API" — nhưng high-level client library thực sự dùng StreamingPull bên dưới, và hiểu sự khác biệt này giải thích nhiều behavior bất ngờ trong production.

Unary Pull — request-response model

Cơ chế hoạt động

Unary Pull là API truyền thống: subscriber gửi một HTTP/gRPC request, nhận lại một response chứa messages, sau đó kết nối đóng. Đây là interaction synchronous và stateless từ góc độ transport layer.

Subscriber → PullRequest(max_messages=N) → Pub/Sub
Pub/Sub    → PullResponse(messages=[...]) → Subscriber
[Kết nối đóng]
Subscriber → AcknowledgeRequest(ack_ids=[...]) → Pub/Sub

Tham số chính: max_messages — số message tối đa trong một response. Giới hạn là 1000 messages per request. Pub/Sub trả về tối đa số này, có thể ít hơn nếu backlog không đủ.

Quan trọng: Nếu backlog hiện tại trống, Unary Pull có thể trả về empty response ngay lập tức — không block đợi message đến. Đây là khác biệt quan trọng so với SQS long polling hay StreamingPull.

Long polling với Unary Pull

Để không phải poll liên tục khi backlog trống, Pub/Sub support một dạng long polling: nếu returnImmediately=false (deprecated trong API mới, nhưng vẫn hoạt động), Pub/Sub giữ connection mở đến khi có message hoặc timeout (khoảng vài giây).

Trong thực tế, nếu dùng Unary Pull để đạt throughput cao, bạn cần nhiều concurrent pull requests. Theo docs, để đạt throughput tối đa, cần ít nhất 10 concurrent pull requests hoạt động song song.

Use case phù hợp

Unary Pull thích hợp khi:

  • Cần strict control over resource consumption: chỉ pull N messages, xử lý xong, pull tiếp
  • Hệ thống có resource constraints (memory, thread pool) cần biết chính xác số messages đang xử lý
  • Implement proxy layer giữa Pub/Sub và hệ thống pull-oriented khác
  • Batch processing: pull toàn bộ batch, xử lý offline, ack

Anti-pattern với Unary Pull: Pull-and-ack ngay lập tức mà không xử lý trước. Một số developer pull message rồi ack ngay để "tránh timeout" — nhưng nếu xử lý fail sau đó, message đã ack và mất đi. Đây là message loss trong production.

StreamingPull — persistent gRPC stream

Cơ chế hoạt động

StreamingPull là bidirectional gRPC streaming RPC. Một kết nối duy nhất duy trì trong thời gian dài, và Pub/Sub push messages qua kết nối đó mà không cần subscriber request từng batch:

[Subscriber mở gRPC stream]
Pub/Sub → Message 1 → Subscriber
Pub/Sub → Message 2 → Subscriber
Subscriber → Ack(msg1) → Pub/Sub
Pub/Sub → Message 3 → Subscriber
Subscriber → Ack(msg2) → Pub/Sub
... [kết nối kéo dài đến khi đóng hoặc server reset]

Đây là mô hình server-push: Pub/Sub không đợi subscriber request, nó chủ động gửi messages ngay khi có. Điều này giải thích tại sao StreamingPull có latency thấp hơn Unary Pull — không có round-trip overhead của "request → wait → response".

Multiplexing và concurrent streams

Một subscriber có thể mở nhiều concurrent StreamingPull streams tới cùng một subscription. Pub/Sub phân phối messages across streams — mỗi stream nhận một subset khác nhau của messages. Đây là cơ chế scale-out:

Subscriber Process A: StreamingPull stream 1 → nhận messages [1, 4, 7, ...]
Subscriber Process B: StreamingPull stream 2 → nhận messages [2, 5, 8, ...]
Subscriber Process C: StreamingPull stream 3 → nhận messages [3, 6, 9, ...]

Pub/Sub không guarantee distribution even — một stream có thể nhận nhiều hơn stream khác dựa trên acknowledgment speed. Đây là cơ chế competing consumers.

Connection lifecycle và server-side reset

Pub/Sub server định kỳ reset StreamingPull connections sau một khoảng thời gian (không được document chính xác, thường vài giờ). Lý do là để tránh "sticky connection" không healthy được giữ mãi mà không ai biết.

Khi server reset kết nối:

  1. Server gửi RPC status UNAVAILABLE hoặc đóng stream
  2. Client library nhận được error/EOF
  3. Client library tự động reconnect — không cần code subscriber handle

Đây là behavior quan trọng: nếu subscribe thấy log "stream was reset" hoặc "connection closed by server", đây là behavior bình thường, không phải lỗi. Client library xử lý transparently.

Ảnh hưởng tới throughput: Trong thời gian reconnect (thường dưới 1 giây), throughput của stream đó bằng 0. Nếu chỉ có một stream, đây là throughput dip ngắn. Nếu có nhiều streams, impact nhỏ hơn vì streams khác vẫn active.

Lease management trong StreamingPull

Với Unary Pull, subscriber tự biết mình đang giữ bao nhiêu messages và manage ack deadline thủ công. Với StreamingPull, high-level client library implement automatic lease management:

  1. Khi subscriber nhận message qua StreamingPull, library bắt đầu một lease timer cho message đó
  2. Trước khi lease timer hết, library tự động gọi ModifyAckDeadline để extend
  3. Khi subscriber gọi ack(), library gửi ack và hủy timer
  4. Nếu subscriber crash mà không ack, library không extend nữa → message sẽ bị redeliver sau khi deadline expire

Quan trọng: Lease management được thực hiện trên một background thread/goroutine trong client library. Nếu subscriber bận xử lý và không yield control, background thread có thể bị block và không extend ack deadline kịp. Đây là một nguyên nhân khá phổ biến của "unexpected redelivery" dù subscriber đang healthy.

Giải pháp: Xử lý message asynchronously — nhận message và dispatch vào thread pool / worker, không xử lý synchronously trên stream callback. Client library Java, Go, Python đều được thiết kế theo pattern này.

Ack batching và throughput optimization

Cả ACK và ModifyAckDeadline đều có thể được batched: thay vì gửi từng ACK riêng lẻ, client library gom nhiều ACKs vào một request. Điều này giảm số RPC calls và tăng throughput.

Xử lý message 1, 2, 3, 4, 5 trong 500ms
→ Gửi một batch ACK: ack_ids=[id1, id2, id3, id4, id5]

Default batching interval trong Java library là khoảng 100ms. Trong Python là tương tự. Bạn có thể giảm interval này nếu muốn ack nhanh hơn (giảm window của potential duplicate), hoặc tăng lên nếu muốn ít RPC calls hơn.

Unary Pull vs StreamingPull — bảng so sánh

Đặc điểmUnary PullStreamingPull
TransportHTTP/gRPC unarygRPC bidirectional stream
Message deliveryRequest-initiatedServer-pushed
LatencyCao hơn (round-trip)Thấp hơn (server-push)
Throughput max~100 MB/s với nhiều concurrent requestsCao hơn với ít connections
Resource controlChính xác (pull N messages)Gián tiếp (flow control)
Lease managementManualTự động trong client library
Reconnect handlingManualTự động
Connection stateStatelessStateful (per stream)
Use caseBatch, proxy, resource-constrainedLow-latency streaming, high-throughput

Competing consumers và parallelism

Pub/Sub hỗ trợ competing consumers pattern tự nhiên: nhiều subscriber instances cùng pull từ một subscription, mỗi instance nhận một subset messages. Đây là cách scale-out subscriber fleet.

Quan trọng là hiểu: subscription là unit of parallelism, không phải topic. Nếu bạn muốn nhiều independent consumers xử lý cùng một message (fan-out), cần nhiều subscriptions. Nếu bạn muốn nhiều workers cùng xử lý messages của một consumer group (load balancing), dùng một subscription và nhiều subscribers.

Topic: "orders"
  ├── Subscription "orders-processor" (1 subscription, 5 subscriber instances)
  │     → Messages phân phối giữa 5 instances
  └── Subscription "orders-analytics" (1 subscription, 1 subscriber)
        → Nhận tất cả messages (independent copy)

Monitoring pull subscription

Metrics quan trọng cho pull subscription:

  • subscription/num_undelivered_messages: số messages trong backlog chưa được deliver (hoặc đã deliver nhưng chưa ack)
  • subscription/oldest_unacked_message_age: tuổi của message unacked lâu nhất (signal của subscriber stuck)
  • subscription/pull_request_count: số pull requests per second (cho Unary Pull)
  • subscription/num_outstanding_messages: số messages đang được held bởi subscriber (delivered but not acked)

oldest_unacked_message_age tăng cao là signal nguy hiểm: subscriber đang bị stuck xử lý một message (hoặc nhiều messages) mà không ack. Cần investigate ngay để tránh redelivery cascade.

References