執行 ZooKeeper,分散式系統協調器

本教學示範如何在 Kubernetes 上使用 Apache ZooKeeper,並搭配 StatefulSetsPodDisruptionBudgetsPodAntiAffinity

開始之前

在開始本教學之前,您應該熟悉下列 Kubernetes 概念

您必須有一個至少有四個節點的叢集,且每個節點至少需要 2 個 CPU 和 4 GiB 的記憶體。在本教學中,您將隔離並排空叢集的節點。這表示叢集將終止並驅逐其節點上的所有 Pod,且節點將暫時變成不可排程。 您應該為本教學使用專用叢集,或者您應確保您造成的中斷不會干擾其他租戶。

本教學假設您已將叢集設定為動態佈建 PersistentVolumes。如果您的叢集未設定為執行此操作,您將必須在本教學開始前手動佈建三個 20 GiB 的 Volume。

目標

完成本教學後,您將了解以下內容。

  • 如何使用 StatefulSet 部署 ZooKeeper 集群。
  • 如何一致地設定集群。
  • 如何在集群中分散 ZooKeeper 伺服器的部署。
  • 如何在計畫性維護期間使用 PodDisruptionBudgets 來確保服務可用性。

ZooKeeper

Apache ZooKeeper 是一個分散式、開放原始碼的協調服務,適用於分散式應用程式。ZooKeeper 可讓您讀取、寫入和觀察資料的更新。資料以類似檔案系統的階層結構組織,並複寫到集群中的所有 ZooKeeper 伺服器(一組 ZooKeeper 伺服器)。所有資料操作都是原子且循序一致的。ZooKeeper 透過使用 Zab 共識協定,將狀態機複寫到集群中的所有伺服器,來確保這一點。

集群使用 Zab 協定來選出領導者,且在選舉完成之前,集群無法寫入資料。完成後,集群使用 Zab 來確保它將所有寫入複寫到仲裁,然後才確認並使其對用戶端可見。在不考慮加權仲裁的情況下,仲裁是包含目前領導者的集群的多數元件。例如,如果集群有三台伺服器,則包含領導者和另一台伺服器的元件構成仲裁。如果集群無法達成仲裁,則集群無法寫入資料。

ZooKeeper 伺服器將其整個狀態機保存在記憶體中,並將每個變更寫入儲存媒體上的持久 WAL(預寫日誌)。當伺服器崩潰時,它可以透過重播 WAL 來復原其先前的狀態。為了防止 WAL 無限制地增長,ZooKeeper 伺服器會定期將它們的記憶體狀態快照到儲存媒體。這些快照可以直接載入到記憶體中,且所有在快照之前的 WAL 條目都可以捨棄。

建立 ZooKeeper 集群

以下資訊清單包含 Headless ServiceServicePodDisruptionBudgetStatefulSet

apiVersion: v1
kind: Service
metadata:
  name: zk-hs
  labels:
    app: zk
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: Service
metadata:
  name: zk-cs
  labels:
    app: zk
spec:
  ports:
  - port: 2181
    name: client
  selector:
    app: zk
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  selector:
    matchLabels:
      app: zk
  maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zk
spec:
  selector:
    matchLabels:
      app: zk
  serviceName: zk-hs
  replicas: 3
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: kubernetes-zookeeper
        imagePullPolicy: Always
        image: "registry.k8s.io/kubernetes-zookeeper:1.0-3.4.10"
        resources:
          requests:
            memory: "1Gi"
            cpu: "0.5"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        command:
        - sh
        - -c
        - "start-zookeeper \
          --servers=3 \
          --data_dir=/var/lib/zookeeper/data \
          --data_log_dir=/var/lib/zookeeper/data/log \
          --conf_dir=/opt/zookeeper/conf \
          --client_port=2181 \
          --election_port=3888 \
          --server_port=2888 \
          --tick_time=2000 \
          --init_limit=10 \
          --sync_limit=5 \
          --heap=512M \
          --max_client_cnxns=60 \
          --snap_retain_count=3 \
          --purge_interval=12 \
          --max_session_timeout=40000 \
          --min_session_timeout=4000 \
          --log_level=INFO"
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

