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/SubTham 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:
- Server gửi RPC status
UNAVAILABLEhoặc đóng stream - Client library nhận được error/EOF
- 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:
- Khi subscriber nhận message qua StreamingPull, library bắt đầu một lease timer cho message đó
- Trước khi lease timer hết, library tự động gọi
ModifyAckDeadlineđể extend - Khi subscriber gọi
ack(), library gửi ack và hủy timer - 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ểm | Unary Pull | StreamingPull |
|---|---|---|
| Transport | HTTP/gRPC unary | gRPC bidirectional stream |
| Message delivery | Request-initiated | Server-pushed |
| Latency | Cao hơn (round-trip) | Thấp hơn (server-push) |
| Throughput max | ~100 MB/s với nhiều concurrent requests | Cao hơn với ít connections |
| Resource control | Chính xác (pull N messages) | Gián tiếp (flow control) |
| Lease management | Manual | Tự động trong client library |
| Reconnect handling | Manual | Tự động |
| Connection state | Stateless | Stateful (per stream) |
| Use case | Batch, proxy, resource-constrained | Low-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.