核心元件(Core Components)
是什麼?
分散式系統的四大核心元件,各自解決不同的效能和可靠性問題。理解它們的職責和適用場景,是 System Design 的基本功。
ℹ️記住這個口訣
LCMQ:Load Balancer 分流量、Cache 省查詢、Message Queue 解耦合、CDN 近用戶。每個元件對應一種瓶頸。
核心觀念
Load Balancer
- 解決問題:單一伺服器無法處理所有請求,需要把流量分散到多台伺服器
- 常見演算法:Round Robin(依序輪流,最簡單)、Weighted Round Robin(高規格機器分更多流量)、Least Connections(分給目前連線最少的伺服器,適合長連線場景)、IP Hash(同一 IP 固定到同一台伺服器,適合需要 Session Affinity 的過渡期)
- L4 vs L7:L4 在 TCP 層分流,只看 IP + Port,速度快、開銷小,適合不需要根據內容路由的場景(如資料庫 Proxy)。L7 在 HTTP 層分流,可以根據 URL Path、Header、Cookie 做智慧路由(如
/api/*到 API 伺服器、/static/*到靜態資源伺服器),適合微服務架構 - 何時用:只要有 2 台以上伺服器就需要 LB。單台伺服器也可以放 LB 做健康檢查和未來擴展的準備
- 怎麼選:自建環境用 Nginx(最普及、社群資源多)或 HAProxy(效能極佳、TCP 支援好);AWS 用 ALB(L7,支援 Path-based routing)或 NLB(L4,超高效能);需要 Service Mesh 用 Envoy
- 常見陷阱:LB 本身也會成為 SPOF — 必須部署至少兩台 LB 做 Active-Passive 或 Active-Active。健康檢查(Health Check)設定太寬鬆會把流量導到已經半死不活的伺服器
Cache
- 解決問題:資料庫查詢太慢或太頻繁,用記憶體快取熱點資料,將回應時間從數十 ms 降到亞毫秒級
- 策略:Cache Aside(應用程式自己管讀寫快取,最常見、最靈活)、Write Through(寫入同時更新 Cache 和 DB,一致性好但寫入慢)、Write Behind(先寫 Cache 再非同步寫 DB,寫入快但有資料遺失風險)、Read Through(Cache 層自動從 DB 載入,應用程式只跟 Cache 互動)
- 淘汰策略:LRU(最近最少使用,通用首選)、LFU(最少頻率使用,適合有明顯熱點的場景)、TTL(存活時間,必須設定以避免快取永遠不更新)
- 何時用:讀寫比超過 10:1 的熱點資料、運算成本高的結果(如複雜 SQL 查詢、API 聚合結果)、變化頻率低的設定資料。不適合:寫入頻繁的資料、每次查詢條件都不同的資料
- 怎麼選:需要複雜資料結構(List、Set、Sorted Set)、Pub/Sub、Lua Script → Redis。純 key-value 快取且追求極致效能 → Memcached(多執行緒,單純 GET/SET 場景略快)。90% 的場景選 Redis 就對了
- 常見陷阱:Cache Penetration(查詢不存在的 Key,每次都穿透到 DB — 解法:快取空值或用 Bloom Filter)、Cache Stampede(快取同時過期,大量請求湧入 DB — 解法:加隨機 TTL 偏移或用分散式鎖)、忘記在資料更新時清除快取導致資料不一致
Message Queue
- 解決問題:服務之間直接呼叫會造成耦合(A 掛了 B 也跟著掛),且無法應對流量尖峰(瞬間 10 倍流量直接把下游壓垮)
- 核心價值:解耦(生產者和消費者互不依賴)、削峰填谷(尖峰時訊息堆在 Queue 裡,消費者按自己的速度處理)、非同步處理(耗時任務不阻塞 API 回應)
- 模式:Point-to-Point(一條訊息只被一個消費者處理,如訂單處理)、Pub/Sub(一條訊息被多個訂閱者處理,如用戶註冊後同時通知多個服務)
- 何時用:任何不需要同步回應的操作(寄信、產生報表、推送通知)、跨服務通訊、需要重試機制的任務、流量波動大的場景。不適合:需要即時回應的查詢操作、資料量很小且服務間直接呼叫就夠用的場景
- 怎麼選:需要高吞吐量(百萬 msg/s)、日誌串流、事件溯源 → Kafka。需要靈活路由(Header/Topic/Fanout)、訊息確認、延遲佇列 → RabbitMQ。想省運維成本、已在 AWS 生態 → SQS + SNS
- 常見陷阱:訊息重複消費(消費者必須做冪等處理,如用訊息 ID 去重)、訊息順序錯亂(Kafka 只保證 Partition 內有序,跨 Partition 無序)、Dead Letter Queue 沒設定導致失敗訊息永遠重試
CDN
- 解決問題:用戶離伺服器太遠,靜態資源載入慢。例如台灣用戶存取放在美國的伺服器,光是網路延遲就要 150ms+。CDN 把資源快取到台灣的邊緣節點,延遲降到 5ms 以內
- 運作方式:第一個用戶請求時,CDN 邊緣節點從 Origin Server 拉取資源並快取;後續用戶直接從邊緣節點取得,不再回源
- Pull vs Push:Pull CDN 被動拉取(第一次請求才快取,適合大多數場景)。Push CDN 主動推送(你上傳檔案到 CDN,適合大檔案如影片、且上線前就要全球可用的場景)
- 何時用:靜態資源(JS、CSS、圖片、影片)、用戶分布在多個地理區域、需要降低 Origin Server 負載。不適合:動態 API 回應(但 CloudFront 等也開始支援動態內容加速)、資料必須即時更新且不能有任何快取延遲的場景
- 怎麼選:已在 AWS 生態 → CloudFront(和 S3/ALB 整合最好)。需要 WAF + DDoS 防護 + CDN 一體化 → Cloudflare(免費方案就很夠用)。需要極致效能和邊緣運算 → Fastly(支援 Wasm)。企業級大流量 → Akamai
- 常見陷阱:快取過期策略沒設好,導致用戶看到舊版 JS/CSS(解法:檔名加 content hash 如
app.a1b2c3.js)。Origin Server 沒設定正確的 Cache-Control Header。CORS 設定遺漏導致跨域請求失敗
常見誤區
⚠️Cache 不是銀彈
Cache Invalidation 是電腦科學最難的問題之一。快取資料和資料庫不一致會造成嚴重 bug。永遠設定 TTL,並在資料更新時主動清除快取。別把所有東西都快取 — 只快取「讀多寫少」的熱點資料。
設計流程
分析流量模式
讀多寫少?需要即時?有流量尖峰?
加入 Load Balancer
多台伺服器前面放 LB,選擇 L4 或 L7
加入 Cache 層
熱點資料放 Redis,減少資料庫壓力
加入 Message Queue
非同步任務放入 Queue,削峰填谷
加入 CDN
靜態資源上 CDN,減少 Origin 負載和延遲
程式碼範例
Cache Aside Pattern(C#)
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly ProductRepository _repo;
public async Task<Product?> GetProduct(int id)
{
// 1. 先查快取
var cached = await _cache.GetAsync<Product>($"product:{id}");
if (cached != null) return cached;
// 2. 快取沒有,查資料庫
var product = await _repo.FindByIdAsync(id);
if (product == null) return null;
// 3. 寫入快取,設定 TTL
await _cache.SetAsync($"product:{id}", product, TimeSpan.FromMinutes(10));
return product;
}
public async Task UpdateProduct(Product product)
{
await _repo.UpdateAsync(product);
// 4. 更新時清除快取,下次讀取會重新載入
await _cache.RemoveAsync($"product:{product.Id}");
}
}Message Queue 消費者(TypeScript)
import amqplib from "amqplib";
// Producer:發送訂單事件
async function publishOrder(order: Order) {
const conn = await amqplib.connect("amqp://localhost");
const channel = await conn.createChannel();
await channel.assertQueue("orders");
channel.sendToQueue("orders", Buffer.from(JSON.stringify(order)));
}
// Consumer:非同步處理訂單
async function consumeOrders() {
const conn = await amqplib.connect("amqp://localhost");
const channel = await conn.createChannel();
await channel.assertQueue("orders");
channel.consume("orders", async (msg) => {
if (!msg) return;
const order: Order = JSON.parse(msg.content.toString());
await processOrder(order); // 寄信、扣庫存、記帳...
channel.ack(msg);
});
}Load Balancer 設定(Nginx)
upstream backend {
least_conn; # 最少連線數演算法
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080 weight=1;
server 10.0.0.3:8080 backup; # 備援伺服器
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}架構圖
架構圖解讀:用戶請求分兩條路 — 靜態資源(圖片、JS、CSS)由 CDN 就近回應,API 請求透過 Load Balancer 分配到 App Server。App Server 先查 Redis Cache,Cache Miss 才查 Database。需要非同步處理的任務(如寄信)透過 RabbitMQ 發給 Worker Service,不阻塞 API 回應。
實戰補充
💡選型決策樹
- Cache:需要 Pub/Sub 或複雜資料結構 → Redis;純 key-value 高速快取 → Memcached
- Message Queue:需要高吞吐量日誌流 → Kafka;需要靈活路由 → RabbitMQ;想省事 → AWS SQS
- CDN:已在 AWS 生態 → CloudFront;需要 WAF + CDN 一體化 → Cloudflare
- Load Balancer:需要高度自定義 → Nginx/HAProxy;雲端環境 → AWS ALB/NLB
理解測驗
🤔 一個電商網站的商品頁面每秒被瀏覽 10,000 次,但商品資料每小時才更新一次。最適合加入哪個元件來優化?
🤔 用戶註冊後需要:寄歡迎信、建立預設設定、通知推薦系統。這些步驟不需要在註冊 API 回應前完成。最適合用哪個元件?
🤔 L4 和 L7 Load Balancer 的主要差異是什麼?
重點整理
💡一句話記住
LB 分流量、Cache 省查詢、MQ 解耦合、CDN 近用戶。 記憶口訣:「分省解近」— 每個元件解決一種瓶頸,缺哪個就加哪個。
| 元件 | 解決的問題 | 常見選型 | 使用時機 | |------|-----------|---------|---------| | Load Balancer | 流量集中在單一伺服器 | Nginx、ALB | 多台伺服器需要分流 | | Cache | 資料庫查詢太頻繁 | Redis、Memcached | 讀多寫少的熱點資料 | | Message Queue | 服務耦合、流量尖峰 | RabbitMQ、Kafka | 非同步處理、削峰填谷 | | CDN | 用戶離伺服器太遠 | CloudFront、Cloudflare | 靜態資源全球分發 |