最簡單、也最為人熟知的分散式模式:複製式負載平衡服務(replicated load-balanced service)。服務中每台伺服器完全相同、都能承接流量;可擴展數量的伺服器前面放一個負載平衡器(load balancer),後者通常採完全輪詢(round-robin)或某種形式的工作階段黏著(session stickiness)。
無狀態服務#
- 無狀態服務(stateless service):不需要保存狀態就能正確運作的服務。最簡單的情況下,甚至每個獨立請求都可以被路由到不同的服務實例。例子:靜態內容伺服器、從多個後端系統接收並聚合回應的複雜中介層系統。

圖 5-1:基本的複製式無狀態服務
- 無狀態系統靠複製提供冗餘與規模;水平擴展系統靠增加副本承接越來越多的使用者。

圖 5-2:複製式無狀態應用的水平擴展
負載平衡的就緒探針#
- 光是複製加負載平衡還不完整——設計複製式服務時,同樣重要的是建立**就緒探針(readiness probe)**來通知負載平衡器。
- 區別:健康探針(health probe)讓容器編排系統判斷應用何時需要重啟;就緒探針判斷應用何時準備好服務使用者請求。
- 許多應用啟動後需要時間初始化——連資料庫、載入外掛、從網路下載服務檔案。這些時候容器是活著的,但還沒準備好。
為複製式服務模式建構應用時,務必包含一個實作就緒檢查的專屬 URL。
動手做:在 Kubernetes 建立複製式服務
以一個提供字典釋義的小型 NodeJS 應用為例。先在本機試跑:
docker run -p 8080:8080 brendanburns/dictionary-server造訪 http://localhost:8080/dog 可看到 dog 的釋義。看容器日誌會發現它立刻開始服務,但要等約 8 MB 的字典從網路下載完成後才回報就緒。
在 Kubernetes 用 Deployment 部署(注意 readinessProbe 設定):
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: dictionary-server
spec:
replicas: 3
template:
metadata:
labels:
app: dictionary-server
spec:
containers:
- name: server
image: brendanburns/dictionary-server
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5以 kubectl create -f dictionary-deploy.yaml 建立。有了多個副本後,需要負載平衡器把請求帶到副本——它同時分配負載、把複製式服務與其消費者隔開,並提供一個獨立於任何特定副本的可解析名稱。在 Kubernetes 用 Service 物件建立:
kind: Service
apiVersion: v1
metadata:
name: dictionary-server-service
spec:
selector:
app: dictionary-server
ports:
- protocol: TCP
port: 8080
targetPort: 8080以 kubectl create -f dictionary-service.yaml 建立。
工作階段追蹤服務#
- 前面的模式把所有使用者的請求平均分給所有副本;但有時你希望特定使用者的請求永遠落在同一台機器:
- 該使用者的資料快取在記憶體裡,落在同一台能提高快取命中率。
- 互動本質上是長時間的,請求之間需要維持某些狀態。
- 工作階段追蹤服務(session tracked services)確保單一使用者的所有請求映射到同一個副本。一般做法是雜湊來源與目的 IP 位址作為鍵來決定伺服器——只要來源與目的 IP 不變,請求就送往同一副本。
- 追蹤通常用**一致性雜湊(consistent hashing)**實作:擴縮容時副本數改變,使用者對副本的映射難免變動,一致性雜湊能把「實際換了副本」的使用者數降到最低,減少擴縮容對應用的衝擊。

圖 5-3:工作階段追蹤服務——特定使用者的所有請求都被路由到單一實例
基於 IP 的工作階段追蹤在叢集內部(內部 IP)可行,但對外部 IP 通常不管用——因為網路位址轉換(NAT)。對外的工作階段追蹤,偏好應用層級的追蹤(例如透過 cookie)。
應用層複製式服務#
前述例子的複製與負載平衡都發生在服務的網路層,與 TCP/IP 之上實際使用的協定無關。但許多應用彼此用 HTTP 溝通——知道應用層協定,就能對複製式無狀態服務模式做進一步加值。
引入快取層#
- 無狀態服務的程式碼仍可能很昂貴:服務請求時要查資料庫、做大量渲染或資料混合。此時快取層非常合理。
- Web 應用最簡單的快取形式是快取式 Web 代理(caching web proxy):一個把使用者請求維持在記憶體狀態的 HTTP 伺服器。兩位使用者請求同一頁面時,只有一個請求打到後端,另一個直接從記憶體回應。本章使用開源 Web 快取 Varnish。

圖 5-4:快取伺服器的運作方式
部署快取#
- 最簡單的部署方式是用邊車模式,讓快取跟著每個 Web 伺服器實例——但這有缺點:快取得跟 Web 伺服器等比例擴展。

圖 5-5:以邊車形式加入 Web 快取伺服器
- 快取的理想是副本少、每個副本資源多(例如:與其 10 個各 1 GB RAM 的副本,不如 2 個各 5 GB 的副本)。因為每一頁會存在每個副本裡:10 個副本等於每頁存 10 次,能放進快取的頁面總量變少,**命中率(hit rate)**下降,快取的效用隨之降低。
- 反過來,Web 伺服器往往需要很多小副本:許多語言(如 NodeJS)只能利用單一核心,需要多副本才能吃滿多核。
- 結論:把快取層配置為 Web 服務層之上的第二個無狀態複製式服務層。

