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

本教程演示如何使用 StatefulSetPodDisruptionBudgetPodAntiAffinity 在 Kubernetes 上執行 Apache ZooKeeper

準備工作

在開始本教程之前,你應該熟悉以下 Kubernetes 概念:

你的叢集必須至少有四個節點,每個節點至少需要 2 個 CPU 和 4 GiB 記憶體。在本教程中,你將隔離並騰空叢集節點。 **這意味著叢集將終止並驅逐其節點上的所有 Pod,並且這些節點將暫時無法排程。** 你應該為本教程使用一個專用叢集,或者你應該確保你造成的干擾不會影響其他租戶。

本教程假設你已將叢集配置為動態配置 PersistentVolumes。如果你的叢集未配置為這樣做,你將必須在本教程開始之前手動配置三個 20 GiB 卷。

目標

完成本教程後,你將瞭解以下內容:

  • 如何使用 StatefulSet 部署 ZooKeeper 叢集。
  • 如何一致地配置叢集。
  • 如何在叢集中分散 ZooKeeper 伺服器的部署。
  • 如何在計劃維護期間使用 PodDisruptionBudget 確保服務可用性。

ZooKeeper

Apache ZooKeeper 是一個分散式、開源的分散式應用程式協調服務。ZooKeeper 允許你讀取、寫入和觀察資料更新。資料以檔案系統一樣的層次結構組織,並複製到叢集(一組 ZooKeeper 伺服器)中的所有 ZooKeeper 伺服器。所有資料操作都是原子和順序一致的。ZooKeeper 透過使用 Zab 共識協議在叢集中的所有伺服器上覆制狀態機來確保這一點。

叢集使用 Zab 協議選舉領導者,並且在選舉完成之前,叢集無法寫入資料。選舉完成後,叢集使用 Zab 協議,以確保在確認並使其對客戶端可見之前,將所有寫入複製到仲裁。不考慮加權仲裁,仲裁是包含當前領導者的叢集中的大多陣列件。例如,如果叢集有三臺伺服器,包含領導者和另一臺伺服器的元件構成一個仲裁。如果叢集無法達成仲裁,則叢集無法寫入資料。

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

建立 ZooKeeper 叢集

下面的清單包含一個無頭服務 (Headless Service)、一個服務 (Service)、一個PodDisruptionBudget 和一個StatefulSet

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 無頭服務、zk-cs 服務、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 正在執行並就緒,使用 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 名稱>-<序號索引> 的形式。由於 zk StatefulSet 的 replicas 欄位設定為 3,因此該集合的控制器建立了三個 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 變為就緒時輸入。因此,ZooKeeper 伺服器的 FQDN 將解析為單個端點,並且該端點將是唯一的 ZooKeeper 伺服器,聲稱其 myid 檔案中配置的身份。

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

這確保了 ZooKeepers 的 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 正在執行並就緒),要麼它們將失敗(如果上述任一條件未滿足)。不會出現一個伺服器代表另一個伺服器確認寫入的情況。

對叢集進行健全性測試

最基本的健全性測試是將資料寫入一個 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 中的清單。

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

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

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

kubectl get pods -w -l app=zk

一旦 zk-2 Pod 正在執行並就緒,使用 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 的 PersistentVolumes。

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 部分將 PersistentVolumes 掛載到 ZooKeeper 伺服器的資料目錄中。

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

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

確保一致配置

促進領導者選舉達成共識部分所述,ZooKeeper 叢集中的伺服器需要一致的配置才能選舉領導者並形成仲裁。它們還需要 Zab 協議的一致配置才能使協議在網路上正常工作。在我們的示例中,我們透過將配置直接嵌入到清單中來實現一致配置。

獲取 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 集成了許多日誌解決方案。您可以選擇最適合您的叢集和應用程式的日誌解決方案。對於叢集級日誌記錄和聚合,請考慮部署一個邊車容器來輪換和傳送日誌。

配置非特權使用者

關於允許應用程式在容器內作為特權使用者執行的最佳實踐尚存爭議。如果你的組織要求應用程式作為非特權使用者執行,你可以使用 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 的 PersistentVolumes 掛載到 ZooKeeper 伺服器的資料目錄時,它只能由 root 使用者訪問。此配置會阻止 ZooKeeper 程序寫入其 WAL 和儲存其快照。

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

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

由於 securityContext 物件的 fsGroup 欄位設定為 1000,因此 Pod 的 PersistentVolumes 的所有權被設定為 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 清單中的就緒探測與活性探測相同。

  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 時遇到親和性 (affinity) 和 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 執行並就緒。

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 解除叢集中所有節點的封鎖。
  • 您必須刪除本教程中使用的 PersistentVolumes 的持久儲存介質。根據您的環境、儲存配置和配置方法,執行必要的步驟,以確保所有儲存都得到回收。
上次修改於 2023 年 12 月 18 日太平洋標準時間上午 2:39:更新 zookeeper.md (baa9da8695)