【系統設計 30 概念】#05 分散式系統與內容傳遞 — CAP 定理、CDN 與即時通訊

測驗:分散式系統與內容傳遞 — CAP 定理、CDN 與即時通訊

共 5 題,點選答案後會立即顯示結果

1. 在 CAP 定理中,為什麼 P(分區容錯)被認為是「必選」的?

  • A. 因為分區容錯的實作成本最低
  • B. 因為沒有分區容錯就無法做水平擴展
  • C. 因為在真實世界中網路分區一定會發生,這是無法避免的前提
  • D. 因為分區容錯可以同時保證一致性和可用性

2. 為什麼大檔案(如圖片、影片)不適合直接存進資料庫,而應該使用 Blob Storage?

  • A. 資料庫無法儲存二進位檔案格式
  • B. 大檔案塞進資料庫會讓資料庫效能嚴重下降,Blob Storage 是專門處理大檔案的服務
  • C. Blob Storage 比資料庫便宜,但速度較慢
  • D. 資料庫只能存文字資料,不能存檔案

3. CDN 第一次收到某個用戶的請求時,節點上還沒有快取。此時 CDN 會怎麼做?

  • A. 直接回傳 404 錯誤,告訴用戶稍後再試
  • B. 把用戶重新導向到原始 Server 的 URL
  • C. 從其他地區的 CDN 節點複製快取過來
  • D. 向原始 Server 拿資料,快取起來後再回給用戶

4. WebSocket 和 Webhook 最根本的差異是什麼?

  • A. WebSocket 用於前端,Webhook 用於後端
  • B. WebSocket 是 Client 和 Server 之間的持續雙向連線;Webhook 是 Server 對 Server 的事件驅動單向通知
  • C. WebSocket 使用 HTTP 協定,Webhook 使用自定義協定
  • D. WebSocket 只能傳文字,Webhook 可以傳任何資料

5. 一個社群媒體平台正在設計「按讚」功能。根據 CAP 定理,這個功能在分散式系統中應該偏向哪種設計?為什麼?

  • A. CP 系統 — 按讚數必須即時精確,不能有任何誤差
  • B. CA 系統 — 按讚功能不需要分區容錯
  • C. AP 系統 — 按讚數晚幾秒更新沒關係,重要的是用戶隨時都能操作
  • D. CAP 三者兼顧 — 按讚功能很簡單,不受 CAP 定理限制

**系列**:系統設計 30 概念(第 5 篇,共 6 篇)
**難度**:L2-進階
**前置知識**:第 1 篇(HTTP、Latency)、第 3 篇(水平擴展)、第 4 篇(Replication 一致性)
**來源**:System Design was HARD until I Learned these 30 Concepts — Ashish Pratap Singh


這篇在講什麼

前幾篇我們學了資料庫怎麼擴展、怎麼複製。但當系統分散在多台機器上,新問題來了:

  • 資料一致跟系統可用,你只能選一個?(CAP 定理)
  • 大檔案放哪裡?怎麼讓全球用戶快速取得?(Blob Storage + CDN)
  • 怎麼做到「即時」通訊,不用一直問 server 有沒有新消息?(WebSocket vs Webhook)

本篇涵蓋系統設計 30 概念的第 21-25 個。


21. CAP Theorem(CAP 定理)

一句話說明

分散式系統不可能同時滿足一致性、可用性、分區容錯 — 三選二,而且分區容錯是必選的。

三個字母分別是什麼

字母 名稱 白話翻譯
C Consistency(一致性) 不管你問哪台 server,拿到的資料都一樣
A Availability(可用性) 不管什麼時候問,都能拿到回應(不會 503)
P Partition Tolerance(分區容錯) 就算某些 server 之間斷線了,系統還能繼續運作

為什麼三個不能同時滿足

想像你有兩台 server(Server A 和 Server B),它們之間的網路斷了(這就是「分區」):

用戶 ──→ Server A ──✕──→ Server B
         有新資料         還是舊資料

這時候你有兩個選擇:

選擇 1:保一致性(CP) — Server A 說「我跟 Server B 同步不了,先不回應你,等網路恢復」

  • 結果:資料一致,但用戶暫時用不了(犧牲可用性)
  • 適合:銀行轉帳 — 寧可讓你等,也不能帳戶金額不一致