圖 5-6:把快取層加進複製式服務
不小心的話,快取會弄壞工作階段追蹤:使用預設的 IP 位址親和性時,所有請求的來源 IP 都是快取的 IP,不是終端使用者的。若照建議部署了少數大快取,基於 IP 的親和性甚至可能讓 Web 層的某些副本完全收不到流量。應改用 cookie 或 HTTP 標頭做工作階段追蹤。
動手做:部署快取層(Varnish)
之前建立的 dictionary-server-service 可透過 DNS 名稱發現。Varnish 設定指向它:
vcl 4.0;
backend default {
.host = "dictionary-server-service";
.port = "8080";
}建立 ConfigMap 保存設定:
kubectl create configmap varnish-config --from-file=default.vcl部署複製式 Varnish 快取:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: varnish-cache
spec:
replicas: 2
template:
metadata:
labels:
app: varnish-cache
spec:
containers:
- name: cache
resources:
requests:
# We'll use two gigabytes for each varnish cache
memory: 2Gi
image: brendanburns/varnish
command:
- varnishd
- -F
- -f
- /etc/varnish-config/default.vcl
- -a
- 0.0.0.0:8080
- -s
# This memory allocation should match the memory request above
- malloc,2G
ports:
- containerPort: 8080
volumeMounts:
- name: varnish
mountPath: /etc/varnish-config
volumes:
- name: varnish
configMap:
name: varnish-config最後為 Varnish 快取部署負載平衡器:
kind: Service
apiVersion: v1
metadata:
name: varnish-service
spec:
selector:
app: varnish-cache
ports:
- protocol: TCP
port: 80
targetPort: 8080分別以 kubectl create -f varnish-deploy.yaml、kubectl create -f varnish-service.yaml 建立。

圖 5-7:為字典伺服器加上快取層
擴充快取層#
Varnish 這類 HTTP 反向代理(reverse proxy)通常是可外掛的,能提供快取之外的多項進階功能。
速率限制與阻斷服務防禦#
- 很少人建站時預期會遭遇阻斷服務(denial-of-service)攻擊;但隨著大家越來越常建 API,「阻斷服務」可能只是開發者設錯客戶端、或 SRE 不小心對正式環境跑了壓測。
- 因此在快取層加上**速率限制(rate limiting)**作為一般性防禦是合理的。Varnish 有 throttle 模組,可依 IP 位址、請求路徑、是否登入來節流。
部署 API 的最佳實踐:匿名存取給相對低的速率上限,要求登入才能取得更高上限。登入既提供稽核(查得出誰造成非預期負載),也是攻擊者的門檻——要成功攻擊得先取得多重身分。
- 使用者觸頂時,伺服器回傳 429(請求過多)錯誤碼;許多使用者想知道還剩多少額度,因此通常也會在 HTTP 標頭放剩餘次數資訊——沒有標準標頭,但許多 API 使用
X-RateLimit-Remaining的變體。
SSL 終結#
- 邊緣層除了快取,另一常見任務是 SSL 終結(SSL termination)。
- 即使叢集內各層之間也打算用 SSL,邊緣與內部服務仍應使用不同憑證;且每個內部服務都該有自己的憑證,確保各層能獨立發版。
- Varnish 不能做 SSL 終結,但 nginx 可以。因此在無狀態應用模式中加入第三層:一層複製式 nginx 伺服器,負責終結 HTTPS 流量並轉發給 Varnish 快取;HTTP 流量照樣進 Varnish,Varnish 再轉發給 Web 應用。
動手做:部署 nginx 與 SSL 終結
以下假設你已有憑證(最簡單的取得方式是 Let’s Encrypt 的工具,或用 openssl 自建),並命名為
server.crt(公開憑證)與server.key(私鑰)。自簽憑證會觸發現代瀏覽器的安全警告,絕不可用於正式環境。
先把憑證上傳為 Kubernetes 的 secret:
kubectl create secret tls ssl --cert=server.crt --key=server.key建立服務 SSL 的 nginx 設定:
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
server_name my-domain.com www.my-domain.com;
ssl on;
ssl_certificate /etc/certs/tls.crt;
ssl_certificate_key /etc/certs/tls.key;
location / {
proxy_pass http://varnish-service:80;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
}轉為 ConfigMap(kubectl create configmap nginx-conf --from-file=nginx.conf),然後建立複製式無狀態 nginx 層:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ssl
spec:
replicas: 4
template:
metadata:
labels:
app: nginx-ssl
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 443
volumeMounts:
- name: conf
mountPath: /etc/nginx
- name: certs
mountPath: /etc/certs
volumes:
- name: conf
configMap:
# This is the ConfigMap for nginx we created previously
name: nginx-conf
- name: certs
secret:
# This is the secret we created above
secretName: ssl最後以 type: LoadBalancer 的 Service 對外暴露:
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
selector:
app: nginx-ssl
type: LoadBalancer
ports:
- protocol: TCP
port: 443
targetPort: 443在支援外部負載平衡器的 Kubernetes 叢集上,這會建立一個在公開 IP 上服務的對外服務;kubectl get services 可查得 IP,之後即可用瀏覽器存取。
本章小結#

圖 5-8:完整的複製式無狀態服務範例
- 從最簡單的複製式無狀態服務模式出發,逐步疊加兩個額外的複製式負載平衡層:快取層(效能)與 SSL 終結層(安全的 Web 服務)。
- 完整模式在 Kubernetes 上用三個 Deployment 與連接各層的 Service 負載平衡器即可部署;完整範例原始碼見 https://github.com/brendandburns/designing-distributed-systems ↗。