大使模式(ambassador pattern):由大使容器仲介(broker)應用容器與外部世界的互動。與其他單節點模式相同,兩個容器緊密共生、被排程到同一台機器上。

大使模式的價值有二:

  • 與其他單節點模式一樣,模組化、可重用的容器本身就有價值——關注點分離讓容器更容易建構與維護。
  • 大使容器可以與多個不同的應用容器搭配重用——程式碼一次寫好、多處使用,開發更快,實作也因為「建一次、用在許多情境」而更一致、品質更高。

圖 3-1:通用的大使模式

用大使為服務分片#

  • 當儲存層的資料大到單一機器無法承載,就需要分片(sharding):把儲存層切成多個互斥的片段,各由一台機器承載。(本章只談「讓既有服務與已存在的分片服務對話」的單節點模式;多節點分片服務的設計詳見第 6 章。)
  • 部署分片服務時的問題:如何與儲存資料的前端或中介層程式碼整合?需要有邏輯把特定請求路由到特定分片,但既有程式碼往往假設連向單一儲存後端,難以事後塞入分片客戶端;分片也讓開發環境(通常只有一個分片)與正式環境(通常很多分片)難以共享設定。

圖 3-2:通用的分片服務

兩種做法的取捨:

  • 分片邏輯放在服務端:分片服務自帶無狀態負載平衡器,把流量導向正確分片——實質上是「作為服務的分散式大使」。客戶端不需要大使,但分片服務的部署更複雜。
  • 客戶端整合單節點大使:由大使把流量路由到正確分片。客戶端部署稍微複雜,但簡化了分片服務的部署。

兩種做法都是合理的,選擇取決於應用的具體情況——例如團隊分界落在架構的哪裡、哪些部分是自己寫程式碼、哪些只是部署現成軟體。

  • 客戶端大使的做法:引入一個包含所有路由邏輯的大使容器,前端或中介層應用只需連向「看起來像單一儲存後端」的 localhost 服務;實際上這是分片大使代理(sharding ambassador proxy),它接收應用的所有請求、轉發到正確的儲存分片、再把結果回傳。
  • 結果是乾淨的關注點分離:應用容器只知道「要連 localhost 上的儲存服務」,大使只包含分片所需的程式碼;大使還能跨應用重用,甚至直接採用現成的開源實作。
動手做:實作分片 Redis(twemproxy)

以 Redis 作為快取,先用 StatefulSet 在 Kubernetes 叢集部署分片 Redis(StatefulSet 會給每個分片唯一的 DNS 名稱,方便設定代理):

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: sharded-redis
spec:
  serviceName: "redis"
  replicas: 3
  template:
    metadata:
      labels:
        app: redis
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: redis
          image: redis
          ports:
            - containerPort: 6379
              name: redis

存成 redis-shards.yaml 後以 kubectl create -f redis-shards.yaml 部署,會建立三個 Redis 容器(kubectl get pods 可見 sharded-redis-[0,1,2])。

再建立 Headless Service 為副本產生 DNS 名稱:

apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
spec:
  ports:
    - port: 6379
      name: redis
  clusterIP: None
  selector:
    app: redis

部署後就有 sharded-redis-0.redissharded-redis-1.redis 等 DNS 條目,可用來設定 twemproxy——Twitter 開發、開源的輕量高效能 memcached/Redis 代理:

redis:
  listen: 127.0.0.1:6379
  hash: fnv1a_64
  distribution: ketama
  auto_eject_hosts: true
  redis: true
  timeout: 400
  server_retry_timeout: 2000
  server_failure_limit: 1
  servers:
    - sharded-redis-0.redis:6379:1
    - sharded-redis-1.redis:6379:1
    - sharded-redis-2.redis:6379:1

代理在 localhost:6379 服務 Redis 協定,讓應用容器可以存取大使。設定用 ConfigMap 部署(kubectl create configmap --from-file=nutcracker.yaml),最後定義 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: ambassador-example
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

