运行 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)