有状态服务
Zookeeper

Running ZooKeeper, A Distributed System Coordinator

本教程展示了如何在 Kubernetes 上使用有状态集(StatefulSets)、 Pod 中断预算(PodDisruptionBudgets)和 Pod 反亲和性(PodAntiAffinity)来运行 Apache Zookeeper。

Before you begin

在开始本教程之前,你应该熟悉以下 Kubernetes 概念:

- Pods
- Cluster DNS
- Headless Services
- PersistentVolumes
- PersistentVolume Provisioning
- StatefulSets
- PodDisruptionBudgets
- PodAntiAffinity
- kubectl CLI

你必须拥有一个至少包含四个节点的集群,并且每个节点至少需要 2 个 CPU 和 4 GiB 的内存。在本教程中,你将对集群的节点进行隔离(cordon)和排空(drain)操作。这意味着集群将终止并驱逐其节点上的所有 Pod,并且这些节点将暂时变得不可调度。你应该为这个教程使用一个专用的集群,或者确保你所造成的干扰不会影响其他租户。

本教程假定你已将集群配置为动态调配持久卷(PersistentVolumes)。如果你的集群未配置成这样,那么在开始本教程之前,你将不得不手动调配三个 20 GiB 的卷。

Objectives

在完成本教程后,你将了解以下内容:

    如何使用有状态集(StatefulSet)部署一个 ZooKeeper 集群。
    如何一致地配置该 ZooKeeper 集群。
    如何在 ZooKeeper 集群中分散部署 ZooKeeper 服务器。
    如何使用 Pod 中断预算(PodDisruptionBudgets)来确保在计划内维护期间的服务可用性。

ZooKeeper

Apache ZooKeeper 是一种用于分布式应用程序的分布式开源协调服务。ZooKeeper 允许你读取、写入和观察数据更新。数据以类似文件系统的层次结构进行组织,并复制到集群(一组 ZooKeeper 服务器)中的所有 ZooKeeper 服务器上。对数据的所有操作都是原子性的,并且具有顺序一致性。ZooKeeper 通过使用 Zab 共识协议在集群中的所有服务器上复制状态机来确保这一点。

集群使用 Zab 协议选举出一个领导者,并且在选举完成之前,集群无法写入数据。一旦选举完成,集群会使用 Zab 协议来确保在确认写入并使数据对客户端可见之前,将所有写入操作复制到法定人数的服务器上。在不考虑加权法定人数的情况下,法定人数是包含当前领导者的集群中的多数组成部分。例如,如果集群有三台服务器,包含领导者和另一台服务器的组成部分就构成了法定人数。如果集群无法达到法定人数,那么集群就无法写入数据。

ZooKeeper 服务器将其整个状态机保存在内存中,并将每一次状态变更写入存储介质上的持久化预写日志(Write Ahead Log,WAL)中。当服务器崩溃时,它可以通过重放 WAL 来恢复之前的状态。为了防止 WAL 无限制地增长,ZooKeeper 服务器会定期将其内存中的状态快照保存到存储介质上。这些快照可以直接加载到内存中,并且在快照之前的所有 WAL 记录都可以被丢弃。

Creating a ZooKeeper ensemble

