运行 ZooKeeper,一个分布式系统协调器

本教程演示了如何使用 StatefulSetsPodDisruptionBudgetsPodAntiAffinity 在 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 处于运行状态且准备就绪,请使用 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-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 变为就绪状态时,会输入每个 Pod 的 A 记录。因此,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 协议尝试提交一个值时,它们要么达成共识并提交该值(如果 leader 选举成功且至少有两个 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 进行的所有写入及其所有快照都保持持久性。

确保配置一致

正如 促进 Leader 选举达成共识 部分所述,ZooKeeper 集群中的服务器需要一致的配置才能选举 Leader 并形成仲裁。它们还需要 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 与许多日志记录解决方案集成。您可以选择最适合您的集群和应用程序的日志记录解决方案。对于集群级日志记录和聚合,请考虑部署 sidecar 容器 来轮换和传输您的日志。

配置非特权用户

允许应用程序在容器内以特权用户身份运行的最佳实践是一个有争议的问题。如果您的组织要求应用程序以非特权用户身份运行,您可以使用 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 时遇到亲和性和 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 包含防止 Pod 同位放置的 PodAntiAffinity 规则,并且由于只有两个节点可调度,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 变为 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 成功执行。

取消隔离第二个节点,以允许重新调度 zk-2

kubectl uncordon kubernetes-node-ixsl

输出类似于此

node "kubernetes-node-ixsl" uncordoned

您可以将 kubectl drainPodDisruptionBudgets 结合使用,以确保您的服务在维护期间保持可用。如果在将节点脱机进行维护之前使用排空来隔离节点并驱逐 Pod,则会尊重表示中断预算的服务。您应始终为关键服务分配额外的容量,以便可以立即重新调度其 Pod。

清理

  • 使用 kubectl uncordon 取消隔离集群中的所有节点。
  • 您必须删除本教程中使用的 PersistentVolumes 的持久存储介质。根据您的环境、存储配置和配置方法,执行必要的步骤以确保回收所有存储空间。
上次修改时间:2023 年 12 月 18 日凌晨 2:39 PST:Update zookeeper.md (baa9da8695)