运行ZooKeeper,一个分布式系统协调器
目标
在本教程之后,您将了解以下内容。
- 如何使用
StatefulSet
部署ZooKeeper集合
。 - 如何使用
ConfigMaps
一致地配置集合。 - 如何在集合中扩展
ZooKeeper
服务器的部署。 - 如何使用
PodDisruptionBudgets
确保计划维护期间的服务可用性。
创建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/v1beta1
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: Parallel
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: "k8s.gcr.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命令创建清单。
代码语言:javascript复制kubectl apply -f zookeeper.yaml
这将创建Headless Service为zk-hs
,Service为zk-cs
,PodDisruptionBudget是zk-pdb
,StatefulSet为zk
。
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
运行并准备就绪,使用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服务器的容器。
促进leader选举
由于在匿名网络中选择leader没有终止算法,因此Zab
需要显式成员资格配置来执行leader选举。集合中的每个服务器都需要具有唯一标识符,所有服务器都需要知道全局标识符集,并且每个标识符需要与网络地址相关联。
使用kubectl exec
获取zk StatefulSet
中Pod
的主机名。
for i in 0 1 2; do kubectl exec zk-$i -- hostname; done
StatefulSet
控制器根据其序数索引为每个Pod提供唯一的主机名。主机名采用<statefulset name> - <ordinal index>
的形式。由于zk StatefulSet的副本字段设置为3,因此Set的控制器创建三个Pod,其主机名设置为zk-0
,zk-1
和zk-2
。
zk-0
zk-1
zk-2
ZooKeeper集合中的服务器使用自然数作为唯一标识符,并将每个服务器的标识符存储在服务器数据目录中名为myid
的文件中。
要检查每个服务器的myid
文件的内容,请使用以下命令。
for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done
因为标识符是自然数,而序数索引是非负整数,所以可以通过向序数加1来生成标识符。
代码语言:javascript复制myid zk-0
1
myid zk-1
2
myid zk-2
3
要获取zk StatefulSet中每个Pod的完全限定域名(FQDN),请使用以下命令。
代码语言:javascript复制for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done
zk-hs
服务为所有Pod创建一个域,
zk-hs.default.svc.cluster.local.
zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local
Kubernetes DNS
中的A记录将FQDN解析为Pod的IP地址。如果Kubernetes重新调度Pod,它将使用Pod的新IP地址更新A记录,但A记录名称不会更改。
ZooKeeper将其应用程序配置存储在名为zoo.cfg
的文件中。使用kubectl exec
查看zk-0
Pod中zoo.cfg
文件的内容。
kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg
在文件底部的server.1
,server.2
和server.3
属性中,1
,2
和3
对应于ZooKeeper服务器的myid
文件中的标识符。它们被设置为zk
StatefulSet中Pod的FQDN。
clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888
达成共识
共识协议要求每个参与者的标识符是唯一的。Zab协议中没有两个参与者应该声明相同的唯一标识符。这对于允许系统中的进程就哪些进程提交了哪些数据达成一致是必要的。如果使用相同的序号启动两个Pod,则两个ZooKeeper服务器都将自己标识为同一服务器。
kubectl get pods -w -l app=zk
代码语言:javascript复制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
这可确保ZooKeepers的zoo.cfg文件中的服务器属性表示正确配置的集合。
代码语言:javascript复制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协议尝试提交value时,他们将达成共识并提交value(如果leader选举成功并且至少有两个Pod正在运行和就绪),或者他们将无法做到(如果不符合任何一个条件)。如果一个服务器代表另一个服务器确认写入,则不会出现任何状态。
综合测试
最基本的健全性测试是将数据写入一个ZooKeeper服务器并从另一个服务器读取数据。
The command below executes the zkCli.sh script to write world to the path /hello on the zk-0 Pod in the ensemble. 下面的命令执行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
获取数据。
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 Basics
部分所述,ZooKeeper将所有条目提交给持久的WAL,并定期将内存状态的快照写入存储介质。使用WAL来提供持久性是使用共识协议来实现复制状态机的应用程序的常用技术。
使用kubectl delete
删除zk StatefulSet
。
kubectl delete statefulset zk
statefulset.apps "zk" deleted
观察StatefulSet中Pod的终止。
代码语言:javascript复制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
重新applyzookeeper.yaml。
代码语言:javascript复制kubectl apply -f zookeeper.yaml
这将创建zk StatefulSet
对象,但清单中的其他API
对象不会被修改,因为它们已经存在。
观察StatefulSet
控制器重新创建StatefulSet
的Pod
。
kubectl get pods -w -l app=zk
Once the zk-2 Pod is Running and Ready, use CTRL-C to terminate 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规范的volumeClaimTemplates
字段指定为每个Pod配置的PersistentVolume
。
volumeClaimTemplates:
- metadata:
name: datadir
annotations:
volume.alpha.kubernetes.io/storage-class: anything
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 20Gi
StatefulSet控制器为StatefulSet中的每个Pod生成PersistentVolumeClaim
。
使用以下命令获取StatefulSet的PersistentVolumeClaims
。
kubectl get pvc -l app=zk
When the StatefulSet recreated its Pods, it remounts the Pods’ 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容器模板的volumeMounts部分在ZooKeeper服务器的数据目录中安装PersistentVolumes
。
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
当(重新)调度zk StatefulSet中的Pod时,它将始终具有安装到ZooKeeper服务器的数据目录的相同PersistentVolume
。即使重新调度Pod,对ZooKeeper服务器的WAL
及其所有快照的所有写入都保持持久。
确保一致的配置
如“促进leader选举和实现共识”部分所述,ZooKeeper集合中的服务器需要一致的配置来选举领导者并形成法定人数。它们还需要一致配置Zab协议,以使协议在网络上正常工作。在我们的示例中,我们通过将配置直接嵌入清单来实现一致的配置。
获取zk StatefulSet。
代码语言:javascript复制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获取日志记录配置。
代码语言:javascript复制kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties
下面的日志记录配置将导致ZooKeeper进程将其所有日志写入标准输出文件流。
代码语言:javascript复制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日志从其中一个Pod中检索最后20个日志行。
代码语言:javascript复制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 supports more powerful, but more complex, logging integrations with Stackdriver and Elasticsearch and Kibana. For cluster level log shipping and aggregation, consider deploying a sidecar container to rotate and ship your logs. Kubernetes支持与Stackdriver,Elasticsearch和Kibana进行更强大更复杂的日志记录集成。对于集群级日志传送和聚合,请考虑部署sidecar容器以轮询和发送日志。
配置非特权用户
允许应用程序作为特权用户在容器内运行的最佳实践是一个有争议的问题。如果你们要求应用程序作为非特权用户运行,则可以使用SecurityContext
来控制入口点运行的用户。
zk StatefulSet的Pod模板包含SecurityContext。
代码语言:javascript复制securityContext:
runAsUser: 1000
fsGroup: 1000
在Pods的容器中,UID 1000对应于zookeeper用户,GID 1000对应于zookeeper组。
从zk-0 Pod
获取ZooKeeper
进程信息。
kubectl exec zk-0 -- ps -elf
由于securityContext对象的runAsUser
字段设置为1000,而不是以root身份运行,因此ZooKeeper进程作为zookeeper用户运行。
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S zookeep 1 0 0 80 0 - 1127 - 20:46 ? 00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
0 S zookeep 27 1 0 80 0 - 1155556 - 20:46 ? 00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg
默认情况下,当Pod的PersistentVolumes挂载到ZooKeeper服务器的数据目录时,只有root用户才能访问它。此配置可防止ZooKeeper进程写入其WAL并存储其快照。
使用以下命令获取zk-0 Pod上ZooKeeper数据目录的文件权限。
代码语言:javascript复制kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data
由于securityContext对象的fsGroup
字段设置为1000,因此Pods的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
来更新分配给服务器的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
,并使用新配置重新创建它们。这可确保在滚动更新期间维护quorum
。
使用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 中的 Pods 来说,Always
是唯一合适的 RestartPolicy
,也是默认值。你不应该覆盖有状态应用的默认策略。
检查 zk-0
Pod 中运行的 ZooKeeper 服务器的进程树
kubectl exec zk-0 -- ps -ef
作为容器入口点的命令的 PID 为 1,Zookeeper 进程是入口点的子进程,PID 为 27。
代码语言:javascript复制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
中的 Pods。
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 一节指定了一个存活探针。
代码语言:javascript复制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 中的 Pods。
kubectl get pod -w -l app=zk
在另一个窗口中,从 Pod zk-0
的文件系统中删除 zookeeper-ready 脚本。
kubectl exec zk-0 -- rm /usr/bin/zookeeper-ready
When the liveness probe for the ZooKeeper process fails, Kubernetes will automatically restart the process for you, ensuring that unhealthy processes in the ensemble are restarted. 当 ZooKeeper 进程的存活探针探测失败时,Kubernetes 将会为你自动重启这个进程,从而保证 ensemble 中不健康状态的进程都被重启。
代码语言:javascript复制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
就绪性测试
就绪不同于存活。如果一个进程是存活的,它是可调度和健康的。如果一个进程是就绪的,它应该能够处理输入。存活是就绪的必要非充分条件。在许多场景下,特别是初始化和终止过程中,一个进程可以是存活但没有就绪的。
如果你指定了一个就绪探针,Kubernetes 将保证在就绪检查通过之前, 你的应用不会接收到网络流量。
对于一个 ZooKeeper 服务器来说,存活即就绪。因此 zookeeper.yaml
清单中的就绪探针和存活探针完全相同。
readinessProbe:
exec:
command:
- sh
- -c
- "zookeeper-ready 2181"
initialDelaySeconds: 15
timeoutSeconds: 5
虽然存活探针和就绪探针是相同的,但同时指定它们两者仍然重要。这保证了 ZooKeeper ensemble 中只有健康的服务器能接收网络流量。
容忍节点故障(Tolerating Node Failure)
ZooKeeper 需要一个 quorum 来提交数据变动。对于一个拥有 3 个服务器的 ensemble 来说, 必须有两个服务器是健康的,写入才能成功。在基于 quorum 的系统里,成员被部署在多个故障域中以保证可用性。为了防止由于某台机器断连引起服务中断,最佳实践是防止应用的多个实例在相同的机器上共存。
默认情况下,Kubernetes 可以把 StatefulSet 的 Pods 部署在相同节点上。对于你创建的 3 个服务器的 ensemble 来说,如果有两个服务器并存于 相同的节点上并且该节点发生故障时,ZooKeeper 服务将中断, 直至至少一个 Pods 被重新调度。
你应该总是提供多余的容量以允许关键系统进程在节点故障时能够被重新调度。如果你这样做了,服务故障就只会持续到 Kubernetes 调度器重新调度某个 ZooKeeper 服务器为止。但是,如果希望你的服务在容忍节点故障时无停服时间,你应该设置 podAntiAffinity。
获取 zk Stateful Set 中的 Pods 的节点。
代码语言:javascript复制for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done
zk
StatefulSet
中所有的 Pods 都被部署在不同的节点。
kubernetes-minion-group-cxpk
kubernetes-minion-group-a5aq
kubernetes-minion-group-2g2d
这是因为 zk StatefulSet
中的 Pods 指定了 PodAntiAffinity
。
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zk
topologyKey: "kubernetes.io/hostname"
requiredDuringSchedulingIgnoredDuringExecution
告诉 Kubernetes 调度器, 在以 topologyKey
指定的域中,绝对不要把带有键为 app、值为 zk 的标签 的两个 Pods 调度到相同的节点。topologyKey
kubernetes.io/hostname
表示 这个域是一个单独的节点。使用不同的规则、标签和选择算符,你能够通过这种技术把你的 ensemble 分布 在不同的物理、网络和电力故障域之间。
节点维护期间保持应用可用(Surviving Maintenance)
在本节中你将会隔离(Cordon)和腾空(Drain)节点。如果你是在一个共享的集群里使用本教程,请保证不会影响到其他租户。
上一小节展示了如何在节点之间分散 Pods 以在计划外的节点故障时保证服务存活。但是你也需要为计划内维护引起的临时节点故障做准备。
使用此命令获取你的集群中的节点。
代码语言:javascript复制kubectl get nodes
使用 kubectl cordon
隔离你的集群中除 4 个节点以外的所有节点。
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
中的Pods
。
kubectl get pods -w -l app=zk
在另一个终端中,使用下面的命令获取 Pods 当前调度的节点。
代码语言:javascript复制for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done
kubernetes-minion-group-pb41
kubernetes-minion-group-ixsl
kubernetes-minion-group-i4c4
使用 kubectl drain
来隔离和腾空 zk-0 Pod
调度所在的节点。
kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data
node "kubernetes-minion-group-pb41" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-pb41, kube-proxy-kubernetes-minion-group-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz
pod "zk-0" deleted
node "kubernetes-minion-group-pb41" drained
由于你的集群中有 4 个节点, 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
的 Pods
并腾空 zk-1
调度所在的节点。
kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data "kubernetes-minion-group-ixsl" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-ixsl, kube-proxy-kubernetes-minion-group-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74
pod "zk-1" deleted
node "kubernetes-minion-group-ixsl" drained
zk-1
Pod 不能被调度,这是因为 zk StatefulSet 包含了一个防止 Pods 共存的 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 中的 Pods 并腾空 zk-2 调度所在的节点。
代码语言:javascript复制kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data
node "kubernetes-minion-group-i4c4" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-i4c4, kube-proxy-kubernetes-minion-group-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-minion-group-i4c4, kube-proxy-kubernetes-minion-group-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2
使用 CRTL-C
终止 kubectl。
你不能腾空第三个节点,因为驱逐 zk-2
将和 zk-budget
冲突。然而这个节点仍然处于隔离状态(Cordoned)。
使用 zkCli.sh
从 zk-0
取回你的健康检查中输入的数值。
kubectl exec zk-0 zkCli.sh get /hello
由于遵守了 PodDisruptionBudget,服务仍然可用。
代码语言:javascript复制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-minion-group-pb41
node "kubernetes-minion-group-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
尝试 drain zk-2 调度的节点。
代码语言:javascript复制 kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-local-data
输出:
代码语言:javascript复制 node "kubernetes-minion-group-i4c4" already cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-minion-group-i4c4, kube-proxy-kubernetes-minion-group-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-minion-group-i4c4" drained
这次 kubectl drain
执行成功。
Uncordon 第二个节点以允许 zk-2
被重新调度。
kubectl uncordon kubernetes-minion-group-ixsl
node "kubernetes-minion-group-ixsl" uncordoned
你可以同时使用 kubectl drain
和 PodDisruptionBudgets
来保证你的服务在维护过程中仍然可用。如果使用 drain 来隔离节点并在此之前删除 pods 使节点进入离线维护状态,如果服务表达了 disruption budget,这个 budget 将被遵守。你应该总是为关键服务分配额外容量,这样它们的 Pods 就能够迅速的重新调度。
清理现场
- 使用
kubectl uncordon
解除集群中所有节点的隔离。 - 你需要删除在本教程中使用的
PersistentVolumes
的持久存储。基于你的环境、存储配置和准备方法,保证回收所有的存储。