執行 ZooKeeper,一個分散式系統協調器
本教程演示如何使用 StatefulSet、PodDisruptionBudget 和 PodAntiAffinity 在 Kubernetes 上執行 Apache ZooKeeper。
準備工作
在開始本教程之前,你應該熟悉以下 Kubernetes 概念:
- Pod
- 叢集 DNS
- 無頭 Service
- PersistentVolumes
- PersistentVolume 動態配置
- 有狀態副本集
- PodDisruptionBudget
- PodAntiAffinity
- kubectl CLI
你的叢集必須至少有四個節點,每個節點至少需要 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-0
、zk-1
和 zk-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.1
、server.2
和 server.3
屬性中,1
、2
和 3
對應於 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 的 spec
的 volumeClaimTemplates
欄位指定為每個 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
。
使用以下命令獲取 StatefulSet
的 PersistentVolumeClaims
。
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
容器 template
的 volumeMounts
部分將 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.sh
從 zk-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 drain
與 PodDisruptionBudgets
結合使用,以確保您的服務在維護期間保持可用。如果在節點離線維護之前使用 drain 命令隔離節點並驅逐 Pod,則表示具有中斷預算的服務將遵守該預算。您應該始終為關鍵服務分配額外容量,以便它們的 Pod 可以立即重新排程。
清理
- 使用
kubectl uncordon
解除叢集中所有節點的封鎖。 - 您必須刪除本教程中使用的 PersistentVolumes 的持久儲存介質。根據您的環境、儲存配置和配置方法,執行必要的步驟,以確保所有儲存都得到回收。