- 教程
- 云原生
- 容器编排
【免费下载链接】kubernetes-handbook
Kubernetes 架构与生态:从云原生到 AI 原生基础设施的构建指南
StatefulSet 是 Kubernetes 中专为有状态应用设计的控制器,能够为 Pod 提供稳定的身份标识(hostname、启动顺序、DNS 名称等),解决数据库、消息队列等有状态服务在容器化编排中的持久化与网络标识难题。本文以 Kubernetes 1.6 环境中部署 Zookeeper 与 Kafka 集群为实战主线,完整讲解 StatefulSet 的核心机制、Headless Service、PodDisruptionBudget、亲和性调度与探针配置,并深入仓库源码说明镜像构建脚本与配置生成逻辑,帮助你掌握一套可直接落地的有状态应用部署方案。
StatefulSet 解决了什么问题
StatefulSet 作为 Controller 为 Pod 提供唯一的标识,可以保证部署和 scale 的顺序。与为无状态服务设计的 Deployment 和 ReplicaSet 不同,StatefulSet 专门面向有状态服务,其应用场景包括:
- 稳定的持久化存储:Pod 重新调度后仍能访问到相同的持久化数据,基于 PVC 实现;
- 稳定的网络标志:Pod 重新调度后其 PodName 和 HostName 不变,基于 Headless Service(即没有 Cluster IP 的 Service)实现;
- 有序部署、有序扩展:Pod 按照定义的顺序从 0 到 N-1 依次创建,在下一个 Pod 运行之前所有之前的 Pod 必须都是 Running 和 Ready 状态;
- 有序收缩、有序删除:从 N-1 到 0 逆序进行。
从上述应用场景可以看出,一个完整的 StatefulSet 方案由以下几个部分组成:
- 用于定义网络标志(DNS domain)的 Headless Service;
- 用于创建 PersistentVolumes 的
volumeClaimTemplates; - 定义具体应用的 StatefulSet。
StatefulSet 中每个 Pod 的 DNS 格式为statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local,其中:
| 组成部分 | 含义 |
|---|---|
serviceName | Headless Service 的名字 |
0..N-1 | Pod 所在的序号,从 0 开始到 N-1 |
statefulSetName | StatefulSet 的名字 |
namespace | 服务所在的 namespace,Headless Service 和 StatefulSet 必须在相同的 namespace |
.cluster.local | Cluster Domain |
部署 Zookeeper 集群
下面以在 Kubernetes 1.6 版本中部署 Zookeeper 为例讲解 StatefulSet 的使用。Zookeeper 镜像基于 CentOS 系统的 JDK 制作(示例中为私人镜像harbor-001.jimmysong.io/library/zookeeper:3.4.6),完整的 Dockerfile 和配置文件位于仓库的 manifests/zookeeper 目录。
镜像构建与脚本设计
Dockerfile 从远程获取 Zookeeper 的安装文件并解压安装,随后将三个辅助脚本复制到/opt/zookeeper/bin/(见 manifests/zookeeper/Dockerfile):
zkGenConfig.sh:生成 zookeeper 配置文件;zkMetrics.sh:获取 zookeeper 的 metrics;zkOk.sh:用来做 ReadinessProbe。
在zkGenConfig.sh(manifests/zookeeper/zkGenConfig.sh)中,脚本通过正则从 hostname 中提取序数,生成集群中每个节点的myid与zoo.cfg:
HOST=`hostname -s` if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then NAME=${BASH_REMATCH[1]} ORD=${BASH_REMATCH[2]} else echo "Failed to extract ordinal from hostname $HOST" exit 1 fi MY_ID=$((ORD+1))该脚本的核心思路是:StatefulSet 创建的 Pod hostname 形如zk-0、zk-1、zk-2,脚本通过print_servers()函数以server.$i=$NAME-$((i-1)).$DOMAIN:$ZK_SERVER_PORT:$ZK_ELECTION_PORT的格式把整个 Zookeeper ensemble 的节点地址写入zoo.cfg,从而让每个节点都知道其他成员的位置;同时向$ZK_DATA_DIR/myid写入唯一的节点 ID(序数 + 1)。脚本还负责创建数据目录、生成log4j.properties与java.env(通过JVMFLAGS="-Xmx$ZK_HEAP_SIZE -Xms$ZK_HEAP_SIZE"控制堆内存),完整流程为validate_env && create_config && create_log_props && create_data_dirs && create_java_env。
zkMetrics.sh脚本实际上执行的是下面的命令,通过 Zookeeper 的四字命令mntr获取监控指标:
$ echo mntr | nc localhost $ZK_CLIENT_PORT >& 1 zk_version 3.4.6-1569965, built on 02/20/2014 09:09 GMT zk_avg_latency 0 zk_max_latency 5 zk_min_latency 0 zk_packets_received 427879 zk_packets_sent 427890 zk_num_alive_connections 3 zk_outstanding_requests 0 zk_server_state leader zk_znode_count 18 zk_watch_count 3 zk_ephemerals_count 4 zk_approximate_data_size 613 zk_open_file_descriptor_count 29 zk_max_file_descriptor_count 1048576 zk_followers 1 zk_synced_followers 1 zk_pending_syncs 0zkOk.sh脚本实际上执行的是下面的命令,通过ruok四字命令判断实例是否健康(manifests/zookeeper/zkOk.sh):当服务端返回imok时退出码为 0,否则为 1,恰好作为 exec 型探针的判断依据:
$ echo ruok | nc 127.0.0.1 $ZK_CLIENT_PORT imokzookeeper.yaml 详解
下面是启动三个 zookeeper 实例的 YAML 配置文件,完整文件见 manifests/zookeeper/zookeeper.yaml:
--- apiVersion: v1 kind: Service metadata: name: zk-svc labels: app: zk-svc spec: ports: - port: 2888 name: server - port: 3888 name: leader-election clusterIP: None selector: app: zk --- apiVersion: v1 kind: ConfigMap metadata: name: zk-cm data: jvm.heap: "1G" tick: "2000" init: "10" sync: "5" client.cnxns: "60" snap.retain: "3" purge.interval: "0" --- apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: zk-pdb spec: selector: matchLabels: app: zk minAvailable: 2 --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: zk spec: serviceName: zk-svc replicas: 3 template: metadata: labels: app: zk spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: "app" operator: In values: - zk topologyKey: "kubernetes.io/hostname" containers: - name: k8szk imagePullPolicy: Always image: harbor-001.jimmysong.io/library/zookeeper:3.4.6 resources: requests: memory: "2Gi" cpu: "500m" ports: - containerPort: 2181 name: client - containerPort: 2888 name: server - containerPort: 3888 name: leader-election env: - name : ZK_REPLICAS value: "3" - name : ZK_HEAP_SIZE valueFrom: configMapKeyRef: name: zk-cm key: jvm.heap - name : ZK_TICK_TIME valueFrom: configMapKeyRef: name: zk-cm key: tick - name : ZK_INIT_LIMIT valueFrom: configMapKeyRef: name: zk-cm key: init - name : ZK_SYNC_LIMIT valueFrom: configMapKeyRef: name: zk-cm key: tick - name : ZK_MAX_CLIENT_CNXNS valueFrom: configMapKeyRef: name: zk-cm key: client.cnxns - name: ZK_SNAP_RETAIN_COUNT valueFrom: configMapKeyRef: name: zk-cm key: snap.retain - name: ZK_PURGE_INTERVAL valueFrom: configMapKeyRef: name: zk-cm key: purge.interval - name: ZK_CLIENT_PORT value: "2181" - name: ZK_SERVER_PORT value: "2888" - name: ZK_ELECTION_PORT value: "3888" command: - sh - -c - zkGenConfig.sh && zkServer.sh start-foreground readinessProbe: exec: command: - "zkOk.sh" initialDelaySeconds: 10 timeoutSeconds: 5 livenessProbe: exec: command: - "zkOk.sh" initialDelaySeconds: 10 timeoutSeconds: 5 securityContext: runAsUser: 1000 fsGroup: 1000这个 YAML 中包含了 StatefulSet 部署有状态应用的所有关键元素,逐一说明如下:
Headless Service(zk-svc):clusterIP: None使其成为无头服务,只为 Pod 提供稳定的 DNS 域名而不做负载均衡。它声明了 Zookeeper 集群内部通信使用的两个端口:2888(server,用于 Follower 与 Leader 之间的数据同步)和 3888(leader-election,用于 Leader 选举)。
ConfigMap(zk-cm):将 Zookeeper 的调优参数与镜像解耦,通过configMapKeyRef注入到容器环境变量中。各参数与zkGenConfig.sh中读取的环境变量一一对应:tick对应tickTime(基本时间单元,单位毫秒)、init对应initLimit(Follower 连接并同步到 Leader 的初始化时间上限,单位 tick)、sync对应syncLimit(Follower 与 Leader 通信的同步时间上限,单位 tick)、client.cnxns对应maxClientCnxns(单客户端最大连接数)、snap.retain对应autopurge.snapRetainCount(保留的快照数量)、purge.interval对应autopurge.purgeInterval(自动清理快照的间隔小时数)。注意示例中ZK_SYNC_LIMIT误取了key: tick,实际生产环境中应指向key: sync。
PodDisruptionBudget(zk-pdb):minAvailable: 2保证集群在任何时候至少有 2 个 Zookeeper 实例可用,避免节点维护(voluntary disruptions)时破坏 Quorum。
StatefulSet(zk):serviceName: zk-svc将 StatefulSet 与 Headless Service 绑定,replicas: 3创建 3 个实例。
Pod 反亲和性(podAntiAffinity):使用requiredDuringSchedulingIgnoredDuringExecution+topologyKey: "kubernetes.io/hostname",强制要求三个 Zookeeper Pod 调度到不同的节点上,保证单个节点宕机不会导致整个集群不可用。这是分布式有状态应用常见的"打散"策略。
启动命令:command: ["sh", "-c", "zkGenConfig.sh && zkServer.sh start-foreground"]先在容器启动时生成配置,再以前台方式启动 Zookeeper(容器主进程必须前台运行,否则 Pod 会立即退出)。
就绪与存活探针:readinessProbe 与 livenessProbe 均使用exec执行zkOk.sh。就绪探针决定 Pod 是否进入 Ready 状态(只有 Ready 后 StatefulSet 才会继续创建下一个 Pod),存活探针决定 Pod 是否需要被重启,initialDelaySeconds: 10给 JVM 启动留出缓冲时间。
securityContext:runAsUser: 1000, fsGroup: 1000与 Dockerfile 中useradd zookeeper(UID/GID 均为 1000,见 manifests/zookeeper/Dockerfile)保持一致,确保数据目录的属主权限匹配。
注:示例中的 YAML 没有配置持久化存储(未使用
volumeClaimTemplates),且镜像为作者私人镜像、外部无法访问。生产环境建议参考 Kubernetes 官方 Zookeeper StatefulSet 教程,为每个 Pod 挂载独立 PVC(详见下文"组件与稳定存储")。
部署 Kafka 集群
Kafka 的 docker 镜像制作跟 zookeeper 类似,都是从远程下载安装包后解压安装(Dockerfile 与配置文件见 manifests/kafka 目录)。与 zookeeper 不同的是,Kafka 侧只需要一个脚本,但它依赖于上一步安装的 zookeeper。
kafkaGenConfig.sh:从 hostname 推导 broker ID
kafkaGenConfig.sh(manifests/kafka/kafkaGenConfig.sh)用来生成 kafka 的配置文件:
#!/bin/bash HOST=`hostname -s` if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then NAME=${BASH_REMATCH[1]} ORD=${BASH_REMATCH[2]} else echo "Failed to extract ordinal from hostname $HOST" exit 1 fi MY_ID=$((ORD+1)) sed -i s"/broker.id=0/broker.id=$MY_ID/g" /opt/kafka/config/server.properties sed -i s'/zookeeper.connect=localhost:2181/zookeeper.connect=zk-0.zk-svc.brand.svc:2181,zk-1.zk-svc.brand.svc:2181,zk-2.zk-svc.brand.svc:2181/g' /opt/kafka/config/server.properties该脚本根据 StatefulSet 生成的 Pod 的 hostname 的后半截数字部分作为 broker ID(broker.id = 序数 + 1,保证每个 broker 拥有全局唯一的整数 ID),同时将server.properties中默认的zookeeper.connect=localhost:2181替换为三个 Zookeeper 节点的完整 DNS 地址。
这里用到的zk-0.zk-svc.brand.svc正是 StatefulSet Pod DNS 命名规则statefulSetName-{0..N-1}.serviceName.namespace的实际体现——zk是 StatefulSet 名、zk-svc是 Headless Service 名、brand是 namespace。Zookeeper 在zkGenConfig.sh中通过print_servers()生成 ensemble 配置,Kafka 侧则直接在连接串中写死三个 Zookeeper 的 DNS,两者相互印证了"以稳定 DNS 名称替代 Pod IP"这一有状态应用的核心设计。
kafka.yaml 详解
下面是创建 3 个 kafka 实例的 YAML 配置(manifests/kafka/kafka.yaml):
--- apiVersion: v1 kind: Service metadata: name: kafka-svc labels: app: kafka spec: ports: - port: 9093 name: server clusterIP: None selector: app: kafka --- apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: kafka-pdb spec: selector: matchLabels: app: kafka minAvailable: 2 --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: kafka spec: serviceName: kafka-svc replicas: 3 template: metadata: labels: app: kafka spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: "app" operator: In values: - kafka topologyKey: "kubernetes.io/hostname" podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 podAffinityTerm: labelSelector: matchExpressions: - key: "app" operator: In values: - zk topologyKey: "kubernetes.io/hostname" terminationGracePeriodSeconds: 300 containers: - name: k8skafka imagePullPolicy: Always image: harbor-001.jimmysong.io/library/kafka:2.10-0.8.2.1 resources: requests: memory: "1Gi" cpu: 500m env: - name: KF_REPLICAS value: "3" ports: - containerPort: 9093 name: server command: - /bin/bash - -c - "/opt/kafka/bin/kafkaGenConfig.sh && /opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties" env: - name: KAFKA_HEAP_OPTS value : "-Xmx512M -Xms512M" - name: KAFKA_OPTS value: "-Dlogging.level=DEBUG" readinessProbe: tcpSocket: port: 9092 initialDelaySeconds: 15 timeoutSeconds: 1与 Zookeeper 配置相比,Kafka 的 YAML 有几个值得注意的差异:
双重亲和性组合:在podAntiAffinity强制要求三个 Kafka broker 分到不同节点之外,还使用了podAffinity的软策略(preferredDuringSchedulingIgnoredDuringExecution,权重 1),倾向让 Kafka 与 Zookeeper(app: zk)调度到同一节点,以减少跨节点访问 Zookeeper 的网络开销——这是"反亲和打散 + 亲和就近"的经典组合。
terminationGracePeriodSeconds: 300:Kafka 关闭时需要将内存中的日志段刷盘并完成分区 leader 迁移,因此设置了 300 秒的宽限期,避免 Pod 被强制删除导致数据不一致。注意 StatefulSet 官方建议不要把该值设为 0,这会导致强制删除 Pod 从而破坏有状态应用的数据安全。
启动命令与探针:启动命令依次执行配置生成与前台启动;readinessProbe 使用tcpSocket探测 9092 端口(Kafka 实际监听端口,定义于 manifests/kafka/server.properties 的port=9092),initialDelaySeconds: 15等待 broker 完成启动注册。
JVM 参数注入:KAFKA_HEAP_OPTS将堆内存固定为 512M(-Xmx512M -Xms512M,防止堆动态伸缩带来的停顿),KAFKA_OPTS开启 DEBUG 级别日志便于排障。Kafka 的server.properties(manifests/kafka/server.properties)中还包含了num.partitions(每 topic 默认分区数,默认 1)、log.retention.hours(日志保留时间,默认 168 小时)、log.segment.bytes(单日志段大小)等可调参数,生产环境可按需通过 ConfigMap 注入。
集群外部访问 StatefulSet 的 Pod
在 Kubernetes 集群外部调试 StatefulSet 中有序的 Pod 时,如何访问这些 Pod?方法是为 Pod 设置 label,然后用kubectl expose将其以 NodePort 的方式暴露到集群外部。以上面的 zookeeper 为例,下面使用命令的方式暴露其中的两个 zookeeper 节点(也可以写一个 service 配置 yaml 文件):
kubectl label pod zk-0 zkInst=0 kubectl label pod zk-1 zkInst=1 kubectl expose po zk-0 --port=2181 --target-port=2181 --name=zk-0 --selector=zkInst=0 --type=NodePort kubectl expose po zk-1 --port=2181 --target-port=2181 --name=zk-1 --selector=zkInst=1 --type=NodePort这样在 Kubernetes 集群外部就可以根据 Pod 所在的主机所映射的端口来访问了。查看zk-0这个 service 可以看到如下结果:
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE zk-0 10.254.98.14 <nodes> 2181:31693/TCP 5m集群外部就可以使用所有 node 中的任何一个IP:31693来访问这个 zookeeper 实例。这种方式的本质是:StatefulSet 保证 Pod 名称与身份稳定不变,因此可以为某个特定序数的 Pod 打上专属 label 并单独暴露,这在调试单个节点、验证 Leader/Follower 角色等场景下非常实用。
深入理解 StatefulSet 的组件、顺序保证与更新策略
在动手部署之外,理解 concepts/statefulset.md 中阐述的 StatefulSet 底层机制,有助于在生产环境正确设计有状态服务。
组件:Headless Service、StatefulSet 与 volumeClaimTemplates
一个典型 StatefulSet 由三部分组成:Headless Service(控制网络域)、StatefulSet(spec中指定副本数)、volumeClaimTemplates(使用 PersistentVolume Provisioner 提供的 PersistentVolumes 作为稳定存储)。volumeClaimTemplates会为每个 Pod 自动创建一个 PVC(命名格式为<claimName>-<statefulSetName>-<ordinal>,例如www-web-0),当 Pod 重新调度到其他节点时,volumeMounts会挂载与 PVC 相关联的同一个 PersistentVolume,从而保证数据不丢失。
需要注意:删除或 scale StatefulSet 不会删除与其关联的 volume。这是为了确保数据安全,通常比自动清除所有相关 StatefulSet 资源更有价值,但意味着不再使用的 PVC 需要手动清理。此外,StatefulSet 目前要求必须有 Headless Service 负责 Pod 的网络身份,创建该 Service 是使用者的责任。
部署与 Scale 的保证
- 对于有 N 个副本的 StatefulSet,Pod 将按照 {0..N-1} 的顺序被创建和部署;
- 删除 Pod 时,按照逆序 {N-1..0} 终结;
- 对 Pod 执行 scale 操作之前,它所有的前任必须处于 Running 和 Ready 状态;
- 在终止 Pod 前,它所有的继任者必须处于完全关闭状态。
例如上面的 nginx 示例(详见 manifests/test/web.yaml)创建后,3 个 Pod 将按照 web-0、web-1、web-2 的顺序创建:web-0 进入 Running 和 Ready 之前,web-1 不会被部署;若 web-0 在 web-1 就绪后失败,web-2 将不会启动,直到 web-0 成功重启并恢复就绪。缩容时顺序相反:replicas=1时 web-2 先被终止,web-2 完全关闭之前 web-1 不会被终止。
Pod 管理策略与更新策略
Kubernetes 1.7 及之后版本,StatefulSet 通过.spec.podManagementPolicy放开顺序保证:
OrderedReady(默认):实现上述有序启动/终止行为;Parallel:并行的启动和终止 Pod,在启动/终止其他 Pod 之前不会等待 Pod 变成运行并就绪或完全终止。
更新策略由.spec.updateStrategy控制:
OnDelete(Kubernetes 1.6 及以前的遗留行为,未指定时默认):控制器不会自动更新 Pod,用户必须手动删除 Pod 才能以新 template 重建;RollingUpdate:控制器按与终止相同的顺序(从最大序数到最小序数)每次更新一个 Pod,等待其 Running 和 Ready 后再更新前身;- 分区(partition):通过
.spec.updateStrategy.rollingUpdate.partition指定后,只有序数大于或等于 partition 的 Pod 会被更新,序数小于 partition 的 Pod 不受影响。这在金丝雀发布或分阶段发布中非常有用;若 partition 大于 replicas,则 template 的更新不会传播到任何 Pod。
小结
本文以 Zookeeper 与 Kafka 两个经典有状态中间件为例,完整展示了 StatefulSet 在 Kubernetes 中的落地方式:通过 Headless Service 提供稳定的 DNS 身份、通过启动脚本从 hostname 推导集群成员与节点 ID、通过 PodDisruptionBudget 与亲和性调度保障集群可用性、通过探针控制有序滚动。所有脚本与配置均可在仓库的 manifests/zookeeper 与 manifests/kafka 目录中找到,concepts/statefulset.md 则提供了更完整的组件、顺序保证与更新策略说明。需要指出的是,示例中的镜像为作者私人镜像且未配置持久化存储,生产环境应替换为可访问的镜像仓库,并通过volumeClaimTemplates为每个 Pod 挂载独立 PVC,以获得真正的数据持久化保障。
- 教程
- 云原生
- 容器编排
【免费下载链接】kubernetes-handbook
Kubernetes 架构与生态:从云原生到 AI 原生基础设施的构建指南
相关推荐
kubernetes-handbook 实战:StatefulSet 原理与有状态应用部署指南
kubernetes handbook 实战:StatefulSet 原理与有状态应用部署指南 StatefulSet 是 Kubernetes 中唯一面向有状
教程云原生容器编排Toonflow 二次元场景图生成实战:成熟都市言情风格约束手册与源码级调用链解析
Toonflow 二次元场景图生成实战:成熟都市言情风格约束手册与源码级调用链解析 导读 本文以 Toonflow 开源仓库中「成熟都市言情二次元动画」风格包的
人工智能大模型AI 应用AI Agent媒体生成后端桌面应用设计冲刺与敏捷开发:如何将两者结合提升产品效率
设计冲刺与敏捷开发:如何将两者结合提升产品效率 设计冲刺与敏捷开发是现代产品开发中两种高效的方法论,它们各自拥有独特的优势。设计冲刺通过五天的集中式协作,快速解
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考