選擇 2:保可用性(AP) — Server A 說「我先用手上的資料回你,之後再跟 Server B 同步」

  • 結果:用戶能用,但可能看到舊資料(犧牲一致性)
  • 適合:社群媒體按讚數 — 晚幾秒更新沒關係,重要的是能刷動態

為什麼 P 是必選的

在真實世界中,網路一定會出問題 — 海底電纜被鯊魚咬、機房跳電、路由器當掉。所以 P(分區容錯)不是選項,是前提。真正的選擇是:

**CP 還是 AP?一致性優先,還是可用性優先?**

Vibe Coder 檢查點

看到系統設計討論時確認:

  • 這個功能對資料一致性的要求有多高?(金流 vs 按讚數)
  • 當部分系統掛了,應該拒絕服務還是回傳可能過時的資料?
  • AI 幫你設計的系統如果用了多台 server,有考慮資料同步的問題嗎?

22. Blob Storage(物件儲存)

一句話說明

專門存大檔案的雲端儲存 — 圖片、影片、PDF 往這裡丟。

為什麼需要 Blob Storage

資料庫(MySQL、PostgreSQL)適合存結構化資料:用戶名稱、訂單編號、交易記錄。但如果你要存:

  • 一張 5MB 的商品圖片
  • 一段 200MB 的教學影片
  • 一份 50MB 的 PDF 報告

硬塞進資料庫會讓它慢到爆炸。Blob Storage 就是專門幹這件事的服務。

它怎麼運作

你的應用程式
  │
  ├─ 結構化資料 ──→ 資料庫(PostgreSQL)
  │   用戶名稱、訂單...     ↑ 存 URL 指向 Blob
  │                          │
  └─ 大檔案 ──────→ Blob Storage(S3)
      圖片、影片...    回傳一個公開 URL

白話翻譯:資料庫裡存的是「這張圖片的網址」,圖片本身放在 Blob Storage。

代表服務

服務 雲端平台
S3(Simple Storage Service) AWS
Cloud Storage Google Cloud
Blob Storage Azure

AI 幫你寫的程式碼裡可能出現

import boto3

s3 = boto3.client('s3')

# 上傳檔案到 S3
s3.upload_file('photo.jpg', 'my-bucket', 'images/photo.jpg')
#               ↑ 本地檔案     ↑ 儲存桶名稱    ↑ 存在哪個路徑

# 產生一個臨時下載連結(1 小時後過期)
url = s3.generate_presigned_url('get_object',
    Params={'Bucket': 'my-bucket', 'Key': 'images/photo.jpg'},
    ExpiresIn=3600)
# ↑ 這個 URL 可以直接給用戶下載,不需要經過你的 server
Code language: PHP (php)

翻譯:把本地的 photo.jpg 上傳到雲端的 my-bucket 儲存桶,然後產生一個暫時的下載連結。

Vibe Coder 檢查點

看到 AI 的程式碼處理檔案上傳時確認:

  • 大檔案是存到 Blob Storage(S3 等),不是塞進資料庫?
  • 有沒有用 presigned URL?(不要讓所有檔案都公開可讀)
  • 儲存桶的權限設定合理嗎?(AI 有時會設成全公開)

23. CDN(Content Delivery Network,內容傳遞網路)

一句話說明

在全球各地放快取伺服器,讓用戶從最近的節點取內容,不用跨洋拿資料。

為什麼需要 CDN

假設你的 server 在美國,台灣的用戶要看一張圖片:

沒有 CDN:
台灣用戶 ──→ 跨太平洋 ──→ 美國 Server ──→ 跨太平洋回來
                                           延遲:200ms+

有 CDN:
台灣用戶 ──→ 台灣 CDN 節點(已快取)
                延遲:20ms

CDN 就是把你的靜態檔案(圖片、CSS、JS、影片)複製到全球各地的伺服器。用戶請求時,自動導到最近的節點。

它的工作流程

第一次請求:
用戶 ──→ CDN 節點(沒有快取)──→ 向原始 Server 拿 ──→ 快取起來 ──→ 回給用戶

第二次請求(同一地區的用戶):
用戶 ──→ CDN 節點(有快取了)──→ 直接回給用戶 ✓

CDN 適合什麼 vs 不適合什麼