開啟終端機,並使用 kubectl apply 命令來建立資訊清單。

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

這會建立 zk-hs Headless Service、zk-cs Service、zk-pdb PodDisruptionBudget 和 zk StatefulSet。

service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created

使用 kubectl get 觀看 StatefulSet 控制器建立 StatefulSet 的 Pod。

kubectl get pods -w -l app=zk

一旦 zk-2 Pod 處於 Running 和 Ready 狀態,請使用 CTRL-C 終止 kubectl。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

StatefulSet 控制器會建立三個 Pod,且每個 Pod 都有一個包含 ZooKeeper 伺服器的容器。

促進領導者選舉

由於在匿名網路中沒有終止演算法來選出領導者,因此 Zab 需要明確的成員資格組態才能執行領導者選舉。集群中的每台伺服器都需要具有唯一的識別碼,所有伺服器都需要知道全域識別碼集,且每個識別碼都需要與網路位址相關聯。

使用 kubectl exec 取得 zk StatefulSet 中 Pod 的主機名稱。

for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

StatefulSet 控制器會根據序數索引為每個 Pod 提供唯一的主機名稱。主機名稱採用 <statefulset name>-<ordinal index> 的格式。由於 zk StatefulSet 的 replicas 欄位設定為 3,因此 Set 的控制器會建立三個 Pod,其主機名稱分別設定為 zk-0zk-1zk-2

zk-0
zk-1
zk-2

ZooKeeper 集群中的伺服器使用自然數作為唯一識別碼,並將每個伺服器的識別碼儲存在伺服器資料目錄中名為 myid 的檔案中。

若要檢查每個伺服器的 myid 檔案內容,請使用以下命令。

for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

由於識別碼是自然數,而序數索引是非負整數,因此您可以透過將序數加 1 來產生識別碼。

myid zk-0
1
myid zk-1
2
myid zk-2
3

若要取得 zk StatefulSet 中每個 Pod 的完整網域名稱 (FQDN),請使用以下命令。

for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-hs 服務會為所有 Pod 建立一個網域,即 zk-hs.default.svc.cluster.local

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

Kubernetes DNS 中的 A 記錄會將 FQDN 解析為 Pod 的 IP 位址。如果 Kubernetes 重新排程 Pod,它將使用 Pod 的新 IP 位址更新 A 記錄,但 A 記錄名稱不會變更。

ZooKeeper 將其應用程式組態儲存在名為 zoo.cfg 的檔案中。使用 kubectl exec 來檢視 zk-0 Pod 中 zoo.cfg 檔案的內容。

kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

在檔案底部的 server.1server.2server.3 屬性中,123 對應於 ZooKeeper 伺服器 myid 檔案中的識別碼。它們被設定為 zk StatefulSet 中 Pod 的 FQDN。

clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

達成共識

共識協定要求每個參與者的識別碼必須是唯一的。Zab 協定中不應有兩個參與者聲稱相同的唯一識別碼。這對於允許系統中的程序就哪些程序已提交哪些資料達成一致至關重要。如果啟動兩個具有相同序數的 Pod,則兩個 ZooKeeper 伺服器都將把自己識別為相同的伺服器。

kubectl get pods -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

每個 Pod 的 A 記錄會在 Pod 變成 Ready 狀態時輸入。因此,ZooKeeper 伺服器的 FQDN 將解析為單一端點,而該端點將是聲稱在其 myid 檔案中設定的身分識別的唯一 ZooKeeper 伺服器。

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

這確保了 ZooKeeper 的 zoo.cfg 檔案中的 servers 屬性代表正確設定的集群。

server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

當伺服器使用 Zab 協定嘗試提交值時,它們將達成共識並提交值(如果領導者選舉成功且至少有兩個 Pod 處於 Running 和 Ready 狀態),或者它們將無法做到這一點(如果未滿足任一條件)。不會出現一個伺服器代表另一個伺服器確認寫入的狀態。

