运行 ZooKeeper,一个分布式系统协调器
本教程演示了使用 Apache Zookeeper 在 Kubernetes 上运行 有状态集、Pod 中断预算 和 Pod 反亲和性。
开始之前
在开始本教程之前,您应该熟悉以下 Kubernetes 概念
您必须拥有至少四个节点的集群,并且每个节点至少需要 2 个 CPU 和 4 GiB 的内存。在本教程中,您将隔离和排干集群的节点。这意味着集群将终止并驱逐其节点上的所有 Pod,并且节点将暂时变为不可调度。您应该为此教程使用专用集群,或者您应该确保您造成的破坏不会干扰其他租户。
本教程假设您已将集群配置为动态供应持久卷。如果您的集群未配置为这样做,则您必须在此教程开始之前手动供应三个 20 GiB 卷。
目标
完成本教程后,您将了解以下内容。
- 如何使用有状态集部署 ZooKeeper 集群。
- 如何一致地配置集群。
- 如何分散集群中 ZooKeeper 服务器的部署。
- 如何在计划维护期间使用 Pod 中断预算来确保服务可用性。
ZooKeeper
Apache ZooKeeper 是一种用于分布式应用程序的分布式开源协调服务。ZooKeeper 允许您读取、写入和观察数据的更新。数据以类似文件系统的层次结构进行组织,并复制到集群中的所有 ZooKeeper 服务器(一组 ZooKeeper 服务器)。对数据的任何操作都是原子且顺序一致的。ZooKeeper 通过使用 Zab 共识协议来确保这一点,该协议在集群中的所有服务器上复制状态机。
集群使用 Zab 协议来选举领导者,并且集群在选举完成之前无法写入数据。完成后,集群使用 Zab 来确保它在确认并使写入对客户端可见之前将所有写入复制到法定人数。不考虑加权法定人数,法定人数是包含当前领导者的集群中的多数组件。例如,如果集群有三个服务器,则包含领导者和另一个服务器的组件构成法定人数。如果集群无法达到法定人数,则集群无法写入数据。
ZooKeeper 服务器将其整个状态机保存在内存中,并将每个变异写入存储介质上的持久性 WAL(预写日志)。当服务器崩溃时,它可以通过重播 WAL 来恢复其先前状态。为了防止 WAL 无限增长,ZooKeeper 服务器会定期将其内存状态快照写入存储介质。这些快照可以直接加载到内存中,并且所有在快照之前的 WAL 条目都可以丢弃。
创建 ZooKeeper 集群
下面的清单包含一个 无头服务、一个 服务、一个 Pod 中断预算 和一个 有状态集。
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
Pod 中断预算和 zk
有状态集。
service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created
使用 kubectl get
来观察有状态集控制器创建有状态集的 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
有状态集控制器创建三个 Pod,每个 Pod 都有一个包含 ZooKeeper 服务器的容器。
促进领导者选举
由于在匿名网络中没有终止算法来选举领导者,因此 Zab 需要显式成员资格配置才能执行领导者选举。集群中的每个服务器都需要具有唯一的标识符,所有服务器都需要知道全局标识符集,并且每个标识符都需要与网络地址相关联。
使用 kubectl exec
获取 zk
有状态集中的 Pod 的主机名。
for i in 0 1 2; do kubectl exec zk-$i -- hostname; done
有状态集控制器为每个 Pod 提供基于其序数索引的唯一主机名。主机名采用 <statefulset name>-<ordinal index>
的形式。由于 zk
有状态集的 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
有状态集中的每个 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
有状态集中的 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 将解析为单个端点,并且该端点将是声明其 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
这确保了 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
有状态集。
kubectl delete statefulset zk
statefulset.apps "zk" deleted
观察有状态集中的 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
有状态集对象,但清单中的其他 API 对象不会修改,因为它们已经存在。
观察有状态集控制器重新创建有状态集的 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 预配的持久卷。
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 的持久卷。
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
部分将持久卷挂载到 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 仪表板查看写入标准输出或标准错误的应用程序日志。
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 进程不是以 root 身份运行,而是以 zookeeper 用户身份运行。
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 的持久卷挂载到 ZooKeeper 服务器的数据目录时,它只能被 root 用户访问。此配置阻止 ZooKeeper 进程写入其 WAL 并存储其快照。
使用以下命令获取zk-0
Pod 上 ZooKeeper 数据目录的文件权限。
kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data
由于securityContext
对象的fsGroup
字段设置为 1000,因此 Pod 的持久卷的所有权设置为 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 时遇到亲和性和 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
取消隔离集群中的所有节点。 - 您必须删除本教程中使用的 PersistentVolume 的持久存储介质。根据您的环境、存储配置和配置方法,执行必要的步骤,以确保所有存储都被回收。