上一章的複製式服務中,每個副本完全同質、能服務所有請求;分片服務(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.memcachememcache-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 複製到第二台機器,流量再度平均分配。

圖 6-3:熱分片系統範例——初始時分片平均分布,當額外流量湧向分片 A 時,A 被複製到兩台機器,B 與 C 則合併到單一機器