健全性測試集群

最基本的健全性測試是將資料寫入一個 ZooKeeper 伺服器,然後從另一個伺服器讀取資料。

以下命令執行 zkCli.sh 腳本,將 world 寫入集群中 zk-0 Pod 上的路徑 /hello

kubectl exec zk-0 -- zkCli.sh create /hello world
WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hello

若要從 zk-1 Pod 取得資料,請使用以下命令。

kubectl exec zk-1 -- zkCli.sh get /hello

您在 zk-0 上建立的資料在集群中的所有伺服器上都可用。

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

提供持久儲存

ZooKeeper 基礎知識章節所述,ZooKeeper 會將所有條目提交到持久 WAL,並定期將記憶體狀態的快照寫入儲存媒體。使用 WAL 提供持久性是使用共識協定來達成複寫狀態機的應用程式的常用技術。

使用 kubectl delete 命令來刪除 zk StatefulSet。

kubectl delete statefulset zk
statefulset.apps "zk" deleted

觀察 StatefulSet 中 Pod 的終止。

kubectl get pods -w -l app=zk

zk-0 完全終止時,使用 CTRL-C 終止 kubectl。

zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m

重新套用 zookeeper.yaml 中的 manifest。

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

這會建立 zk StatefulSet 物件,但 manifest 中的其他 API 物件不會被修改,因為它們已存在。

觀察 StatefulSet 控制器重新建立 StatefulSet 的 Pod。

kubectl get pods -w -l app=zk

一旦 zk-2 Pod 處於 Running 和 Ready 狀態,請使用 CTRL-C 終止 kubectl。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

使用以下命令從 zk-2 Pod 取得您在健全性測試期間輸入的值。

kubectl exec zk-2 zkCli.sh get /hello

即使您終止並重新建立了 zk StatefulSet 中的所有 Pod,集群仍然提供原始值。

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

zk StatefulSet 的 specvolumeClaimTemplates 欄位指定了為每個 Pod 配置的 PersistentVolume。

volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

StatefulSet 控制器為 StatefulSet 中的每個 Pod 產生一個 PersistentVolumeClaim

使用以下命令取得 StatefulSetPersistentVolumeClaims

kubectl get pvc -l app=zk

StatefulSet 重新建立其 Pod 時,它會重新掛載 Pod 的 PersistentVolume。

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
datadir-zk-0   Bound     pvc-bed742cd-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-1   Bound     pvc-bedd27d2-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-2   Bound     pvc-bee0817e-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h

StatefulSet 的容器 templatevolumeMounts 區段將 PersistentVolume 掛載到 ZooKeeper 伺服器的資料目錄中。

volumeMounts:
- name: datadir
  mountPath: /var/lib/zookeeper

zk StatefulSet 中的 Pod 被(重新)排程時,它將始終將相同的 PersistentVolume 掛載到 ZooKeeper 伺服器的資料目錄。即使 Pod 被重新排程,對 ZooKeeper 伺服器的 WAL 進行的所有寫入及其所有快照都保持持久性。

確保組態一致

促進領導者選舉達成共識章節所述,ZooKeeper 集群中的伺服器需要一致的組態才能選出領導者並形成仲裁。它們還需要 Zab 協定的一致組態,以便協定在網路上正常運作。在我們的範例中,我們透過將組態直接嵌入到 manifest 中來實現一致的組態。

取得 zk StatefulSet。

kubectl get sts zk -o yaml
…
command:
      - sh
      - -c
      - "start-zookeeper \
        --servers=3 \
        --data_dir=/var/lib/zookeeper/data \
        --data_log_dir=/var/lib/zookeeper/data/log \
        --conf_dir=/opt/zookeeper/conf \
        --client_port=2181 \
        --election_port=3888 \
        --server_port=2888 \
        --tick_time=2000 \
        --init_limit=10 \
        --sync_limit=5 \
        --heap=512M \
        --max_client_cnxns=60 \
        --snap_retain_count=3 \
        --purge_interval=12 \
        --max_session_timeout=40000 \
        --min_session_timeout=4000 \
        --log_level=INFO"
