跳至主要內容
ESC
ACE 服務實戰 — 第 4/11 篇

ACE-209:Cloud Pub/Sub 與事件驅動架構——解耦微服務的核心技術

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 個

訂閱類型

Pub/Sub 建立訂閱項目的設定畫面,傳送類型下拉可選提取、推送、寫入 BigQuery、寫入 Cloud Storage,下方還有重試政策、死信主題、僅傳送一次、訊息排序、確認期限 10 秒等選項
這是建立訂閱項目的完整表單,本節四種訂閱其實都藏在最上面那個「傳送類型」下拉裡——提取(Pull)、推送(Push)、寫入 BigQuery、寫入 Cloud Storage 挑一個而已。往下還有確認期限(預設 10 秒,最長能拉到 600 秒)、死信主題、訊息排序。最關鍵的考點在這裡:「僅傳送一次(exactly-once)」這個開關只有選「提取」時才點得到,換成另外三種它就消失了——這正是為什麼 exactly-once 只支援 Pull 訂閱。

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

死信處理流程

  1. 訊息交付失敗(消費者 nack 或超時)
  2. Pub/Sub 重試(依退避策略)
  3. 達到最大嘗試次數(max-delivery-attempts)
  4. 訊息轉移到死信 Topic
  5. 另一個消費者(或告警)處理死信

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/SubCloud TasksEventarc
主要用途非同步訊息傳遞任務排程與重試GCP 事件路由
目標多個 Subscriber單一 WorkerCloud 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_messagesBacklog 訊息數> 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 與安全

GCP IAM 授予存取權的角色選單,左側為基本角色 Owner/Editor/Viewer,右側為依產品分類的預先定義角色,示範最小權限的角色選取
下面那幾條 gcloud 指令裡的 --role=roles/pubsub.publisher、roles/pubsub.subscriber,在 Console 就是從這個角色選單挑出來的。左邊那組是「基本角色」(Owner/Editor/Viewer),右邊才是依產品切細的「預先定義角色」。ACE 幾乎每次都在考最小權限:發布者只給 pubsub.publisher、訂閱者只給 pubsub.subscriber,別圖方便丟一個 Editor 下去——真的被入侵時,攻擊者能碰的範圍差很多。
# 給發布者 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

必背知識點

  1. Pub/Sub Lite 已於 2026/3/18 正式關閉,考試選 Cloud Pub/Sub
  2. 一個 Topic 多個 Subscription = Fan-out;一個 Subscription 多個消費者 = 競爭消費
  3. 最大訊息大小 10MB,保留 7 天
  4. Ordering Key 保證相同 key 的訊息按序交付
  5. Dead Letter Topic 需要給 Pub/Sub SA 發布權限
  6. Exactly-once delivery 是 GA 功能,但只支援 Pull Subscription
  7. BigQuery SubscriptionCloud Storage Subscription 都是 GA 功能——但它們是 export 類型,只有 at-least-once,沒有 exactly-once,別誤以為直寫 BQ 就保證不重複

選型題公式

場景答案
多個服務同時收到事件Cloud Pub/Sub(fan-out)
1 小時後執行任務Cloud Tasks(延遲執行)
GCS 上傳觸發 Cloud RunEventarc
限制 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 入門,學習如何零運維部署容器化應用程式。

ACE 服務實戰 — 4/11 完成 查看系列全覽 →

留言討論

徽章解鎖!