適合放 CDN 不適合放 CDN
圖片、影片 個人化內容(你的購物車)
CSS、JavaScript 檔案 即時變動的資料(股價)
字型檔案 需要驗證身份的 API 回應
靜態 HTML 頁面 資料庫查詢結果

代表服務

服務 特色
Cloudflare 免費方案很大方,也提供安全防護
AWS CloudFront 跟 S3 無縫整合
Akamai 老牌 CDN,企業級

Blob Storage + CDN 的黃金組合

在實務中,這兩個常常一起用:

上傳流程:
用戶上傳圖片 ──→ 你的 Server ──→ 存到 S3(Blob Storage)

讀取流程:
用戶瀏覽圖片 ──→ CDN 節點 ──→(如果沒快取)──→ 從 S3 拿
                   ↓
              直接回給用戶(超快)

S3 負責存檔,CDN 負責快速分發。

Vibe Coder 檢查點

看到 AI 的系統架構時確認:

  • 靜態資源(圖片、JS、CSS)有沒有走 CDN?
  • CDN 的快取時間設多久?(太長會看到舊版,太短效果不好)
  • 更新檔案時有沒有做 cache busting?(例如檔名加 hash:app.3f7a.js

24. WebSockets

一句話說明

client 和 server 之間的持續雙向通訊通道 — 兩邊都能隨時發訊息。

跟一般 HTTP 有什麼不同

一般 HTTP(像寄信):
Client: 「有新消息嗎?」 ──→ Server: 「沒有」
Client: 「有新消息嗎?」 ──→ Server: 「沒有」
Client: 「有新消息嗎?」 ──→ Server: 「有!給你」
↑ 要一直問(polling),很浪費

WebSocket(像打電話):
Client ←──建立連線──→ Server
         (保持通話中)
Server: 「有新消息了!」 ──→ Client    ← server 主動推
Client: 「收到,我要回覆」 ──→ Server  ← client 也能隨時說

最小範例

前端建立 WebSocket 連線:

// 建立 WebSocket 連線(ws:// 是 WebSocket 協定)
const socket = new WebSocket('ws://example.com/chat');

// 連線成功時
socket.onopen = () => {
    socket.send('你好!');  // 發訊息給 server
};

// 收到 server 的訊息時
socket.onmessage = (event) => {
    console.log('收到:', event.data);
};

// 連線斷開時
socket.onclose = () => {
    console.log('斷線了');
};
Code language: JavaScript (javascript)

翻譯:連上 server 後,雙方隨時可以互傳訊息,不用每次都重新建立連線。

後端(用 Python FastAPI 為例):

from fastapi import WebSocket

@app.websocket("/chat")       # 這不是一般的 HTTP 路由,是 WebSocket 路由
async def chat(ws: WebSocket):
    await ws.accept()          # 接受連線
    while True:                # 持續監聽(不會結束)
        msg = await ws.receive_text()   # 等用戶說話
        await ws.send_text(f"你說了:{msg}")  # 回覆
Code language: PHP (php)

翻譯:server 接受連線後,進入一個無限迴圈,一直等著收訊息、回訊息。

什麼時候用 WebSocket

場景 為什麼需要
即時聊天(LINE、Slack) 訊息要即時送達
多人遊戲 玩家動作要即時同步
股票行情 價格一直在變,要即時更新
協作編輯(Google Docs) 每個人的打字都要即時顯示

代價

WebSocket 不是免費的午餐:

  • 每個連線都占用 server 資源(記憶體、檔案描述符)
  • 1 萬個用戶同時在線 = 1 萬條持續連線
  • 需要處理斷線重連的邏輯

Vibe Coder 檢查點

看到 AI 用 WebSocket 時確認:

  • 真的需要即時雙向通訊嗎?如果只是偶爾拿一次資料,用一般 HTTP 就好
  • 有沒有處理斷線重連?(網路不穩時 WebSocket 會斷)
  • ws:// 是不加密的,正式環境要用 wss://(就像 httphttps

25. Webhooks

一句話說明

「事情做完了叫我」 — server 主動通知另一個 server,不用一直輪詢。

跟 WebSocket 有什麼不同

很多人搞混這兩個,其實它們解決的問題完全不同:

WebSocket Webhook
方向 Client 和 Server 雙向 Server 對 Server 單向
連線 持續保持連線 沒有持續連線,事件觸發時發一個 HTTP 請求
適合 即時互動(聊天、遊戲) 事件通知(付款成功、程式碼推送)
白話 打電話(一直通著) 快遞到了打電話通知你(叫一次就掛)

實際場景:Stripe 付款通知

沒有 Webhook(你一直問):
你的 Server: 「付款成功了嗎?」 ──→ Stripe: 「還沒」
你的 Server: 「付款成功了嗎?」 ──→ Stripe: 「還沒」
你的 Server: 「付款成功了嗎?」 ──→ Stripe: 「成功了!」
↑ 每秒問一次,浪費資源(polling)

有 Webhook(Stripe 通知你):
你的 Server: (做自己的事...)
Stripe: 「付款成功了!」 ──→ POST https://你的server.com/webhook/stripe
↑ 事情發生時才通知,高效
Code language: JavaScript (javascript)

最小範例

你需要做的事 — 提供一個接收通知的 API:

from fastapi import Request

@app.post("/webhook/stripe")         # Stripe 會打這個網址
async def stripe_webhook(request: Request):
    payload = await request.json()    # 拿到 Stripe 送來的資料

    event_type = payload['type']      # 看是什麼事件

    if event_type == 'payment_intent.succeeded':
        order_id = payload['data']['object']['metadata']['order_id']
        await update_order_status(order_id, 'paid')   # 更新訂單狀態

    return {"status": "ok"}           # 回覆 Stripe:我收到了
Code language: PHP (php)

翻譯:你的 server 提供一個網址,Stripe 付款成功後會自動發一個 POST 請求到這個網址,你接收到後更新訂單狀態。

常見的 Webhook 使用場景

服務 什麼時候觸發 通知什麼
Stripe 付款成功/失敗 交易結果
GitHub 有人 push 程式碼 commit 資訊
Slack 有人在頻道發訊息 訊息內容
Shopify 有新訂單 訂單資料

Webhook 的安全注意事項

任何人都可以偽造一個 POST 請求打到你的 webhook URL,所以必須驗證來源:

import hmac
import hashlib

def verify_stripe_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
    # ↑ 用 Stripe 給你的 secret 驗證這個請求真的來自 Stripe
Code language: PHP (php)

翻譯:用一個只有你和 Stripe 知道的密鑰,驗算收到的資料有沒有被竄改。

Vibe Coder 檢查點

看到 AI 寫了 webhook 端點時確認:

  • 有沒有驗證請求來源?(不驗證的話任何人都能偽造通知)
  • 有沒有回覆正確的 HTTP 狀態碼?(通常回 200,否則對方會重試)
  • 有沒有處理「重複通知」的情況?(網路問題可能導致同一事件通知兩次)

五個概念的關係整理

這五個概念在一個真實系統中可能同時出現:

用戶上傳圖片
  │
  ├─ 圖片存到 Blob StorageS3)        ← #22
  │
  ├─ 透過 CDN 分發給全球用戶              ← #23
  │
  ├─ 多台 Server 之間的資料同步           ← #21 CAP 定理
  │   要選 CP 還是 AP?
  │
  ├─ 即時通知好友「上傳了新照片」          ← #24 WebSocket
  │   透過持續連線推送
  │
  └─ 觸發第三方圖片辨識服務               ← #25 Webhook
      辨識完成後回調通知你的 server