…

用於啟動 ZooKeeper 伺服器的命令將組態作為命令列參數傳遞。您也可以使用環境變數將組態傳遞給集群。

設定日誌記錄

zkGenConfig.sh 腳本產生的檔案之一控制 ZooKeeper 的日誌記錄。ZooKeeper 使用 Log4j,並且預設情況下,它對其日誌記錄組態使用基於時間和大小的滾動檔案附加器。

使用以下命令從 zk StatefulSet 中的其中一個 Pod 取得日誌記錄組態。

kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties

以下日誌記錄組態將導致 ZooKeeper 程序將其所有日誌寫入標準輸出檔案流。

zookeeper.root.logger=CONSOLE
zookeeper.console.threshold=INFO
log4j.rootLogger=${zookeeper.root.logger}
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n

這是容器內部安全記錄日誌的最簡單方法。由於應用程式將日誌寫入標準輸出,Kubernetes 將為您處理日誌輪換。Kubernetes 還實作了合理的保留策略,以確保寫入標準輸出和標準錯誤的應用程式日誌不會耗盡本機儲存媒體。

使用 kubectl logs 從其中一個 Pod 檢索最後 20 行日誌。

kubectl logs zk-0 --tail 20

您可以使用 kubectl logs 和 Kubernetes Dashboard 來檢視寫入標準輸出或標準錯誤的應用程式日誌。

2016-12-06 19:34:16,236 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740
2016-12-06 19:34:16,237 [myid:1] - INFO  [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client)
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749
2016-12-06 19:34:26,156 [myid:1] - INFO  [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client)
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750
2016-12-06 19:34:26,226 [myid:1] - INFO  [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client)
2016-12-06 19:34:36,151 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client)
2016-12-06 19:34:36,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client)
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client)
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client)

Kubernetes 與許多日誌記錄解決方案整合。您可以選擇最適合您的集群和應用程式的日誌記錄解決方案。對於集群層級的日誌記錄和彙總,請考慮部署 sidecar 容器 來輪換和傳輸您的日誌。

設定非特權使用者

允許應用程式在容器內以特權使用者身分執行的最佳實務做法是一個有爭議的問題。如果您的組織要求應用程式以非特權使用者身分執行,您可以使用 SecurityContext 來控制作為進入點執行的使用者。

zk StatefulSet 的 Pod template 包含一個 SecurityContext

securityContext:
  runAsUser: 1000
  fsGroup: 1000

在 Pod 的容器中,UID 1000 對應於 zookeeper 使用者,GID 1000 對應於 zookeeper 群組。

zk-0 Pod 取得 ZooKeeper 程序資訊。

kubectl exec zk-0 -- ps -elf

由於 securityContext 物件的 runAsUser 欄位設定為 1000,因此 ZooKeeper 程序以 zookeeper 使用者身分執行,而不是以 root 身分執行。

F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S zookeep+     1     0  0  80   0 -  1127 -      20:46 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
0 S zookeep+    27     1  0  80   0 - 1155556 -    20:46 ?        00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

預設情況下,當 Pod 的 PersistentVolume 掛載到 ZooKeeper 伺服器的資料目錄時,只有 root 使用者可以存取它。此組態會阻止 ZooKeeper 程序寫入其 WAL 並儲存其快照。

使用以下命令取得 zk-0 Pod 上 ZooKeeper 資料目錄的檔案權限。

kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data

由於 securityContext 物件的 fsGroup 欄位設定為 1000,因此 Pod 的 PersistentVolume 的所有權設定為 zookeeper 群組,並且 ZooKeeper 程序能夠讀取和寫入其資料。

drwxr-sr-x 3 zookeeper zookeeper 4096 Dec  5 20:45 /var/lib/zookeeper/data