下面的清单文件中包含了一个无头服务(Headless Service)、一个普通服务(Service)、一个 Pod 中断预算(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 Pod 中断预算以及 zk 有状态集。

service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created

使用 kubectl get 命令来观察有状态集控制器创建有状态集的 Pod。具体来说,你可以使用如下命令持续查看 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

有状态集控制器会创建三个 Pod,并且每个 Pod 都包含一个运行着 ZooKeeper 服务器的容器。

Facilitating leader election

由于在匿名网络中没有用于选举领导者的终止算法,Zab 协议需要显式的成员配置来进行领导者选举。集群中的每台服务器都需要有一个唯一的标识符,所有服务器都需要知道全局的标识符集合,并且每个标识符都需要与一个网络地址相关联。

使用 kubectl exec 命令来获取 zk 有状态集中各个 Pod 的主机名。

for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

有状态集控制器会根据每个 Pod 的序号索引为其分配一个唯一的主机名。这些主机名的格式为 <有状态集名称>-<序号索引>。由于 zk 有状态集的 replicas 字段被设置为 3,因此该有状态集控制器会创建三个 Pod,它们的主机名分别为 zk-0、zk-1 和 zk-2。

zk-0
zk-1
zk-2

ZooKeeper 集群中的服务器使用自然数作为唯一标识符,并将每台服务器的标识符存储在服务器数据目录中一个名为 myid 的文件里。

要查看每台服务器的 myid 文件内容,请使用以下命令。

for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

由于这些标识符是自然数,而序号索引是非负整数,你可以通过将序号加 1 来生成一个标识符。

myid zk-0
1
myid zk-1
2
myid zk-2
3

要获取 zk 有状态集中每个 Pod 的完全限定域名(FQDN),请使用以下命令。

for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk - hs 服务为所有 Pod 创建了一个域名:zk - hs.default.svc.cluster.local。

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

Kubernetes DNS 中的 A 记录会将完全限定域名(FQDN)解析为 Pod 的 IP 地址。如果 Kubernetes 重新调度 Pod,它会用 Pod 的新 IP 地址更新 A 记录,但 A 记录的名称不会改变。

ZooKeeper 将其应用配置存储在一个名为 zoo.cfg 的文件中。使用 kubectl exec 命令查看 zk - 0 Pod 中 zoo.cfg 文件的内容。

kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

在该文件底部的 server.1、server.2 和 server.3 属性中,数字 1、2 和 3 对应于 ZooKeeper 服务器的 myid 文件中的标识符。它们被设置为 zk 有状态集中各个 Pod 的完全限定域名(FQDN)。

clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

Achieving consensus

共识协议要求每个参与者的标识符必须是唯一的。在 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 文件中的 server 属性所代表的集群是经过正确配置的。

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 处于运行且就绪状态),要么无法达成此目的(若上述任一条件不满足)。不会出现一台服务器代表另一台服务器确认写入操作的情况。

Sanity testing the ensemble

最基本的健全性测试是向一台 ZooKeeper 服务器写入数据,然后从另一台服务器读取该数据。

下面的命令会执行 zkCli.sh 脚本,在集群中的 zk - 0 Pod 上,向路径 /hello 写入 world 数据。

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

Providing durable storage

正如在 ZooKeeper 基础部分所提到的,ZooKeeper 会将所有条目提交到持久化的预写式日志(WAL)中,并定期将内存状态的快照写入存储介质。对于使用共识协议来实现复制状态机的应用程序而言,使用预写式日志来保证持久性是一种常用的技术。

现在可以使用 kubectl delete 命令来删除 zk 有状态集,命令如下:

kubectl delete statefulset zk
statefulset.apps "zk" deleted

要观察有状态集中 Pod 的终止情况,可以使用以下命令:

kubectl get pods -w -l app=zk

当 zk-0 Pod 完全终止后,按下 Ctrl+C 组合键来终止 kubectl 命令的执行。

zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m

若要重新应用 zookeeper.yaml 中的清单文件

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

这会创建 zk 有状态集对象,但是清单文件中的其他 API 对象不会被修改,因为它们已经存在了。

要观察有状态集控制器重新创建该有状态集的 Pod,可以使用以下命令:

kubectl get pods -w -l app=zk