Code language: CSS (css)

本篇總結

概念 一句話記憶 關鍵詞
CAP 定理 分散式系統三選二,P 必選 一致性 vs 可用性
Blob Storage 大檔案倉庫,資料庫存 URL 就好 S3、儲存桶
CDN 全球快取,靜態資源從最近的節點拿 Cloudflare、延遲
WebSocket 持續雙向通話,即時互傳訊息 ws://、即時聊天
Webhook 事情做完打電話通知,不用一直問 POST 回調、事件驅動

必看懂 vs 知道就好

必看懂(面試和實務都常見):
- CAP 定理的三選二邏輯 — 會被問
- CDN 的運作原理 — 每個 Web 應用都在用
- WebSocket 和 Webhook 的差異 — 很容易搞混

知道就好(遇到再查):
- S3 的 ACL 權限細節
- CDN 的 cache invalidation 策略
- WebSocket 的心跳機制和自動重連實作

下一篇預告

第 6 篇(完結篇)會涵蓋概念 26-30:微服務架構、訊息佇列與常見的架構模式。當系統變大、團隊變多,怎麼把一個巨大的應用拆成可獨立部署的小服務?

進階測驗:分散式系統與內容傳遞 — CAP 定理、CDN 與即時通訊

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在設計一個線上銀行轉帳系統,部署在三個不同地區的資料中心。產品經理問你:「當其中一個資料中心跟其他兩個斷線時,用戶的轉帳請求該怎麼處理?」你會怎麼建議? 情境題

  • A. 讓斷線的資料中心繼續獨立處理轉帳,之後再同步餘額,確保用戶體驗不受影響
  • B. 暫停斷線資料中心的轉帳功能,等網路恢復同步後才允許操作,避免帳戶餘額不一致
  • C. 將所有用戶的請求都導到同一個資料中心處理,避免任何資料同步問題
  • D. 讓用戶自己選擇要一致性還是可用性,提供兩種模式切換

