1. 为什么你的ZooKeeper集群在K8s里总是不稳?从亲和性与污点说起
大家好,我是老李,在运维和架构这个坑里摸爬滚打了十几年,尤其爱折腾分布式中间件。今天想和大家聊聊一个非常具体、但又经常让人头疼的场景:在Kubernetes里部署一个高可用的ZooKeeper集群。你可能已经照着教程把StatefulSet、无头服务都配好了,但一上线就发现,集群状态时好时坏,节点故障时恢复慢,甚至整个服务都可能挂掉。问题出在哪?很多时候,根源不在于ZooKeeper本身,而在于K8s的调度策略没做精细化的优化。
简单来说,ZooKeeper作为一个分布式协调服务,它的核心是选举和一致性。一个典型的集群至少需要3个节点才能容忍一个节点故障。在物理机或虚拟机的时代,我们通常会手动把这3个节点部署到3台不同的物理服务器上,防止“一锅端”。但到了K8s这个动态的、声明式的环境里,事情就变了味。默认情况下,K8s的调度器只关心资源够不够(CPU、内存),它可不管你的三个ZooKeeper Pod会不会被一股脑地塞到同一个物理节点上。想象一下,如果承载这三个Pod的宿主机突然宕机,你的整个ZooKeeper集群就瞬间瘫痪了,所有依赖它的服务(比如Kafka、HBase)都会跟着遭殃。
这就是我们需要亲和性(Affinity)与反亲和性(Anti-Affinity)策略的原因。亲和性可以理解为“吸引”,比如让某些Pod尽量部署在一起;而反亲和性则是“排斥”,是我们这次的重点,它强制要求带有相同标签的Pod不能部署在同一拓扑域(比如同一台主机)上。通过配置反亲和性,我们可以确保三个ZooKeeper Pod被分散到三台不同的Node节点上,从而实现真正的高可用。
但光有反亲和性还不够,我们还会遇到另一个典型的“拦路虎”:节点污点(Taint)。在标准的K8s生产集群中,Master节点通常会被打上污点,以防止普通的业务Pod调度上去,从而保证控制平面的稳定。如果你的集群只有1个Master和2个Worker,而你又设置了严格的反亲和性(每个节点只能跑一个ZooKeeper Pod),那么你会发现,无论如何都只能启动2个Pod,第三个会一直处于“Pending”状态,因为没地方可去了。这时候,就需要请出污点容忍(Toleration)这个法宝,让我们的ZooKeeper Pod能够“忍受”Master节点的污点,从而被调度上去,凑齐三节点集群。
所以,这篇文章就是带你一步步实战,如何通过亲和性(反亲和性)策略和污点容忍机制,在K8s里部署一个既高可用又资源利用合理的ZooKeeper集群。我会分享我踩过的坑、验证过的配置,以及一些实用的排查技巧。无论你是刚开始接触K8s的运维,还是正在为生产环境稳定性发愁的架构师,相信都能从中找到答案。
2. 部署蓝图:构建稳健ZooKeeper集群的完整思路
在动手写YAML之前,我们先花点时间把整个部署的蓝图理清楚。这就像盖房子先画图纸,思路清晰了,后面填代码才能又快又稳。我见过不少朋友直接复制粘贴网上的配置,结果跑起来各种诡异问题,其实就是缺了这步“顶层设计”。
首先,我们要明确核心目标:在K8s中部署一个3节点的ZooKeeper集群,要求(1)三个Pod必须分散在不同的物理节点上;(2)即使某个节点故障,集群仍能保持可用;(3)能适应资源有限的测试环境(比如只有3台机器,其中一台还是Master)。
为了实现目标,我们需要以下几个关键组件协同工作:
无头服务(Headless Service):这是为StatefulSet量身定做的服务。它的
clusterIP设置为None,不会分配集群IP,也不会做负载均衡。那它有什么用呢?它会为每个Pod创建一个稳定的DNS域名,格式是<pod-name>.<service-name>.<namespace>.svc.cluster.local。对于ZooKeeper这种需要每个节点都有唯一标识(server id)并通过固定域名相互通信的集群来说,这是基石。你的zoo.cfg配置文件里,server.0、server.1这些条目,填的就是这个域名。配置映射(ConfigMap):我们把ZooKeeper的核心配置文件
zoo.cfg放在这里。这样做的好处是配置和镜像解耦,需要修改参数(比如超时时间)时,不用重新构建镜像,直接更新ConfigMap并重启Pod即可。配置文件里最关键的就是那几行server.x=的配置,必须指向上面无头服务生成的域名。Pod中断预算(PodDisruptionBudget, PDB):这是一个经常被忽略但非常重要的资源对象。它用来保护你的应用,避免在计划内的中断操作(比如节点维护、集群升级)时,同时停掉太多Pod导致服务不可用。我们可以设置
minAvailable: 2,这意味着在任何时候,至少要有2个ZooKeeper Pod是健康的。这样,K8s在进行驱逐等操作时,会遵守这个约定,保障集群的可用性下限。有状态副本集(StatefulSet):这是部署ZooKeeper的“本体”。与Deployment不同,StatefulSet管理的Pod拥有唯一、持久的标识符(如
zk-0,zk-1,zk-2)和稳定的存储卷。这些特性完美匹配ZooKeeper的需求:每个Pod有固定的myid,数据需要持久化。在这里面,我们将集成反亲和性规则和污点容忍配置。对外服务(Service):最后,我们还需要一个普通的Service(比如
NodePort或LoadBalancer类型),为集群外部客户端(比如你的应用程序)提供一个统一的访问入口。
关于亲和性与污点的具体策略思路:
- 反亲和性:我们将在StatefulSet的Pod模板中,使用
podAntiAffinity的requiredDuringSchedulingIgnoredDuringExecution(硬策略)。规则是:如果某个节点上已经存在一个带有app=zookeeper标签的Pod,那么调度器就绝不能再把第二个带有同样标签的Pod调度到这个节点上。拓扑域(topologyKey)我们选择kubernetes.io/hostname,这意味着以“主机名”为区分单位,确保Pod分散在不同物理节点。 - 污点容忍:针对Master节点的污点,我们在Pod模板中增加
tolerations字段。关键是要匹配Master节点污点的key和effect。通常Master节点的污点是node-role.kubernetes.io/master:NoSchedule,我们的容忍配置就要对应这个键和效果。
理清了这些,你就知道每一段YAML配置存在的意义,而不是一堆看不懂的符号了。接下来,我们就进入实战环节,一步步把它们实现出来。
3. 实战第一步:搭建基础环境与核心配置
好了,蓝图在手,我们现在开始动手搭建。我假设你已经有一个正在运行的K8s集群(至少包含1个Master和2个Worker节点),并且配置好了存储类(StorageClass),比如nfs-client,用于动态提供持久化存储。
3.1 调整Master节点污点,为调度铺路
默认情况下,Master节点有污点,不参与普通Pod调度。为了演示污点容忍的效果,我们先把它调整为一个稍温和的污点,后续再测试严格的。
# 查看当前节点污点 kubectl describe node master | grep Taint # 将污点设置为 PreferNoSchedule,表示尽量不调度,但不是绝对禁止 kubectl taint node master node-role.kubernetes.io/master:PreferNoSchedule这个命令给名为master的节点打上了一个PreferNoSchedule效果的污点。调度器会尽量避免把Pod放过来,但如果我们后面配置了反亲和性导致实在没地方去了,它还是有可能被调度过来的。这为我们后面的对比实验做了准备。
3.2 创建命名空间与配置映射
我喜欢把不同的应用放在独立的命名空间里,方便管理。我们就创建一个叫zk-cluster的命名空间。
kubectl create namespace zk-cluster接下来是ZooKeeper的配置文件。创建一个文件叫zookeeper-configmap.yaml:
apiVersion: v1 kind: ConfigMap metadata: name: zookeeper-config namespace: zk-cluster data: zoo.cfg: | tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data dataLogDir=/datalog clientPort=2181 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 # 核心配置:使用StatefulSet和无头服务生成的DNS域名 server.0=zk-0.zk-headless.zk-cluster.svc.cluster.local:2888:3888 server.1=zk-1.zk-headless.zk-cluster.svc.cluster.local:2888:3888 server.2=zk-2.zk-headless.zk-cluster.svc.cluster.local:2888:3888注意看最后三行,zk-0、zk-1、zk-2正是我们即将创建的StatefulSet Pod的名字,zk-headless是无头服务的名字,zk-cluster是命名空间。这个域名格式是固定的,也是集群内部通信的关键。
应用这个配置:
kubectl apply -f zookeeper-configmap.yaml3.3 创建无头服务(Headless Service)
创建文件zookeeper-headless-svc.yaml:
apiVersion: v1 kind: Service metadata: name: zk-headless namespace: zk-cluster labels: app: zookeeper spec: clusterIP: None # 这就是“无头”的含义 ports: - port: 2181 name: client - port: 2888 name: server - port: 3888 name: leader-election selector: app: zookeeper # 这个选择器会匹配StatefulSet Pod的标签这个服务不提供负载均衡,也不分配Cluster IP。它的唯一作用就是为每个app=zookeeper的Pod注册一个DNS A记录。应用它:
kubectl apply -f zookeeper-headless-svc.yaml3.4 设置Pod中断预算(PDB)
创建文件zookeeper-pdb.yaml:
apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: zookeeper-pdb namespace: zk-cluster spec: minAvailable: 2 # 保证任何时候至少有2个Pod可用 selector: matchLabels: app: zookeeper这个PDB会保护我们的ZooKeeper集群,在例如节点排水(drain)操作时,K8s会确保不会同时中断超过1个Pod,从而满足“至少2个可用”的条件。应用它:
kubectl apply -f zookeeper-pdb.yaml基础工作完成,我们已经准备好了ZooKeeper运行所需的外部配置和网络环境。接下来,就是重头戏——定义StatefulSet,并将亲和性与污点容忍策略融入其中。
4. 核心攻坚:在StatefulSet中实现亲和性与污点容忍
这是整个部署最核心的部分,我们将把前面讨论的所有策略,都实现在一个StatefulSet的定义里。我建议你仔细看每一段配置,理解其用意。
4.1 编写集成策略的StatefulSet配置
创建一个文件zookeeper-statefulset.yaml,内容如下,我会分段解释:
apiVersion: apps/v1 kind: StatefulSet metadata: name: zk namespace: zk-cluster spec: serviceName: zk-headless # 关联我们刚才创建的无头服务 replicas: 3 # 3节点集群 selector: matchLabels: app: zookeeper template: metadata: labels: app: zookeeper # Pod标签,非常重要,用于服务发现、PDB和亲和性规则 spec: # ---------- 第一部分:污点容忍配置 ---------- tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" # 使用Exists,只要存在这个key的污点就容忍,不关心value effect: "NoSchedule" - key: "node-role.kubernetes.io/master" operator: "Exists" effect: "PreferNoSchedule" # 这里我们容忍了两种常见的Master污点效果,确保Pod可以被调度到Master节点。 # ---------- 第二部分:亲和性(反亲和性)配置 ---------- affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬性反亲和规则 - labelSelector: matchExpressions: - key: app operator: In values: - zookeeper topologyKey: kubernetes.io/hostname # 这条规则解读:调度时,如果某个节点(topologyKey定义的范围)上已经存在一个 # 带有标签`app=zookeeper`的Pod,那么禁止再调度第二个同类Pod到此节点。 # 这样就强制了3个Pod分散在3台不同的主机上。 # ---------- 第三部分:容器定义 ---------- containers: - name: zookeeper image: zookeeper:3.8.0 # 使用官方镜像 ports: - containerPort: 2181 name: client - containerPort: 2888 name: server - containerPort: 3888 name: leader-election env: - name: ZOO_MY_ID valueFrom: fieldRef: fieldPath: metadata.name # 获取Pod名字,如zk-0 - name: ZOO_SERVERS value: "server.0=zk-0.zk-headless.zk-cluster.svc.cluster.local:2888:3888;2181 server.1=zk-1.zk-headless.zk-cluster.svc.cluster.local:2888:3888;2181 server.2=zk-2.zk-headless.zk-cluster.svc.cluster.local:2888:3888;2181" # 这里通过环境变量传递集群配置,比挂载配置文件更灵活。注意域名和上面ConfigMap里的一致。 volumeMounts: - name: data mountPath: /data - name: datalog mountPath: /datalog - name: config mountPath: /conf resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" volumes: - name: config configMap: name: zookeeper-config # 挂载我们之前创建的ConfigMap # ---------- 第四部分:持久化存储声明 ---------- volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "nfs-client" # 替换成你的实际存储类 resources: requests: storage: 5Gi - metadata: name: datalog spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "nfs-client" resources: requests: storage: 5Gi这个配置比较长,但结构清晰。它实现了:
- 污点容忍:让Pod可以调度到被打上
NoSchedule或PreferNoSchedule污点的Master节点。 - 硬反亲和性:强制Pod分散在不同物理节点,实现高可用。
- 完整ZooKeeper配置:通过环境变量和ConfigMap提供运行参数。
- 持久化存储:确保ZooKeeper的事务日志和快照数据不会随Pod消失而丢失。
现在,应用这个StatefulSet:
kubectl apply -f zookeeper-statefulset.yaml4.2 观察调度结果与策略验证
部署完成后,别急着庆祝,我们先来验证一下策略是否真的生效了。
# 查看Pod创建情况,注意观察READY状态和所在的NODE kubectl get pods -n zk-cluster -o wide -w稍等片刻,三个Pod应该都会变成Running状态。关键要看NODE那一列:
- 理想情况:如果你的集群有3个或以上Worker节点,并且资源充足,那么三个Pod应该被调度到三个不同的Worker节点上。因为反亲和性规则生效,它们不会挤在一起。
- 我们的测试情况:如果集群只有1个Master和2个Worker,你会看到两个Pod在Worker节点上,一个Pod在Master节点上。这正是污点容忍和反亲和性共同作用的结果:反亲和性阻止了两个Pod去同一个Worker,所以第三个Pod无处可去;但由于我们配置了容忍Master污点,调度器就把它放到了Master节点上。
你可以通过下面命令更详细地查看调度事件和节点信息:
# 查看某个Pod的调度事件,确认是否有被污点阻挡或反亲和性排斥的警告 kubectl describe pod zk-0 -n zk-cluster | grep -A 10 -B 5 Events # 查看每个节点上`app=zookeeper`的Pod分布 kubectl get pods -n zk-cluster -o wide --sort-by='.spec.nodeName'通过这样的观察,你就能直观地理解亲和性与污点容忍是如何控制Pod布局的。接下来,我们通过修改污点,来做一组对比实验,加深理解。
5. 策略效果对比实验:从PreferNoSchedule到NoSchedule
理论说再多,不如亲手试一下。我们现在就通过改变Master节点的污点,来直观感受不同调度策略下的集群状态。这个实验能帮你彻底明白NoSchedule和PreferNoSchedule的区别,以及为什么我们的容忍配置里两者都要写。
5.1 实验一:Master污点为PreferNoSchedule(当前状态)
此时Master的污点是我们最开始设置的PreferNoSchedule。我们刚刚部署的StatefulSet包含了容忍此污点的配置。因此,三个Pod都能顺利调度并运行。你可以通过kubectl get pods -n zk-cluster -o wide确认,三个Pod分别位于三个不同的节点上(包括Master)。
这证明了:当Worker节点资源因反亲和性而“不足”时,配置了容忍度的Pod可以突破PreferNoSchedule的限制,调度到Master节点,从而满足副本数要求。
5.2 实验二:将Master污点改为NoSchedule
现在,我们来模拟一个更严格的生产环境常见设置:Master节点严禁调度业务Pod。
# 首先删除旧的污点(key相同,effect不同需要先删除) kubectl taint node master node-role.kubernetes.io/master:PreferNoSchedule- # 添加新的、更严格的NoSchedule污点 kubectl taint node master node-role.kubernetes.io/master:NoSchedule # 确认污点已更新 kubectl describe node master | grep Taint污点更新后,已有的Pod不会受影响,它们会继续运行。但如果我们重新部署StatefulSet,或者某个Pod所在的节点故障需要重新调度,情况就不同了。
让我们模拟重新部署:
# 删除现有的StatefulSet(Pod也会被删除) kubectl delete statefulset zk -n zk-cluster # 立即重新创建StatefulSet kubectl apply -f zookeeper-statefulset.yaml # 观察Pod状态 kubectl get pods -n zk-cluster -o wide -w这次你很可能会看到,只有两个Pod(例如zk-0和zk-1)很快在Worker节点上进入Running状态,而第三个Pod(zk-2)会一直卡在Pending状态。
5.3 诊断Pending状态的Pod
为什么第三个Pod调度失败了?我们来看一下具体原因:
kubectl describe pod zk-2 -n zk-cluster在输出的事件(Events)部分,你很可能会看到类似这样的信息:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 2m default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/master: }, 2 node(s) didn't match pod affinity/anti-affinity rules.这条信息非常清晰地告诉了我们调度器的心路历程:
- 节点1(Master):有无法容忍的污点(
node-role.kubernetes.io/master:)。等等,我们的Pod不是配置了容忍NoSchedule吗?注意看,我们配置的容忍是operator: Exists,它应该能匹配这个污点。这里提示“无法容忍”,很可能是因为污点的value部分为空,或者需要更精确的匹配。在实际测试中,有时需要将容忍配置为operator: Equal并指定空的value: ""。但更常见的是,调度器在评估了所有节点后,给出了综合原因。 - 节点2和节点3(两个Worker):不满足Pod的反亲和性规则。因为每个Worker节点上已经有一个
app=zookeeper的Pod了(zk-0和zk-1),根据我们设置的硬反亲和规则,绝不允许第二个同类Pod再上去。
结论就是:在NoSchedule污点和硬反亲和性的双重夹击下,集群里没有任何一个节点能满足调度第三个Pod的条件,所以它永远处于Pending。
5.4 实验三:启用污点容忍,解决调度困境
这个困境的解法,就是确保我们的污点容忍配置是正确且强力的。对于我们当前的NoSchedule污点,确保StatefulSet YAML中的容忍配置如下(通常我们已配置):
tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" # 或者使用 "Equal",并将value设为空字符串 "" effect: "NoSchedule"确认配置无误后,我们再次清理并部署:
kubectl delete statefulset zk -n zk-cluster kubectl apply -f zookeeper-statefulset.yaml这次,你应该能看到三个Pod都成功运行了,zk-2也被调度到了Master节点上。这完美地证明了污点容忍策略成功覆盖了严格的NoSchedule污点,使得Pod能够在反亲和性规则造成的资源紧张情况下,利用Master节点的资源,保障了集群的完整性和可用性。
通过这一组对比实验,你应该能深刻体会到,在资源有限的集群中,反亲和性保障了高可用,而污点容忍则提供了调度灵活性,两者结合才能应对复杂的生产环境。
6. 高级调优与生产环境注意事项
基础的集群跑起来了,但想在生产环境用得更稳,我们还得再琢磨一下。这里分享几个我踩过坑之后总结的高级调优点和注意事项,让你的ZooKeeper集群更“抗造”。
6.1 亲和性策略的灵活运用:软策略与硬策略的结合
我们之前用的是requiredDuringSchedulingIgnoredDuringExecution,这是个“硬策略”,必须满足,否则不调度。这保证了高可用,但在某些大规模集群或资源申请紧张时,可能会造成Pod一直Pending。
K8s还提供了preferredDuringSchedulingIgnoredDuringExecution,即“软策略”或“偏好”。你可以这样用:
affinity: podAntiAffinity: # 硬策略:必须分散在不同主机 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - zookeeper topologyKey: kubernetes.io/hostname # 软策略:最好也分散在不同可用区(AZ) preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - zookeeper topologyKey: failure-domain.beta.kubernetes.io/zone # 或 topology.kubernetes.io/zone这个配置的意思是:首先,必须满足不同主机(硬性要求);其次,尽可能让Pod也分布在不同可用区(软性偏好,权重100)。这样既保证了节点级高可用,又向着跨可用区容灾的目标努力,调度也更灵活。
6.2 污点容忍的精细控制
我们之前用operator: Exists来容忍所有key为node-role.kubernetes.io/master的污点,这比较粗放。在生产中,为了安全,可以进行更精细的控制。
- 精确匹配Value:如果Master节点的污点有具体的value,比如
node-role.kubernetes.io/master=master:NoSchedule,那么容忍配置最好使用Equal并指定value。tolerations: - key: "node-role.kubernetes.io/master" operator: "Equal" value: "master" # 与污点的value匹配 effect: "NoSchedule" - 容忍时间(TolerationSeconds):对于
NoExecute污点,可以设置tolerationSeconds。这表示Pod被调度到该节点后,如果节点被添加了NoExecute污点,Pod还能继续运行多久(例如3600秒),之后才会被驱逐。这为故障处理或节点维护提供了缓冲时间。tolerations: - key: "custom-taint" operator: "Exists" effect: "NoExecute" tolerationSeconds: 3600
6.3 资源限制与健康检查
ZooKeeper对延迟很敏感,资源不足会导致选举超时、集群不稳定。
- 资源请求(Requests)与限制(Limits):一定要设置合理。请求值(
requests)是调度依据,确保节点有足够资源;限制值(limits)防止单个Pod吃光资源。内存尤其重要,ZooKeeper的JVM堆内存和本地日志都会消耗内存。resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "1000m" - 就绪探针(Readiness Probe):必须配置!ZooKeeper启动后需要时间进行数据同步和加入集群。只有当就绪探针通过后,Service才会将流量导入该Pod。可以使用
netcat检查2181端口,或者使用zkServer.sh status命令。readinessProbe: exec: command: - sh - -c - "echo ruok | nc localhost 2181 | grep imok" initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 - 存活探针(Liveness Probe):谨慎配置。一个过于敏感的存活探针可能会导致健康的ZooKeeper实例被误杀,引发集群震荡。建议使用比就绪探针更宽松的检查,或者结合其他监控指标。
6.4 存储与性能优化
- 存储类选择:ZooKeeper的写性能(事务日志)直接影响吞吐量。务必使用低延迟、高IOPS的存储,如本地SSD盘(Local PV)或高性能云盘。避免使用网络延迟高的存储。
- 分离数据目录:像我们配置中那样,将数据目录(
dataDir)和事务日志目录(dataLogDir)挂载到不同的存储卷上,可以提升写入性能。 - 监控与告警:集成Prometheus监控,采集ZooKeeper的
znode数量、连接数、延迟、选举次数等指标。设置关键告警,如节点失联、延迟过高。
把这些点都考虑到并做好,你的K8s ZooKeeper集群的稳定性和性能就能上一个大的台阶。记住,配置没有银弹,最好的配置是在理解原理的基础上,结合自己实际环境的监控数据不断调整出来的。
7. 集群验证与故障排查速查手册
部署和优化都做完了,最后一步就是验证集群是否真的健康,以及当出现问题的时候,如何快速定位。我整理了一个“速查手册”,里面都是实战中高频用到的命令和思路。
7.1 基础健康状态检查
# 1. 查看Pod基本状态 kubectl get pods -n zk-cluster -o wide # 确保所有Pod都是Running,且READY为1/1。观察分配到的节点是否符合亲和性预期。 # 2. 查看Pod日志,检查启动过程有无错误 kubectl logs zk-0 -n zk-cluster # 重点关注有无连接其他节点失败、选举相关的错误日志。 # 3. 进入Pod内部,使用ZooKeeper自带命令检查 kubectl exec -it zk-0 -n zk-cluster -- /bin/bash # 执行四字命令 echo ruok | nc localhost 2181 # 应该返回 `imok` echo stat | nc localhost 2181 # 查看详细状态,关注Mode(leader/follower)、节点连接信息等。7.2 集群内部通信与选举验证
这是验证无头服务和反亲和性是否生效的关键。
# 1. 从Pod内部,测试是否能通过DNS域名解析到其他Pod kubectl exec zk-0 -n zk-cluster -- nslookup zk-1.zk-headless.zk-cluster.svc.cluster.local # 应该能成功解析出zk-1的Pod IP。 # 2. 模拟客户端,测试跨Pod访问 # 在zk-0上,尝试连接zk-1的服务端口 kubectl exec zk-0 -n zk-cluster -- nc -zv zk-1.zk-headless.zk-cluster.svc.cluster.local 2888 kubectl exec zk-0 -n zk-cluster -- nc -zv zk-1.zk-headless.zk-cluster.svc.cluster.local 3888 # 端口应能连通。如果不通,检查网络策略(NetworkPolicy)或防火墙规则。 # 3. 检查集群选举状态 # 分别登录三个Pod,查看其角色 for i in 0 1 2; do echo "=== zk-$i status ===" kubectl exec zk-$i -n zk-cluster -- bash -c "echo stat | nc localhost 2181 | grep Mode" done # 输出中,应该只有一个Pod是`Mode: leader`,另外两个是`Mode: follower`。如果出现`Mode: standalone`或`Mode: looking`,说明集群未成功组建。7.3 常见故障与排查思路
问题一:Pod一直处于Pending状态。
- 排查步骤:
kubectl describe pod <pod-name>查看事件,最常见的就是我们实验里看到的“污点无法容忍”或“不满足反亲和性”。kubectl describe node <node-name>检查目标节点的资源(CPU、内存)是否真的充足。- 检查StorageClass,确认持久卷声明(PVC)是否成功绑定(
kubectl get pvc -n zk-cluster)。
问题二:Pod反复重启(CrashLoopBackOff)。
- 排查步骤:
kubectl logs <pod-name> --previous查看上一次崩溃的日志。- 检查配置错误:
myid文件是否生成正确(应为0,1,2),zoo.cfg中的服务器列表域名是否正确。 - 检查存储权限:Pod是否对挂载的
/data和/datalog目录有写入权限。特别是使用NFS等网络存储时。
问题三:集群状态不稳定,频繁选举。
- 排查步骤:
- 检查Pod资源使用率:
kubectl top pods -n zk-cluster,看是否达到限制(limits)导致进程被OOMKill或Throttle。 - 检查节点负载:
kubectl top nodes,高负载可能导致心跳超时。 - 检查网络延迟:在Pod之间互相ping或测试端口延迟,网络波动是ZooKeeper的天敌。
- 检查就绪/存活探针配置是否过于激进。
- 检查Pod资源使用率:
问题四:客户端无法从外部连接。
- 排查步骤:
- 确认对外服务的类型(如NodePort)和端口是否正确暴露:
kubectl get svc zk-client -n zk-cluster。 - 检查NodePort在节点防火墙是否开放。
- 从集群内部Pod测试连接对外服务的ClusterIP,确认服务本身正常。
- 确认对外服务的类型(如NodePort)和端口是否正确暴露:
把这些命令和思路存下来,下次遇到问题就不会慌了。其实运维的很多工作就是这样,把正确的配置做好,把排查的路径摸熟,剩下的就是保持观察和耐心。经过这样一番从设计到部署,从优化到验证的完整流程,我相信你已经能够在K8s上驾驭一个稳健的ZooKeeper集群了。记住,所有复杂的配置,最终目的都是为了那简单的两个字:稳定。