ACE-209:Cloud Pub/Sub 與事件驅動架構——解耦微服務的核心技術
前言
系統從單體架構走向微服務,最頭痛的往往不是技術選型,而是怎麼讓服務之間不互相綁死。Cloud Pub/Sub 就是來解這個問題的:訂單服務按下「送出」之後,不用知道有沒有人在聽、聽的人是誰、聽的人掛了沒——這就是 Pub/Sub 在幫你做的解耦,生產者和消費者各自獨立擴展,彼此根本不用知道對方存在。
這篇是 ACE 進階系列第 9 課。我們會從 Pub/Sub 的核心概念開始,搞懂事件驅動架構有哪些設計模式,再學會在實際場景裡挑對工具。
Pub/Sub 核心概念
基本架構
Publisher (發布者)
│
▼
┌──────────┐
│ Topic │ ← 訊息的頻道/主題
└────┬─────┘
│ 一個 Topic 可有多個 Subscription
├──────────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Subscription│ │ Subscription│
│ A │ │ B │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
Subscriber A Subscriber B
三個核心元素:
| 元素 | 說明 | 類比 |
|---|---|---|
| Topic | 訊息頻道,發布者往這裡送訊息 | YouTube 頻道 |
| Subscription | 訂閱,消費者從這裡取訊息 | YouTube 訂閱 |
| Message | 訊息本體,含 data(bytes)和 attributes(鍵值對) | 影片 |
訊息規格限制
| 限制 | 值 |
|---|---|
| 最大訊息大小 | 10 MB |
| 訊息保留期限 | 7 天(預設) |
| 最長確認期限(ack deadline) | 600 秒 |
| 每個 Topic 的 Subscription 上限 | 10,000 個 |
訂閱類型
Pull Subscription(拉取訂閱)
消費者主動向 Pub/Sub 拉取訊息:
# 建立 pull subscription
gcloud pubsub subscriptions create my-sub \
--topic=my-topic \
--ack-deadline=60
# 拉取訊息
gcloud pubsub subscriptions pull my-sub --limit=10
# 確認訊息(ack)
gcloud pubsub subscriptions ack my-sub \
--ack-ids=ACK_ID_1,ACK_ID_2
消費者躲在防火牆後面、處理速度忽快忽慢(批次跑),或者你就是想自己控制吃訊息的速度,那就用 Pull。
運作上你要記得一件事:消費者拉到訊息後,必須在 ack deadline 內回報「我處理完了」(ack);要是超時還沒 ack,Pub/Sub 會把這則訊息再投一次。這就是所謂的 at-least-once(至少送到一次,可能重複)——所以你的處理邏輯要能被重跑也不出錯,同一則訊息算第二次、第三次進來,結果都一樣,這個性質叫冪等(idempotent)。
如果真的不能容忍重複,Pull 訂閱可以開 Exactly-once delivery(GA),確保訊息只被處理一次,但會略微增加延遲。
Push Subscription(推送訂閱)
Pub/Sub 主動將訊息 POST 到指定 HTTPS Endpoint:
# 建立 push subscription
gcloud pubsub subscriptions create my-push-sub \
--topic=my-topic \
--push-endpoint=https://my-service.run.app/pubsub/push \
--push-auth-service-account=sa@project.iam.gserviceaccount.com \
--ack-deadline=60
訊息格式(POST body):
{
"message": {
"data": "SGVsbG8gV29ybGQ=", // base64 encoded
"attributes": { "key": "value" },
"messageId": "2070443601311540",
"publishTime": "2026-03-11T10:00:00Z"
},
"subscription": "projects/my-project/subscriptions/my-push-sub"
}
消費者回傳 2xx 狀態碼 表示成功 ack;回傳其他狀態碼則訊息重試。
如果你的消費端是 Cloud Run、App Engine 這種無伺服器服務,懶得自己維護一個 pull loop,事件來了就觸發處理,那 Push 最省事——讓 Pub/Sub 主動把訊息 POST 給你就好。
BigQuery Subscription(直接寫入 BigQuery)
無需中間消費者,直接將訊息寫入 BigQuery 表格:
gcloud pubsub subscriptions create my-bq-sub \
--topic=my-topic \
--bigquery-table=my-project:my-dataset.my-table \
--write-metadata # 同時寫入 publish_time、subscription_name 等元數據
要求:
- BigQuery 表格 Schema 必須包含
data欄位(BYTES 或 STRING 型別) - Pub/Sub service account 需要 BigQuery Data Editor 角色
適合場景:
- 串流分析 pipeline
- 直接從事件建立分析報表
- 不需要業務邏輯處理
一個容易踩的雷:BigQuery 訂閱(還有下面的 Storage 訂閱)屬於 export 類型,只支援 at-least-once,不支援 exactly-once。也就是說同一筆事件有可能在 BigQuery 裡出現第二次。如果你需要「寫進 BQ 且保證不重複」,得改走 Dataflow,而不是直寫訂閱。
Cloud Storage Subscription(直接寫入 GCS)
將訊息批次寫入 Cloud Storage:
gcloud pubsub subscriptions create my-gcs-sub \
--topic=my-topic \
--cloud-storage-bucket=my-bucket \
--cloud-storage-file-prefix=pubsub/ \
--cloud-storage-max-duration=600s \
--cloud-storage-max-bytes=10MB
適合場景:
- 長期存檔
- 後續批次處理(Dataproc、Cloud Batch)
- Data Lake 攝取
進階特性
Ordering Keys(排序鍵)
保證相同 key 的訊息按發布順序交付:
from google.cloud import pubsub_v1
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path("my-project", "my-topic")
# 使用 ordering key,相同 user_id 的訊息按順序處理
future = publisher.publish(
topic_path,
data=b"Order placed",
ordering_key="user-123" # 相同 key 保序交付
)
注意事項:
- Subscription 也必須啟用 ordering(
--enable-message-ordering) - 若某訊息處理失敗,相同 key 的後續訊息會暫停,直到失敗的訊息被 nack 或超時
- 可能影響吞吐量(相同 key 的訊息只會分配到單一消費者)
Dead Letter Topic(死信主題)
當訊息多次交付失敗後,轉移到死信 Topic:
# 建立死信 topic
gcloud pubsub topics create dead-letter-topic
# 建立 subscription 並配置死信策略
gcloud pubsub subscriptions create my-sub \
--topic=my-topic \
--dead-letter-topic=dead-letter-topic \
--max-delivery-attempts=5 # 失敗 5 次後轉移
# 給 Pub/Sub service account 必要權限
PROJECT_NUM=$(gcloud projects describe my-project --format='value(projectNumber)')
PUBSUB_SA="service-${PROJECT_NUM}@gcp-sa-pubsub.iam.gserviceaccount.com"
gcloud pubsub topics add-iam-policy-binding dead-letter-topic \
--member="serviceAccount:${PUBSUB_SA}" \
--role=roles/pubsub.publisher
gcloud pubsub subscriptions add-iam-policy-binding my-sub \
--member="serviceAccount:${PUBSUB_SA}" \
--role=roles/pubsub.subscriber
死信處理流程:
- 訊息交付失敗(消費者 nack 或超時)
- Pub/Sub 重試(依退避策略)
- 達到最大嘗試次數(max-delivery-attempts)
- 訊息轉移到死信 Topic
- 另一個消費者(或告警)處理死信
Exactly-Once Delivery(精確一次交付)
# 啟用精確一次交付(GA 功能)
gcloud pubsub subscriptions create my-sub \
--topic=my-topic \
--enable-exactly-once-delivery
代價:
- 更高延遲(需要額外確認機制)
- 較低吞吐量
- 只在 Pull Subscription 支援
建議:大多數情況用 at-least-once 搭配消費者端的冪等設計就夠了,真的非用不可時再開 exactly-once。
訊息過濾(Message Filtering)
# 只接收特定 attribute 的訊息
gcloud pubsub subscriptions create filtered-sub \
--topic=my-topic \
--message-filter='attributes.region = "us-central1"'
事件驅動架構設計模式
模式一:Fan-Out(廣播)
一個事件觸發多個下游服務:
Order Placed Event (Topic)
│
├──► Email Service (Subscription A)
├──► Inventory Service (Subscription B)
├──► Analytics Service (Subscription C)
└──► Fraud Detection (Subscription D)
實現:每個下游服務建立獨立的 Subscription,獨立接收同樣的訊息。
模式二:Task Queue(任務佇列)
多個 Worker 競爭消費訊息:
Image Upload Events (Topic)
│
└──► processing-sub (Subscription)
│
├──► Worker 1 (Cloud Run 實例)
├──► Worker 2 (Cloud Run 實例)
└──► Worker 3 (Cloud Run 實例)
特性:一個 Subscription 中的訊息只會被一個消費者處理(競爭消費)。
模式三:事件溯源(Event Sourcing)
所有狀態變更都以事件形式記錄:
User Service → account-events (Topic)
│
├──► account-events-bigquery-sub → BigQuery (分析)
├──► account-events-storage-sub → GCS (存檔)
└──► account-events-sync-sub → 其他服務同步
模式四:CQRS(命令查詢職責分離)
寫入操作發布事件,讀取模型訂閱事件更新:
Write Model (Command) → domain-events (Topic) → Read Model (Query)
│
Subscription 更新快取/搜尋索引
Cloud Pub/Sub vs Cloud Tasks vs Eventarc
考試最愛在這三個之間挖坑——題目關鍵字一換,答案就跟著變。先把它們的分工看清楚:
| 特性 | Cloud Pub/Sub | Cloud Tasks | Eventarc |
|---|---|---|---|
| 主要用途 | 非同步訊息傳遞 | 任務排程與重試 | GCP 事件路由 |
| 目標 | 多個 Subscriber | 單一 Worker | Cloud Run/Functions |
| 排程執行 | ❌ | ✅(指定時間執行) | ❌ |
| 明確任務 | ❌ | ✅(可查詢、刪除任務) | ❌ |
| 保序交付 | ✅(ordering key) | ❌ | ❌ |
| Fan-out | ✅ | ❌ | 有限 |
| 觸發來源 | 任何應用 | 任何應用 | GCP 服務事件 |
| 最大延遲 | 無 | 30 天 | 近乎即時 |
選型決策:
用 Cloud Pub/Sub 當:
- 需要一對多廣播(fan-out)
- 需要高吞吐量的訊息串流
- 生產者和消費者需要解耦
- 訊息可能有多個獨立消費者
用 Cloud Tasks 當:
- 有單一目標(一個 Worker / 一個 HTTP 端點),而不是廣播給很多人
- 需要延遲執行或排程(e.g., 1 小時後發送 email)
- 需要可查詢、可刪除的任務列表
- 需要限制 Worker 的 QPS(速率控制)
Cloud Tasks 不是 exactly-once。官方明講它是 at-least-once:99.999% 以上只會跑一次,但仍有可能重複投遞,所以 handler 一樣要寫成冪等。別被「任務佇列」這個名字騙了。
用 Eventarc 當:
- 響應 GCP 服務事件(GCS 上傳、BigQuery 完成、Pub/Sub 訊息)
- 統一管理 GCP 事件路由到 Cloud Run
- CloudEvent 標準格式整合
⚠️ 注意:Cloud Pub/Sub Lite 已於 2026/3/18 正式關閉(依 Google Cloud release notes)。重點是題目裡再看到 Pub/Sub Lite,直接刪掉,選 Cloud Pub/Sub。
Cloud Scheduler:定時觸發的補充
先把它的定位講清楚:Cloud Scheduler 不是訊息系統,而是一個按時間戳觸發前面三者的鬧鐘——時間到了,它去敲 Pub/Sub Topic 或 HTTP 端點。所以它跟 Pub/Sub / Cloud Tasks 不是競爭關係,而是經常搭配使用:
# 每天早上 9 點觸發 Pub/Sub Topic
gcloud scheduler jobs create pubsub daily-report \
--schedule="0 9 * * *" \
--topic=report-trigger \
--message-body='{"type": "daily"}'
# 每 5 分鐘呼叫 Cloud Run 端點
gcloud scheduler jobs create http health-check \
--schedule="*/5 * * * *" \
--uri=https://my-service-xxx.run.app/cron/health \
--http-method=POST \
--oidc-service-account-email=scheduler-sa@my-project.iam.gserviceaccount.com
| 場景 | 選擇 |
|---|---|
| 定時觸發(cron-like) | Cloud Scheduler → Pub/Sub 或 HTTP |
| 延遲執行(X 分鐘後) | Cloud Tasks(指定 scheduleTime) |
| 即時非同步廣播 | Cloud Pub/Sub |
📝 考場提點
Pub/Sub vs Cloud Tasks vs Eventarc 是 ACE 的高頻選型題,判斷全看題目埋了哪個關鍵字:
- 「fan-out」「多個服務都要收到同一個事件」「多訂閱者」→ Cloud Pub/Sub
- 「延遲 X 分鐘後執行」「限制 Worker 的 QPS / 速率」「指定 scheduleTime」「單一目標端點」→ Cloud Tasks
- 「GCS 上傳檔案後觸發 Cloud Run」「某個 GCP 服務發生事件就跑」「CloudEvent」→ Eventarc
最反直覺的陷阱:Cloud Tasks 不是 exactly-once,是 at-least-once。名字叫「任務佇列」很容易讓人以為它保證只跑一次,但官方只承諾 99.999% 以上不重複,handler 還是得寫成冪等。看到題目把「保證只執行一次」綁到 Cloud Tasks,那是錯的選項。
實戰範例:電商訂單事件系統
底下是一個完整的電商訂單事件驅動系統,從頭到尾跑一遍:
# 1. 建立訂單事件 Topic
gcloud pubsub topics create order-events
# 2. 建立訂單死信 Topic
gcloud pubsub topics create order-events-dlq
# 3. 訂單通知服務(Push 訂閱 → Cloud Run)
gcloud pubsub subscriptions create order-notification-sub \
--topic=order-events \
--push-endpoint=https://notification-service.run.app/orders \
--push-auth-service-account=notification-sa@project.iam.gserviceaccount.com \
--ack-deadline=60 \
--dead-letter-topic=order-events-dlq \
--max-delivery-attempts=5
# 4. 庫存服務(Pull 訂閱,批次處理)
gcloud pubsub subscriptions create order-inventory-sub \
--topic=order-events \
--ack-deadline=120 \
--max-delivery-attempts=5 \
--dead-letter-topic=order-events-dlq \
--enable-message-ordering # 保序處理
# 5. 分析服務(BigQuery 直接訂閱)
gcloud pubsub subscriptions create order-analytics-sub \
--topic=order-events \
--bigquery-table=my-project:analytics.orders \
--write-metadata
# 6. 發布訂單事件(Python)
import json
import base64
from google.cloud import pubsub_v1
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path("my-project", "order-events")
order = {
"order_id": "ORD-2026031100001",
"user_id": "USER-123",
"items": [{"sku": "SKU-001", "qty": 2}],
"total": 1299.00
}
# 發布訊息(使用 ordering key 保序)
future = publisher.publish(
topic_path,
data=json.dumps(order).encode("utf-8"),
ordering_key=f"user-{order['user_id']}", # 同用戶訂單保序
event_type="ORDER_PLACED",
version="v1"
)
message_id = future.result()
print(f"Published message ID: {message_id}")
Pub/Sub 監控與維運
關鍵指標
# 查看 subscription 的未確認訊息數(backlog)
gcloud monitoring metrics list \
--filter="metric.type:pubsub.googleapis.com/subscription/num_undelivered_messages"
| 指標 | 說明 | 告警建議 |
|---|---|---|
num_undelivered_messages | Backlog 訊息數 | > 1000 時告警 |
oldest_unacked_message_age | 最舊未確認訊息年齡 | > 300s 時告警 |
delivery_attempts_count | 交付嘗試次數 | Dead letter 增加時告警 |
調整 Subscription 配置
# 修改 ack deadline
gcloud pubsub subscriptions update my-sub \
--ack-deadline=120
# 啟用 exactly-once delivery(慎用)
gcloud pubsub subscriptions update my-sub \
--enable-exactly-once-delivery
# 查看 subscription 詳細資訊
gcloud pubsub subscriptions describe my-sub
IAM 與安全
# 給發布者 Topic 發布權限
gcloud pubsub topics add-iam-policy-binding my-topic \
--member="serviceAccount:publisher-sa@project.iam.gserviceaccount.com" \
--role=roles/pubsub.publisher
# 給訂閱者 Subscription 訂閱權限
gcloud pubsub subscriptions add-iam-policy-binding my-sub \
--member="serviceAccount:subscriber-sa@project.iam.gserviceaccount.com" \
--role=roles/pubsub.subscriber
# Push Subscription 的 Service Account 需要 Token Creator
gcloud iam service-accounts add-iam-policy-binding \
push-endpoint-sa@project.iam.gserviceaccount.com \
--member="serviceAccount:push-invoker-sa@project.iam.gserviceaccount.com" \
--role=roles/iam.serviceAccountTokenCreator
ACE 考試重點整理
📝 考場提點
一題最常見的觀念陷阱,先把「只給一個人」跟「廣播給所有人」分清楚:
- 同一個 Subscription 掛多個 consumer = 競爭消費,一則訊息只會被其中一個處理(任務分流就靠這個)
- 同一個 Topic 掛多個 Subscription = fan-out,每個 Subscription 都會收到完整一份(廣播就靠這個)
順手把幾個最會考、又容易混的點收進同一個框:
- exactly-once 只支援 Pull 訂閱;Push 與 BigQuery / Storage 這些 export 訂閱都只有 at-least-once,所以處理邏輯一律要冪等
- Pub/Sub Lite 已於 2026/3/18 正式關閉;題目出現 Lite 直接刪,選 Cloud Pub/Sub
- 必背數字:最大訊息 10 MB、預設保留 7 天、ack deadline 最長 600 秒、每個 Topic 上限 10,000 個 Subscription
必背知識點
- Pub/Sub Lite 已於 2026/3/18 正式關閉,考試選 Cloud Pub/Sub
- 一個 Topic 多個 Subscription = Fan-out;一個 Subscription 多個消費者 = 競爭消費
- 最大訊息大小 10MB,保留 7 天
- Ordering Key 保證相同 key 的訊息按序交付
- Dead Letter Topic 需要給 Pub/Sub SA 發布權限
- Exactly-once delivery 是 GA 功能,但只支援 Pull Subscription
- BigQuery Subscription 和 Cloud Storage Subscription 都是 GA 功能——但它們是 export 類型,只有 at-least-once,沒有 exactly-once,別誤以為直寫 BQ 就保證不重複
選型題公式
| 場景 | 答案 |
|---|---|
| 多個服務同時收到事件 | Cloud Pub/Sub(fan-out) |
| 1 小時後執行任務 | Cloud Tasks(延遲執行) |
| GCS 上傳觸發 Cloud Run | Eventarc |
| 限制 Worker 處理速率 | Cloud Tasks(rate limiting) |
| 高吞吐量訊息串流 | Cloud Pub/Sub |
| 單一目標、可排程/延遲的任務 | Cloud Tasks(at-least-once,handler 要冪等) |
常見陷阱題
Q:如何讓訊息只被一個消費者處理? A:多個消費者訂閱同一個 Subscription(競爭消費)。如果訂閱不同 Subscription,每個 Subscription 都會收到同一份訊息。
Q:Pub/Sub 能保證訊息順序嗎?
A:預設不保證。需要使用 Ordering Key 並在 Subscription 啟用 enable-message-ordering,才能保證相同 key 的訊息按序交付。
Q:訊息未被 ack 會怎樣? A:超過 ack deadline 後,Pub/Sub 會重新投遞該訊息(at-least-once 語義)。要避免重複處理,消費者應設計為冪等(idempotent)。
Q:如何處理持續處理失敗的訊息? A:設定 Dead Letter Topic,指定最大重試次數(max-delivery-attempts),失敗訊息自動轉移,避免阻塞正常訊息流。
總結
要做事件驅動架構,Cloud Pub/Sub 幾乎是繞不開的一塊,本課的解耦設計、四種訂閱類型、Ordering Key 與 Dead Letter,都是日後實作會反覆用到的底子。
如果整課只能帶走兩句話進考場,記這兩個最會出陷阱的:Pub/Sub Lite 已於 2026/3/18 正式關閉,題目看到它一律直接刪;還有 exactly-once 只有 Pull 訂閱支援,Push、BigQuery、Storage 這些 export 類型都只有 at-least-once,所以不管哪一種,handler 都要寫成冪等。其餘細節我整理在下面的考場提點,掃一遍就好。
下一課 GCP-109:Cloud Run 入門,學習如何零運維部署容器化應用程式。