管理 ZooKeeper 程序

ZooKeeper 文件提到「您會希望有一個監管程序來管理您的每個 ZooKeeper 伺服器程序 (JVM)。」在分散式系統中,利用監控程式(監管程序)來重新啟動失敗的程序是一種常見模式。在 Kubernetes 中部署應用程式時,您應該使用 Kubernetes 作為應用程式的監控程式,而不是使用外部公用程式作為監管程序。

更新集群

zk StatefulSet 配置為使用 RollingUpdate 更新策略。

您可以使用 kubectl patch 來更新分配給伺服器的 cpus 數量。

kubectl patch sts zk --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'
statefulset.apps/zk patched

使用 kubectl rollout status 來觀察更新的狀態。

kubectl rollout status sts/zk
waiting for statefulset rolling update to complete 0 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 1 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 2 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
statefulset rolling update complete 3 pods at revision zk-5db4499664...

這會以相反的序數順序一次終止一個 Pod,並使用新的組態重新建立它們。這確保了在滾動更新期間保持仲裁。

使用 kubectl rollout history 命令來檢視歷史記錄或先前的組態。

kubectl rollout history sts/zk

輸出與此類似

statefulsets "zk"
REVISION
1
2

使用 kubectl rollout undo 命令來還原修改。

kubectl rollout undo sts/zk

輸出與此類似

statefulset.apps/zk rolled back

處理程序失敗

重新啟動策略控制 Kubernetes 如何處理 Pod 中容器進入點的程序失敗。對於 StatefulSet 中的 Pod,唯一適當的 RestartPolicy 是 Always,這是預設值。對於具狀態應用程式,您應該**永遠不要**覆寫預設策略。

使用以下命令來檢查在 zk-0 Pod 中執行的 ZooKeeper 伺服器的程序樹狀結構。

kubectl exec zk-0 -- ps -ef

用作容器進入點的命令具有 PID 1,而 ZooKeeper 程序(進入點的子程序)具有 PID 27。

UID        PID  PPID  C STIME TTY          TIME CMD
zookeep+     1     0  0 15:03 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
zookeep+    27     1  0 15:03 ?        00:00:03 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg

在另一個終端機中,使用以下命令觀察 zk StatefulSet 中的 Pod。

kubectl get pod -w -l app=zk

在另一個終端機中,使用以下命令終止 Pod zk-0 中的 ZooKeeper 程序。

kubectl exec zk-0 -- pkill java

ZooKeeper 程序的終止導致其父程序終止。由於容器的 RestartPolicy 是 Always,因此它重新啟動了父程序。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          21m
zk-1      1/1       Running   0          20m
zk-2      1/1       Running   0          19m
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Error     0          29m
zk-0      0/1       Running   1         29m
zk-0      1/1       Running   1         29m

如果您的應用程式使用腳本(例如 zkServer.sh)來啟動實作應用程式業務邏輯的程序,則腳本必須與子程序一起終止。這確保了當實作應用程式業務邏輯的程序失敗時,Kubernetes 將重新啟動應用程式的容器。

測試活性

僅僅配置您的應用程式以重新啟動失敗的程序不足以保持分散式系統的健康。在某些情況下,系統的程序可能既處於活動狀態又無回應,或其他方面不健康。您應該使用活性探針來通知 Kubernetes 您的應用程式的程序不健康,並且應該重新啟動它們。

zk StatefulSet 的 Pod template 指定了一個活性探針。

  livenessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

探針呼叫一個 bash 腳本,該腳本使用 ZooKeeper ruok 四字母單字來測試伺服器的健康狀況。

OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK" == "imok" ]; then
    exit 0
else
    exit 1
fi

在一個終端機視窗中,使用以下命令來觀察 zk StatefulSet 中的 Pod。

kubectl get pod -w -l app=zk

在另一個視窗中,使用以下命令從 Pod zk-0 的檔案系統中刪除 zookeeper-ready 腳本。

