运行 ZooKeeper,一个分布式系统协调器
本教程演示如何使用 StatefulSet、PodDisruptionBudgets 和 PodAntiAffinity 在 Kubernetes 上运行 Apache Zookeeper。
准备开始
在开始本教程之前,你应该熟悉以下 Kubernetes 概念:
你必须拥有一个至少包含四个节点的集群,并且每个节点至少需要 2 个 CPU 和 4 GiB 内存。在本教程中,你将隔离并排空集群的节点。**这意味着集群将终止并驱逐其节点上的所有 Pod,并且节点将暂时变得不可调度。** 你应该为本教程使用专用集群,或者你应该确保你造成的中断不会干扰其他租户。
本教程假设你已将集群配置为动态供应持久卷。如果你的集群未配置为这样做,则必须在开始本教程之前手动供应三个 20 GiB 的卷。
目标
完成本教程后,你将了解以下内容。
- 如何使用 StatefulSet 部署 ZooKeeper 集群。
- 如何一致地配置集群。
- 如何在集群中分散 ZooKeeper 服务器的部署。
- 如何在计划内维护期间使用 PodDisruptionBudgets 来确保服务可用性。
ZooKeeper
Apache ZooKeeper 是一个用于分布式应用程序的分布式开源协调服务。ZooKeeper 允许你读取、写入和观察数据的更新。数据以类似于文件系统的层次结构组织,并复制到集群(一组 ZooKeeper 服务器)中的所有 ZooKeeper 服务器。所有数据操作都是原子且顺序一致的。ZooKeeper 通过使用 Zab 共识协议在集群中的所有服务器上复制状态机来确保这一点。
集群使用 Zab 协议来选举领导者,并且集群在完成选举之前无法写入数据。完成后,集群使用 Zab 来确保它将所有写入复制到仲裁,然后再确认并使其对客户端可见。不考虑加权仲裁,仲裁是包含当前领导者的集群的多数组件。例如,如果集群有三个服务器,则包含领导者和一个其他服务器的组件构成仲裁。如果集群无法达到仲裁,则集群无法写入数据。
ZooKeeper 服务器将其整个状态机保存在内存中,并将每次突变写入存储介质上的持久 WAL(预写日志)。当服务器崩溃时,它可以通过重播 WAL 来恢复其先前的状态。为了防止 WAL 无限制地增长,ZooKeeper 服务器会定期将其内存状态快照到存储介质。这些快照可以直接加载到内存中,并且可以丢弃快照之前的所有 WAL 条目。
创建 ZooKeeper 集群
下面的清单包含一个 无头服务、一个 服务、一个 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 处于 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 名称>-<序号索引>
。由于 zk
StatefulSet 的 replicas
字段设置为 3
,因此该 Set 的控制器创建三个 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 变为 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
中的清单。
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 处于 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 的 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 的 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
的容器 template
的 volumeMounts
部分将 PersistentVolume 挂载到 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 进程以 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) 的监管进程。”在分布式系统中使用 watchdog(监管进程)来重启失败的进程是一种常见模式。在 Kubernetes 中部署应用程序时,您应该使用 Kubernetes 作为应用程序的 watchdog,而不是使用外部实用程序作为监管进程。
更新集群
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
StatefulSet
中包含 PodAntiAffinity
规则,该规则会阻止 Pod 的共址,而且只有两个节点可以调度,所以 zk-1
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
。但是,该节点仍将保持 cordoned 状态。
使用 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
命令取消对第一个节点的 cordoned 状态。
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
命令会成功执行。
取消对第二个节点的 cordoned 状态,允许 zk-2
被重新调度。
kubectl uncordon kubernetes-node-ixsl
输出类似于此
node "kubernetes-node-ixsl" uncordoned
您可以将 kubectl drain
与 PodDisruptionBudgets
结合使用,以确保您的服务在维护期间仍然可用。如果使用 drain 命令在将节点离线进行维护之前 cordon 节点并驱逐 Pod,则会遵守声明了中断预算的服务的中断预算。您应该始终为关键服务分配额外的容量,以便它们的 Pod 可以立即被重新调度。
清理
- 使用
kubectl uncordon
命令取消对集群中所有节点的 cordoned 状态。 - 您必须删除本教程中使用的 PersistentVolumes 的持久化存储介质。根据您的环境、存储配置和配置方法,按照必要的步骤来确保所有存储都被回收。