2. 你的電商平台讓用戶上傳商品圖片。目前圖片是用 Base64 編碼直接存在 PostgreSQL 資料庫裡,最近用戶抱怨頁面載入越來越慢。AI 幫你提出了以下改善方案,哪個最合理? 情境題

  • A. 升級資料庫的硬體規格(更多 RAM 和 SSD),加速圖片讀取
  • B. 把圖片壓縮到更小的尺寸後繼續存在資料庫裡
  • C. 將圖片遷移到 S3(Blob Storage),資料庫只存圖片的 URL,並搭配 CDN 分發
  • D. 直接用 CDN 取代資料庫來存放所有商品資料,包括圖片和商品描述

3. 你正在開發一個應用,需要在用戶完成 Stripe 付款後自動更新訂單狀態。你有兩個方案可選,哪個更合適? 情境題

方案 A:用 WebSocket 建立與 Stripe 的持續連線,即時接收付款結果 方案 B:設定一個 Webhook 端點,讓 Stripe 付款成功後主動 POST 通知你的 server
  • A. 方案 A 更好,因為 WebSocket 是即時的,Webhook 有延遲
  • B. 方案 B 更好,因為這是 Server 對 Server 的事件通知場景,不需要持續連線,Webhook 更高效
  • C. 兩者效果一樣,隨便選都行
  • D. 兩者都不適合,應該每秒用 HTTP 輪詢 Stripe API 檢查付款狀態

4. AI 幫你寫了以下 WebSocket 前端程式碼,準備部署到正式環境。這段程式碼有什麼問題? 錯誤診斷

const socket = new WebSocket(‘ws://myapp.com/chat’); socket.onopen = () => { socket.send(‘連線成功’); }; socket.onmessage = (event) => { document.getElementById(‘messages’).innerHTML += event.data; };
  • A. onopen 裡不應該立刻發送訊息,應該等待 server 確認
  • B. innerHTML += 有 XSS 安全風險,但跟 WebSocket 設定無關
  • C. 缺少 onerror 事件處理,但連線本身沒問題
  • D. 正式環境使用了 ws:// 而不是 wss://,連線未加密,且缺少 onclose 斷線重連處理

5. AI 幫你寫了以下 Webhook 端點來接收 Stripe 付款通知,但上線後發現有人偽造了付款成功的通知,導致未付款的訂單被標記為已付款。這段程式碼的問題出在哪裡? 錯誤診斷

@app.post(“/webhook/stripe”) async def stripe_webhook(request: Request): payload = await request.json() event_type = payload[‘type’] if event_type == ‘payment_intent.succeeded’: order_id = payload[‘data’][‘object’][‘metadata’][‘order_id’] await update_order_status(order_id, ‘paid’) return {“status”: “ok”}
  • A. 應該用 GET 而不是 POST 來接收 Webhook
  • B. 缺少 try/except 錯誤處理,導致異常時訂單狀態不正確
  • C. 沒有驗證請求來源的簽名(signature),任何人都能偽造 POST 請求到這個 URL 來假冒 Stripe 通知
  • D. Webhook URL 應該使用隨機路徑而不是固定的 /webhook/stripe,這樣別人猜不到

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *