上一章的複製式服務中,每個副本完全同質、能服務所有請求;分片服務(sharded service)則相反:每個副本(稱為分片,shard)只能服務所有請求的一個子集。負載平衡節點(稱為 root)負責檢查每個請求,把它分配給適當的分片處理。
複製式服務通常用於建構無狀態服務;分片服務通常用於建構有狀態服務。分片的主要理由是狀態大到單一機器無法承載——分片讓服務能隨「需要服務的狀態大小」擴展。

圖 6-1:複製式服務 vs. 分片服務
分片快取#
分片快取(sharded cache)位於使用者請求與實際前端實作之間。(第 3 章談過如何用大使把資料分發到分片服務;本節談如何建構那個服務本身。)設計時要考慮:為什麼需要分片快取、快取在架構中的角色、複製式分片快取、分片函式。

圖 6-2:分片快取
為什麼需要分片快取#
- 分片任何服務的首要理由都是擴大服務所能儲存的資料量。
- 算術範例:每台快取有 10 GB RAM、可服務 100 RPS(requests per second);服務的可能結果總共 200 GB、預期 1,000 RPS。
- 複製式部署:需要 10 個副本滿足 1,000 RPS,但每個副本獨立、存的資料幾乎相同,整個分散式快取最多只能放總資料集的 5%(10 GB/200 GB)——冗餘很好,記憶體利用率糟糕。
- 10 路分片部署:RPS 一樣夠(10 × 100 = 1,000),但每個快取服務完全不同的資料子集,能放總資料集的 50%(10 × 10 GB/200 GB)——每個鍵只存在於單一快取,記憶體利用率提升十倍。
快取在系統效能中的角色#
核心問題:如果快取掛了,對使用者與服務的衝擊是什麼?
- 複製式快取較無此顧慮:它可水平擴展,單一副本故障只造成暫時性失敗。分片快取不同:特定使用者或請求永遠映射到同一分片,該分片故障期間,那些請求就一直未命中,直到分片復原。快取本質是暫時性資料,未命中可以重算——但重算比直接用快取慢,會影響終端使用者效能。
- 快取效能以**命中率(hit rate)**衡量:使用者請求的資料存在於快取中的比例。命中率決定了分散式系統的整體容量與效能。
- 容量範例:服務層能扛 1,000 RPS,超過就回 HTTP 500。加上命中率 50% 的快取後,最大 RPS 從 1,000 提升到 2,000(2,000 個請求裡 1,000 個由快取服務)。但這代表快取一掛,服務層立刻過載、一半的使用者請求會失敗。
- 延遲範例:系統 100 毫秒服務一個請求;加上命中率 25%、10 毫秒回應的快取後,平均延遲降到 77.5 毫秒。延遲面的快取故障通常只是「變慢」,但某些情況下效能衝擊會讓請求在佇列裡堆積、最終逾時。
保守評定容量:上例中與其把服務容量標成 2,000 RPS,不如標 1,500 RPS——這樣即使一半的快取副本故障,服務仍然穩定。另外,務必分別在「有快取」與「無快取」下壓測系統,理解快取對整體效能的實際影響。
- 不只是故障:升級或重新部署分片快取時,不能只是部署新副本就假設它會接住負載——發布新版分片快取通常會暫時損失部分容量。
複製式分片快取#
- 當系統對快取的延遲或負載依賴深到「故障或發版時丟掉一整個分片」無法接受、或單一分片的負載大到需要擴展時,可以部署分片且複製(sharded, replicated)的服務:每個快取分片不再由單一伺服器實作,而是由一個複製式服務實作。
- 實作與部署更複雜,但優點顯著:
- 每個分片能抵禦故障、故障期間依然存在,你可以放心依賴快取帶來的效能提升,而不必為分片故障設計降級。
- 只要願意超額配置(over-provision)分片容量,就能在尖峰流量時段安全地發布快取,不必等離峰。
- 每個分片是獨立的複製式服務,可依各自負載獨立擴展——即本章末的「熱分片」。
動手做:部署大使與 memcache 分片快取
與第 3 章的分片 Redis 類似,先把 memcache 部署為 StatefulSet:
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: sharded-memcache
spec:
serviceName: "memcache"
replicas: 3
template:
metadata:
labels:
app: memcache
spec:
terminationGracePeriodSeconds: 10
containers:
- name: memcache
image: memcached
ports:
- containerPort: 11211
name: memcache再建立產生 DNS 名稱的 Service:
apiVersion: v1
kind: Service
metadata:
name: memcache
labels:
app: memcache
spec:
ports:
- port: 11211
name: memcache
clusterIP: None
selector:
app: memecache部署後便有 memcache-0.memcache、memcache-1.memcache 等 DNS 條目,可設定 twemproxy:
memcache:
listen: 127.0.0.1:11211
hash: fnv1a_64
distribution: ketama
auto_eject_hosts: true
timeout: 400
server_retry_timeout: 2000
server_failure_limit: 1
servers:
- memcache-0.memcache:11211:1
- memcache-1.memcache:11211:1
- memcache-2.memcache:11211:1以 ConfigMap 部署設定(kubectl create configmap --from-file=nutcracker.yaml twem-config),然後定義大使 Pod:
apiVersion: v1
kind: Pod
metadata:
name: sharded-memcache-ambassador
spec:
containers:
# This is where the application container would go, for example
# - name: nginx
# image: nginx
# This is the ambassador container
- name: twemproxy
image: ganomede/twemproxy
command:
- nutcracker
- -c
- /etc/config/nutcracker.yaml
- -v
- 7
- -s
- 6222
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: twem-config替代方案:複製式分片路由服務。 不用大使模式,也可以部署共享的分片路由服務。取捨:
- 服務的價值是降低複雜度——不必在每個要存取分片 memcache 的 Pod 旁部署大使,透過具名的負載平衡服務即可存取。
- 缺點有二:作為共享服務,需求增加時得跟著擴大;且多了一個網路跳點,增加請求延遲與整體網路頻寬消耗。
做法是把 twemproxy 改為監聽所有介面(listen: 0.0.0.0:11211,其餘設定相同),建立對應 ConfigMap 後,以 Deployment 部署複製式分片路由器(3 副本、掛載 shared-nutcracker.yaml),最後宣告負載平衡器:
kind: Service
apiVersion: v1
metadata:
name: shard-router-service
spec:
selector:
app: shared-twemproxy
ports:
- protocol: TCP
port: 11211
targetPort: 11211分片函式檢視#
- 有 10 個獨立分片時,某個請求
Req該用 0 ~ 9 的哪個分片S?這個映射就是**分片函式(sharding function)**的責任:Shard = ShardingFunction(Req)。它與雜湊函式非常相似——桶式雜湊表(bucket-based hashtable)其實就可視為分片服務的一例。 - 常見定義:雜湊函式加模除(%)運算。對分片而言,雜湊函式最重要的兩個特性:
- 決定性(determinism):相同輸入永遠得到相同輸出——確保特定請求永遠去同一個分片。
- 均勻性(uniformity):輸出在輸出空間中均勻分布——確保負載平均分散於各分片。
- 現代語言內建大量高品質雜湊函式,但輸出範圍通常遠大於分片數,因此用模除縮到適當範圍:
Shard = hash(Req) % 10。只要雜湊函式具備決定性與均勻性,模除會保留這些特性。
選擇鍵#
直接把整個請求物件丟給內建雜湊函式,做不出好的分片函式。以包含「請求時間、客戶端來源 IP、HTTP 請求路徑」的請求為例:
shard(request):{12:00, 1.2.3.4, /some/file.html}與{12:01, 5.6.7.8, /some/file.html}會落在不同分片——但多數情況下 IP 與時間根本不影響回應。太一般化,把回應相同的請求拆開了。shard(request.path):兩個請求映射到同一分片,一個請求的回應可以從快取服務另一個——通常更好。- 但若客戶端 IP 會影響回應(例如依 IP 查地理區域、回傳不同語言的內容),
shard(request.path)反而會出錯——法國 IP 的請求可能拿到快取裡的英文頁面。 shard(request.ip, request.path):也有問題——兩個不同的法國 IP 會映射到不同分片,分片效率差。太特定,沒把相同的請求歸在一起。- 較好的做法:
shard(country(request.ip), request.path)——先從 IP 判國家,再以國家作為鍵的一部分;法國的多個請求路由到同一分片,美國的請求路由到另一分片。
為分片函式選對鍵是分片系統設計的關鍵,而選對鍵的前提是理解你預期會看到的請求。鍵太一般化會把回應不同的請求混在一起;太特定則無法把相同請求歸群。
一致性雜湊函式#
- 新服務的初始分片很單純,麻煩的是重新分片(re-sharding):把快取從 10 個副本擴到 11 個很容易,但分片函式從
hash(Req) % 10變成hash(Req) % 11時,大量請求會被映射到與先前不同的分片。 - 對分片快取而言,這會讓未命中率暴增,直到快取依新函式重新填滿——最壞情況下,發布新分片函式等同於一次完整的快取故障。
- 解法:一致性雜湊函式(consistent hashing functions)——保證調整到
# shards個分片時,只重新映射# keys / # shards個鍵。例如從 10 分片擴到 11 分片,只有不到 10%(K/11)的鍵被重新映射,遠勝於整個分片服務報廢。
動手做:建構一致性 HTTP 分片代理
分片 HTTP 請求的第一個問題是分片鍵。好的通用鍵是請求路徑加上 fragment 與查詢參數(即讓請求唯一的一切),不含使用者的 cookie 或語言/位置(如 EN_US);若服務對使用者或其位置有大量客製,就得把它們也納入雜湊鍵。
用 nginx 作為分片代理:
worker_processes 5;
error_log error.log;
pid nginx.pid;
worker_rlimit_nofile 8192;
events {
worker_connections 1024;
}
http {
# define a named 'backend' that we can use in the proxy directive
# below.
upstream backend {
# Has the full URI of the request and use a consistent hash
hash $request_uri consistent
server web-shard-1.web;
server web-shard-2.web;
server web-shard-3.web;
}
server {
listen localhost:80;
location / {
proxy_pass http://backend;
}
}
}重點:以完整請求 URI 作為雜湊鍵,並以關鍵字 consistent 指定使用一致性雜湊函式。
分片複製式服務#
- 分片不只適用於快取——任何資料多到單機放不下的服務都適用。與前面例子不同的是,此時鍵與分片函式不在 HTTP 請求裡,而是使用者的某種脈絡(context)。
- 例子:大型多人遊戲。遊戲世界大到單機放不下,但虛擬世界中相距遙遠的玩家幾乎不會互動——因此可以把遊戲世界分片到多台機器,分片函式以玩家位置為鍵,讓同一地點的所有玩家落在同一組伺服器上。
熱分片系統#
- 理想上分片快取的負載完全平均,但實際上常因自然流量型態出現熱分片(hot shard)——某個分片被灌入不成比例的流量。
- 例子:使用者相片的分片快取。某張相片爆紅、流量暴增時,存放它的分片就「熱」了。
- 有了複製式分片快取,就能對該分片擴容因應;若為每個分片設定自動擴縮(autoscaling),還能隨自然流量的移動動態伸縮各分片。
- 書中示例:三個分片初始流量均等;之後分片 A 的流量變成 B、C 的四倍——熱分片系統把分片 B 移到與 C 同一台機器,並把分片 A 複製到第二台機器,流量再度平均分配。