一旦 zk-2 Pod 处于 “运行(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 有状态集中的所有 Pod,该 ZooKeeper 集群仍然能够提供最初设置的值。

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 有状态集的 spec 中的 volumeClaimTemplates 字段指定了为每个 Pod 配置的持久卷声明。

volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

有状态集控制器会为有状态集中的每个 Pod 生成一个持久卷声明(PersistentVolumeClaim)。

要获取该有状态集的持久卷声明,可以使用以下命令:

kubectl get pvc -l app=zk

当有状态集重新创建其 Pod 时,它会重新挂载这些 Pod 的持久卷。

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
datadir-zk-0   Bound     pvc-bed742cd-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-1   Bound     pvc-bedd27d2-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-2   Bound     pvc-bee0817e-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h

有状态集的容器模板中的 volumeMounts 部分会将持久卷挂载到 ZooKeeper 服务器的数据目录中。

volumeMounts:
- name: datadir
  mountPath: /var/lib/zookeeper

当 zk 有状态集中的一个 Pod 被(重新)调度时,它总会将相同的持久卷挂载到 ZooKeeper 服务器的数据目录。即使 Pod 被重新调度,所有对 ZooKeeper 服务器的预写式日志(WALs)的写入以及所有的快照都将保持持久性。

Ensuring consistent configuration

如 “促进领导者选举” 和 “达成共识” 部分所述,ZooKeeper 集群中的服务器需要一致的配置才能选举出领导者并形成法定人数。为了让 Zab 协议在网络上正常工作,它们还需要对该协议进行一致的配置。在我们的示例中,我们通过将配置直接嵌入清单文件来实现一致的配置。

要获取 zk 有状态集,可以使用以下命令:

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 服务器的命令将配置作为命令行参数传递。你也可以使用环境变量来向集群传递配置信息。

Configuring logging

zkGenConfig.sh 脚本生成的文件中有一个用于控制 ZooKeeper 的日志记录。ZooKeeper 使用 Log4j,并且默认情况下,其日志配置采用基于时间和大小的滚动文件追加器。

若要从 zk 有状态集中的某个 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 还实施了合理的保留策略,以确保写入标准输出和标准错误的应用程序日志不会耗尽本地存储介质。

你可以使用以下命令从 zk 有状态集的其中一个 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 与许多日志记录解决方案相集成。你可以选择最适合你的集群和应用程序的日志记录解决方案。对于集群级别的日志记录和聚合,可考虑部署一个边车(sidecar)容器来进行日志轮转并传输日志。

Configuring a non-privileged user

允许应用程序在容器内以特权用户身份运行的最佳实践颇具争议。若你的组织要求应用程序以非特权用户身份运行,那么可以借助 SecurityContext 来控制入口点所使用的用户。

zk 有状态集的 Pod 模板包含了一个 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 的持久卷挂载到 ZooKeeper 服务器的数据目录时,只有根用户可以访问该目录。这种配置会阻止 ZooKeeper 进程写入其预写式日志(WAL)并存储其快照。

使用以下命令获取 zk - 0 Pod 上 ZooKeeper 数据目录的文件权限。

kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data

由于安全上下文(securityContext)对象的 fsGroup 字段被设置为 1000,Pod 的持久卷的所有权被设置给了 zookeeper 组,这样 ZooKeeper 进程就能够对其数据进行读写操作了。

drwxr-sr-x 3 zookeeper zookeeper 4096 Dec  5 20:45 /var/lib/zookeeper/data

Managing the ZooKeeper process

ZooKeeper 文档中提到:“你需要有一个监控进程来管理每个 ZooKeeper 服务器进程(JVM)”。在分布式系统中,利用看门狗(监控进程)来重启失败的进程是一种常见模式。当在 Kubernetes 中部署应用程序时,你应该使用 Kubernetes 作为应用程序的看门狗,而不是使用外部工具作为监控进程。

Updating the ensemble

zk 有状态集被配置为使用滚动更新(RollingUpdate)策略。

你可以使用 kubectl patch 命令来更新分配给服务器的 CPU 数量。

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

The output is similar to this:

statefulsets "zk"
REVISION
1
2

使用 kubectl rollout undo 命令来回滚所做的修改。

kubectl rollout undo sts/zk

输出内容如下类似:

statefulset.apps/zk rolled back

Handling process failure

重启策略控制着 Kubernetes 如何处理 Pod 中容器入口点的进程故障。对于有状态集中的 Pod,唯一合适的重启策略是 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 有状态集中的 Pod。

kubectl get pod -w -l app=zk

在另一个终端中,使用以下命令终止 zk-0 Pod 中的 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 会重新启动应用程序的容器。

Testing for liveness

仅将应用程序配置为重启失败的进程,并不足以维持分布式系统的健康状态。在某些情况下,系统进程可能处于运行状态但无响应,或者处于不健康状态。你应该使用存活探针(liveness probes)来告知 Kubernetes 你的应用程序进程处于不健康状态,以便让其重启这些进程。

zk 有状态集的 Pod 模板指定了一个存活探针。

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 有状态集中的 Pod。

kubectl get pod -w -l app=zk

在另一个窗口中,使用以下命令从 zk-0 Pod 的文件系统中删除 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

Testing for readiness

就绪(Readiness)和存活(Liveness)并非同一概念。如果一个进程处于存活状态,意味着它已被调度且处于健康状态。如果一个进程是就绪的,那就表示它能够处理输入。存活是就绪的必要条件,但并非充分条件。存在一些情况,尤其是在初始化和终止过程中,一个进程可能处于存活状态但尚未就绪。

如果你指定了一个就绪探针,那么在应用程序的进程通过就绪检查之前,Kubernetes 将确保这些进程不会接收到网络流量。

对于 ZooKeeper 服务器而言,存活意味着就绪。因此,zookeeper.yaml 清单中的就绪探针与存活探针是相同的。

  readinessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5

尽管存活探针和就绪探针是相同的,但同时指定两者仍然很重要。这能确保只有 ZooKeeper 集群中健康的服务器才会接收到网络流量。

Tolerating Node failure

ZooKeeper 需要达到法定数量的服务器才能成功提交对数据的变更。对于一个由三台服务器组成的集群,必须有两台服务器处于健康状态,写入操作才能成功。在基于法定人数的系统中,成员会部署在不同的故障域中以确保可用性。为避免因单个机器故障而导致系统中断,最佳实践是避免在同一台机器上共置应用程序的多个实例。

默认情况下,Kubernetes 可能会将有状态集中的 Pod 共置在同一节点上。对于你创建的三台服务器组成的集群,如果有两台服务器在同一节点上,并且该节点发生故障,那么你的 ZooKeeper 服务的客户端将经历中断,直到至少有一个 Pod 能够被重新调度。

你应该始终预留额外的容量,以便在节点发生故障时能够重新调度关键系统的进程。如果你这样做了,那么中断只会持续到 Kubernetes 调度器重新调度其中一台 ZooKeeper 服务器为止。然而,如果你希望你的服务能够容忍节点故障而不出现停机时间,你应该设置 podAntiAffinity(Pod 反亲和性)。

使用下面的命令获取 zk 有状态集中 Pod 所在的节点。

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk 有状态集中的所有 Pod 都被部署在了不同的节点上。

kubernetes-node-cxpk
kubernetes-node-a5aq
kubernetes-node-2g2d

这是因为 zk 有状态集中的 Pod 指定了 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 表明该域是单个节点。通过使用不同的规则、标签和选择器,你可以扩展这种技术,以便将你的集群分布在物理、网络和电源故障域中。

Surviving maintenance

在本节中,你将对节点进行隔离(cordon)和排空(drain)操作。如果你在共享集群上使用本教程,请务必确保这不会对其他租户产生不利影响。

上一节向你展示了如何将 Pod 分布在各个节点上,以便在发生意外的节点故障时仍能正常运行,但你还需要针对因计划内维护而导致的临时节点故障做好规划。

使用此命令获取你集群中的节点。

kubectl get nodes

本教程假定集群中至少有四个节点。如果集群的节点数超过四个,请使用 kubectl cordon 命令将除四个节点之外的所有节点都设置为隔离状态。将节点数量限制为四个,这将确保在接下来的维护模拟中,当 Kubernetes 调度 ZooKeeper Pod 时,会遇到亲和性和 Pod 中断预算(PodDisruptionBudget)方面的限制条件。

kubectl cordon <node-name>

使用此命令来获取 zk-pdb 的 Pod 中断预算(PodDisruptionBudget)。

kubectl get pdb zk-pdb

max-unavailable 字段向 Kubernetes 表明,在任何时候,zk 有状态集中最多只能有一个 Pod 处于不可用状态。

NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-pdb    N/A             1                 1

在一个终端中,使用此命令来监控 zk 有状态集中的 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 的节点进行隔离(cordon)和排空(drain)操作,你可以按照以下步骤进行。

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 Pod 被重新调度到了另一个节点上。

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 Pod 所调度到的节点执行排空(drain)操作。

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 有状态集包含了一个 Pod 反亲和性规则,该规则会阻止 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

继续监控有状态集的 Pod,同时对 zk-2 所在的节点执行排空操作。

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

输出内容如下类似:

node "kubernetes-node-i4c4" cordoned

WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2

使用 CTRL-C 终止 kubectl。

你无法排空第三个节点,因为驱逐 zk-2 将违反 zk-budget。不过,该节点仍将处于隔离状态。

使用 zkCli.sh 从 zk-0 中检索你在健全性测试期间输入的值。

kubectl exec zk-0 zkCli.sh get /hello

该服务仍然可用,因为它的 Pod 中断预算(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 drain 与 Pod 中断预算(PodDisruptionBudgets)结合使用,以确保你的服务在维护期间仍然可用。如果在将节点离线进行维护之前,使用 drain 命令来隔离节点并驱逐 Pod,那么设置了中断预算的服务,其预算将会得到遵守。对于关键服务,你始终应该预留额外的资源容量,这样其 Pod 就能立即被重新调度。

Cleaning up

- 使用 kubectl uncordon 命令来解除集群中所有节点的隔离状态。
- 你必须删除本教程中所使用的持久卷(PersistentVolumes)对应的持久存储介质。根据你的环境、存储配置以及配置供应方法,遵循必要的步骤,以确保所有存储都能被回收。