kubectl exec zk-0 -- rm /opt/zookeeper/bin/zookeeper-ready

當 ZooKeeper 程序的活性探針失敗時,Kubernetes 將自動為您重新啟動程序,確保集群中不健康的程序被重新啟動。

kubectl get pod -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Running   0          1h
zk-0      0/1       Running   1         1h
zk-0      1/1       Running   1         1h

測試就緒性

就緒性與活性不同。如果程序處於活動狀態,則表示已排程且健康。如果程序已就緒,則表示它能夠處理輸入。活性是就緒性的必要條件,但不是充分條件。在某些情況下,尤其是在初始化和終止期間,程序可能處於活動狀態但不就緒。

如果您指定就緒性探針,Kubernetes 將確保您的應用程式的程序在它們的就緒性檢查通過之前不會接收網路流量。

對於 ZooKeeper 伺服器,活性意味著就緒性。因此,zookeeper.yaml manifest 中的就緒性探針與活性探針相同。

  readinessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

即使活性探針和就緒性探針相同,同時指定兩者也很重要。這確保了只有 ZooKeeper 集群中健康的伺服器才能接收網路流量。

容忍節點故障

ZooKeeper 需要仲裁數量的伺服器才能成功地將變更提交到資料。對於由三個伺服器組成的集群,必須有兩個伺服器處於健康狀態,寫入才能成功。在基於仲裁的系統中,成員部署在不同的故障網域中以確保可用性。為了避免因單個機器的丟失而導致服務中斷,最佳實務做法是避免在同一台機器上共置多個應用程式實例。

預設情況下,Kubernetes 可能會將 StatefulSet 中的 Pod 共置在同一個節點上。對於您建立的三伺服器集群,如果兩個伺服器位於同一個節點上,並且該節點發生故障,則您的 ZooKeeper 服務的用戶端將遇到服務中斷,直到至少一個 Pod 可以重新排程。

您應該始終配置額外的容量,以便在節點故障時重新排程關鍵系統的程序。如果您這樣做,則服務中斷只會持續到 Kubernetes 排程器重新排程其中一個 ZooKeeper 伺服器為止。但是,如果您希望您的服務能夠容忍節點故障而不會停機,則應設定 podAntiAffinity

使用以下命令取得 zk StatefulSet 中 Pod 的節點。

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk StatefulSet 中的所有 Pod 都部署在不同的節點上。

kubernetes-node-cxpk
kubernetes-node-a5aq
kubernetes-node-2g2d

這是因為 zk StatefulSet 中的 Pod 指定了 PodAntiAffinity

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: "app"
              operator: In
              values:
                - zk
        topologyKey: "kubernetes.io/hostname"

requiredDuringSchedulingIgnoredDuringExecution 欄位告訴 Kubernetes 排程器,它永遠不應該在 topologyKey 定義的網域中共置兩個具有 app 標籤為 zk 的 Pod。topologyKey kubernetes.io/hostname 表示網域是單個節點。使用不同的規則、標籤和選擇器,您可以擴展此技術以將您的集群分散到物理、網路和電源故障網域。

在維護期間倖存

在本節中,您將封鎖和清空節點。如果您在共用集群上使用本教學課程,請確保這不會對其他租戶產生不利影響。

上一節向您展示了如何將您的 Pod 分散到多個節點以在計劃外的節點故障中倖存下來,但您還需要為計劃維護導致的暫時性節點故障做好準備。

使用此命令取得集群中的節點。

kubectl get nodes

本教學課程假設集群至少有四個節點。如果集群有超過四個節點,請使用 kubectl cordon 來封鎖除四個節點之外的所有節點。限制為四個節點將確保 Kubernetes 在以下維護模擬中排程 zookeeper Pod 時遇到親和性和 PodDisruptionBudget 約束。

kubectl cordon <node-name>

使用此命令取得 zk-pdb PodDisruptionBudget

kubectl get pdb zk-pdb

max-unavailable 欄位向 Kubernetes 指示,zk StatefulSet 中最多只能有一個 Pod 在任何時間都不可用。

NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-pdb    N/A             1                 1

在一個終端機中,使用此命令觀察 zk StatefulSet 中的 Pod。

kubectl get pods -w -l app=zk

在另一個終端機中,使用此命令取得目前已排程 Pod 的節點。

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

輸出與此類似

kubernetes-node-pb41
kubernetes-node-ixsl
kubernetes-node-i4c4

使用 kubectl drain 來封鎖和清空排程 zk-0 Pod 的節點。

kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

輸出與此類似

node "kubernetes-node-pb41" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-pb41, kube-proxy-kubernetes-node-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz
pod "zk-0" deleted
node "kubernetes-node-pb41" drained

由於您的集群中有四個節點,kubectl drain 成功,並且 zk-0 被重新排程到另一個節點。

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m

繼續在第一個終端機中觀察 StatefulSet 的 Pod,並清空排程 zk-1 的節點。

kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

輸出與此類似

"kubernetes-node-ixsl" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-ixsl, kube-proxy-kubernetes-node-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74
pod "zk-1" deleted
node "kubernetes-node-ixsl" drained

zk-1 Pod 無法排程,因為 zk StatefulSet 包含一個 PodAntiAffinity 規則,該規則阻止 Pod 的共置,並且由於只有兩個節點可排程,因此 Pod 將保持 Pending 狀態。

kubectl get pods -w -l app=zk

輸出與此類似

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s

繼續觀察 StatefulSet 的 Pod,並清空排程 zk-2 的節點。

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

輸出與此類似

node "kubernetes-node-i4c4" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2

使用 CTRL-C 終止 kubectl。

您無法清空第三個節點,因為驅逐 zk-2 會違反 zk-budget。但是,該節點將保持封鎖狀態。

使用 zkCli.shzk-0 檢索您在健全性測試期間輸入的值。

kubectl exec zk-0 zkCli.sh get /hello

該服務仍然可用,因為它的 PodDisruptionBudget 受到尊重。

WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x200000002
ctime = Wed Dec 07 00:08:59 UTC 2016
mZxid = 0x200000002
mtime = Wed Dec 07 00:08:59 UTC 2016
pZxid = 0x200000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

使用 kubectl uncordon 取消封鎖第一個節點。

kubectl uncordon kubernetes-node-pb41

輸出與此類似

node "kubernetes-node-pb41" uncordoned

zk-1 已在此節點上重新排程。等待直到 zk-1 處於 Running 和 Ready 狀態。

kubectl get pods -w -l app=zk

輸出與此類似

NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         12m
zk-1      0/1       ContainerCreating   0         12m
zk-1      0/1       Running   0         13m
zk-1      1/1       Running   0         13m

嘗試清空排程 zk-2 的節點。

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

輸出與此類似

node "kubernetes-node-i4c4" already cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
pod "heapster-v1.2.0-2604621511-wht1r" deleted
pod "zk-2" deleted
node "kubernetes-node-i4c4" drained

這次 kubectl drain 成功。

取消封鎖第二個節點以允許 zk-2 重新排程。

kubectl uncordon kubernetes-node-ixsl

輸出與此類似

node "kubernetes-node-ixsl" uncordoned

您可以結合使用 kubectl drainPodDisruptionBudgets,以確保您的服務在維護期間保持可用。如果在節點離線進行維護之前使用 drain 來封鎖節點並驅逐 Pod,則表達了中斷預算的服務將受到尊重。您應該始終為關鍵服務分配額外容量,以便可以立即重新排程其 Pod。

清理

  • 使用 kubectl uncordon 取消封鎖集群中的所有節點。
  • 您必須刪除本教學課程中使用的 PersistentVolume 的持久儲存媒體。根據您的環境、儲存組態和佈建方法,遵循必要的步驟,以確保回收所有儲存空間。
上次修改時間:2023 年 12 月 18 日 凌晨 2:39 PST:Update zookeeper.md (baa9da8695)