這個 Pod 定義了大使;再注入使用者自己的應用容器即完成。

用大使做服務仲介#

  • 要讓應用可攜(portable)於多種環境(公有雲、實體資料中心、私有雲),主要挑戰之一是服務發現(service discovery)與設定。例如依賴 MySQL 的前端:在公有雲上 MySQL 可能是 SaaS 服務,在私有雲則可能得動態啟一台跑 MySQL 的虛擬機或容器。
  • 可攜的應用必須知道如何檢視自身環境、找到正確的 MySQL 服務來連線;執行這種發現與連結的系統,通常稱為服務仲介(service broker)
  • 大使解法:應用永遠只連 localhost 上的服務實例(如 MySQL);由服務仲介大使負責檢視環境、仲介出正確的連線。

圖 3-3:服務仲介大使建立 MySQL 服務

用大使做實驗或請求分流#

  • 請求分流(request splitting):讓一部分請求不由主要正式服務處理,而是導向服務的另一個實作。最常見的用途是對新的 Beta 版做實驗,判斷新版軟體的可靠性與效能是否與現行版本相當。
  • 另一種用法是分接(tee)流量:所有流量同時送往正式系統與尚未上線的新版;回應以正式系統為準,分接服務的回應則被忽略——常用來在不影響正式使用者的前提下,對新版模擬正式負載。
  • 大使解法:應用容器照樣連 localhost;大使容器接收請求、把請求代理給正式與實驗系統,再把正式系統的回應當作自己的處理結果回傳。
  • 這種關注點分離讓每個容器的程式碼精簡聚焦,請求分流大使也能在各種應用與情境中重用。
動手做:實作 10% 實驗(nginx)

用 nginx 作為大使實作請求分流(此為 HTTP 設定,可輕易改為 HTTPS):

worker_processes 5;
error_log error.log;
pid        nginx.pid;
worker_rlimit_nofile 8192;

events {
  worker_connections   1024;
}

http {
    upstream backend {
        ip_hash;
        server web weight=9;
        server experiment;
    }

    server {
        listen localhost:80;
        location / {
             proxy_pass http://backend;
        }
    }
}

兩個設定重點:

  • ip_hash:確保同一位使用者不會在實驗版與主站之間來回跳動,維持一致的使用體驗。
  • weight=9:90% 流量送往既有主應用,10% 導向實驗版。

設定同樣以 ConfigMap 部署(kubectl create configmaps --from-file=nginx.conf)。注意必須先建立 webexperiment 兩個 Service——nginx 找不到要代理的服務就不肯啟動:

# This is the 'experiment' service
apiVersion: v1
kind: Service
metadata:
  name: experiment
  labels:
    app: experiment
spec:
  ports:
    - port: 80
      name: web
  selector:
    # Change this selector to match your application's labels
    app: experiment
---
# This is the 'prod' service
apiVersion: v1
kind: Service
metadata:
  name: web
  labels:
    app: web
spec:
  ports:
    - port: 80
      name: web
  selector:
    # Change this selector to match your application's labels
    app: web

最後把 nginx 部署為 Pod 內的大使容器:

apiVersion: v1
kind: Pod
metadata:
  name: experiment-example
spec:
  containers:
    # This is where the application container would go, for example
    # - name: some-name
    #   image: some-image
    # This is the ambassador container
    - name: nginx
      image: nginx
      volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx
  volumes:
    - name: config-volume
      configMap:
        name: experiment-config

之後即可在 Pod 中加入第二個(第三、第四個)容器來利用這個大使。

與分片服務的討論相同,實驗框架也可以部署為應用前方的獨立微服務,而非整合進客戶端 Pod。代價是多了一個需要維護、擴展、監控的服務——若實驗會是架構中的長期元件,這可能值得;若只是偶爾使用,客戶端大使更合理。