K8s StatefulSet五大有状态集群实战_Nginx_Etcd_Redis_Cassandra_MySQL

StatefulSet 一篇封神:手把手在 K8s 部署 Nginx / Etcd / Redis 哨兵 / Cassandra / MySQL 五大有状态集群

摘要:用 Deployment 跑 MySQL、Redis 这类有状态应用,必踩三坑:IP 漂移集群找不到人、卷被抢数据读不出、多副本乱序启动直接脑裂。根因就是用错了控制器 ------ 应该用 StatefulSet。本文讲透 StatefulSet 四大特性 + 四大组件,实战部署 Nginx、Etcd、Redis 哨兵、Cassandra、MySQL 五个集群,每个都有完整 YAML + 验证命令。


一、环境准备

bash 复制代码
root@master30:~# kubectl create ns statefulset
root@master30:~# kubectl config set-context --current --namespace statefulset

二、无状态应用 vs 有状态应用

维度 无状态应用(Stateless) 有状态应用(Stateful)
核心特征 不保存任何请求/会话数据,每次请求独立 依赖持久化的会话/数据,请求间有依赖关系
数据存储 数据仅在请求期间存在,不落地/仅落地外部存储 数据需持久化(本地/分布式存储),依赖固定存储
实例一致性 所有实例完全相同,可随意扩缩容/替换 实例有身份(如编号),启动/扩缩容有顺序要求
网络标识 共享 IP/域名,无固定网络标识 有固定网络标识(如 Headless Service + DNS)

三句话总结:

  1. 无状态核心:实例"无记忆",可随意替换/扩容,是云原生优先推荐的应用形态;
  2. 有状态核心:实例"有记忆",依赖持久化和固定身份,需通过 StatefulSet 等组件保障稳定性;
  3. 最佳实践:将应用拆分为「无状态业务层 + 有状态数据层」,既提升扩展能力,又保障数据安全。

三、StatefulSet 介绍

定义

StatefulSet 是专门管有状态应用的 K8s 控制器。和 Deployment 的本质区别:每个 Pod 有固定身份(名字、DNS、存储),重建后不变,数据不丢。

适用场景:数据库(MySQL、PostgreSQL)、分布式中间件(Redis 集群、ZooKeeper)、消息队列(Kafka)等依赖节点身份和数据持久化的服务。

四大特性

特性 一句话
固定身份 Pod 名 mysql-0/mysql-1/mysql-2,DNS mysql-0.无头服务.命名空间.svc,重建不变
有序操作 建 Pod 从 0→N 逐个来,删 Pod 从 N→0 倒着来,更新也按序从 N→0 倒着来
稳定存储 每个 Pod 自动绑专属 PVC(模板名-STS名-序号),Pod 删了 PVC 还在,数据不丢
依赖无头服务 必须配一个 clusterIP: None 的 Headless Service,专门给 Pod 发固定 DNS,不做负载均衡

StatefulSet VS Deployment

控制器 StatefulSet Deployment
适用场景 有状态应用(数据库、分布式集群、消息队列) 无状态应用(Web 服务、API 接口、静态服务)
网络 Headless Service(固定 DNS 解析) ClusterIP/LoadBalancer(共享 IP)
存储 必须 PVC + PV(固定存储卷绑定) 可选 PVC(无数据持久化要求)
扩缩容 按实例编号顺序扩缩容(如 0→1→2) 无顺序,瞬间完成
更新策略 有序更新(从最后一个实例开始) 滚动更新/重建,无顺序
重启/重建 实例重启后需恢复原有数据/身份 实例重启后无影响

关键组件

  • StatefulSet 控制器:K8s 核心控制器之一,负责管理 Pod 全生命周期,按序号规则创建、更新、删除,维护 Pod 身份和存储的稳定性。
  • Headless Service:核心作用是为 Pod 提供固定 DNS 解析,无 ClusterIP,支持 Pod 间通过 hostname 稳定通信。
  • PVC 模板:定义每个 Pod 所需的存储规格,StatefulSet 会自动为每个 Pod 创建对应 PVC,无需手动创建。
  • PV:实际的存储资源,与 PVC 一一绑定,可选用本地存储、云存储(AWS EBS、阿里云 EBS)、分布式存储(Ceph)等。

四、示例 1:部署 Nginx 集群

需求:部署 3 节点 Nginx 集群,实现数据持久化、固定网络标识、有序部署。

步骤 1:创建 Headless Service

bash 复制代码
root@master30:~# cat > nginx-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: statefulset
spec:
  selector:
    app: nginx     
  clusterIP: None
  ports:
  - port: 80
    targetPort: 80
    name: nginx-port
EOF

root@master30:~# kubectl apply -f nginx-service.yaml

root@master30:~# kubectl get svc
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    16m

步骤 2:创建 StatefulSet

bash 复制代码
root@master30:~# cat > nginx-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"  # 指向上面配置的Headless Service的名字
  replicas: 3
  minReadySeconds: 10  # Pod 启动并通过健康检查后,至少再等 10 秒,才参数到k8s可用的pod里
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10  # Pod 被删除时,K8s 给它 10 秒时间 "优雅退场",到时没退,直接强杀
      containers:
      - name: nginx
        image: hub.laoma.cloud/library/nginx:latest
        ports:
        - containerPort: 80
          name: nginx
        volumeMounts:
        - name: nginx-data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: nginx-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

root@master30:~# kubectl apply -f nginx-statefulset.yaml

观察有序创建过程:

bash 复制代码
root@master30:~# watch kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          10m
nginx-1   1/1     Running   0          10m
nginx-2   1/1     Running   0          9m41s

root@master30:~# kubectl get sts
NAME    READY   AGE
nginx   3/3     10m

root@master30:~# kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}'
nginx-data-nginx-0 pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f
nginx-data-nginx-1 pvc-e853eccd-8060-4326-8c45-0b88c34a3d97
nginx-data-nginx-2 pvc-27d944f7-1e62-4352-9023-3874e6867ef9

步骤 3:准备 Pod 数据

直接在 NFS 共享目录写入各自主页:

bash 复制代码
root@master30:~# kubectl get pvc|awk '{print $1,$3}'
NAME VOLUME
nginx-data-nginx-0 pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f
nginx-data-nginx-1 pvc-e853eccd-8060-4326-8c45-0b88c34a3d97
nginx-data-nginx-2 pvc-27d944f7-1e62-4352-9023-3874e6867ef9

root@master30:~# echo nginx-data-nginx-0 > /shares/statefulset-nginx-data-nginx-0-pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f/index.html
root@master30:~# echo nginx-data-nginx-1 > /shares/statefulset-nginx-data-nginx-1-pvc-e853eccd-8060-4326-8c45-0b88c34a3d97/index.html
root@master30:~# echo nginx-data-nginx-2 > /shares/statefulset-nginx-data-nginx-2-pvc-27d944f7-1e62-4352-9023-3874e6867ef9/index.html

也可以用脚本一键完成(在 master30 上执行):

bash 复制代码
root@master30:~# cat > prepare_nginx_content.sh <<'EOF'
nfs_share_path=/shares
namespace=statefulset
kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}' > pvc.list
cat pvc.list | while read pvc
do
  pvc_name=$(echo -n $pvc | awk '{print $1}')
  pv_name=$(echo -n $pvc | awk '{print $2}')
  echo ${pvc_name} > ${nfs_share_path}/${namespace}-${pvc_name}-${pv_name}/index.html
done
rm pvc.list
EOF

root@master30:~# bash prepare_nginx_content.sh

步骤 4:验证部署

bash 复制代码
root@master30:~# kubectl run test --image nginx -it -- bash
If you don't see a command prompt, try pressing enter.

root@test:/# curl nginx-0.nginx
nginx-data-nginx-0
root@test:/# curl nginx-1.nginx
nginx-data-nginx-1
root@test:/# curl nginx-2.nginx
nginx-data-nginx-2

删除 Pod 验证数据持久化:

bash 复制代码
root@master30:~# kubectl delete pod nginx-2
root@master30:~# kubectl get pods
NAME       READY   STATUS              RESTARTS   AGE
nginx-0    1/1     Running             0          49m
nginx-1    1/1     Running             0          49m
nginx-2    0/1     ContainerCreating   0          3s

root@master30:~# kubectl exec test -it -- bash
root@test:/# curl nginx-2.nginx
nginx-data-nginx-2

🤔 思考:如何为 StatefulSet 提供一个统一的 IP 入口?

答案:再创建一个普通的 ClusterIP 类型 Service,暴露 StatefulSet。

bash 复制代码
root@master30:~# cat > nginx-service-www.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  labels:
    app: www
  name: www
spec:
  ports:
  - name: 80-80
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx             # 根据标签匹配,对应上面StatefulSet创建的pod的标签
  type: ClusterIP
EOF

root@master30:~# kubectl apply -f nginx-service-www.yaml
root@master30:~# kubectl get svc www
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
www     ClusterIP   10.100.163.95   <none>        80/TCP    20s

root@master30:~# curl 10.100.163.95
nginx-data-nginx-0
root@master30:~# curl 10.100.163.95
nginx-data-nginx-1
root@master30:~# curl 10.100.163.95
nginx-data-nginx-2

步骤 5:清理环境

bash 复制代码
root@master30:~# kubectl delete sts nginx
root@master30:~# kubectl delete svc nginx www
root@master30:~# kubectl delete pvc nginx-data-nginx-{0..2}
root@master30:~# rm -fr /shares/statefulset-nginx-data-nginx-{0..2}-*
root@master30:~# kubectl delete pod test

五、示例 2:部署 Etcd 集群

需求:部署 3 节点 etcd 集群,实现数据持久化、固定网络标识、有序部署。

步骤 1:创建 Headless Service

bash 复制代码
root@master30:~# cat > etcd-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: etcd
  namespace: statefulset
  labels:
    app: etcd
spec:
  ports:
  - port: 2379
    name: client
  - port: 2380
    name: peer
  clusterIP: None
  selector:
    app: etcd
EOF

root@master30:~# kubectl apply -f etcd-service.yaml

root@master30:~# kubectl get svc
NAME   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
etcd   ClusterIP   None         <none>        2379/TCP,2380/TCP   5s

步骤 2:创建 StatefulSet

bash 复制代码
root@master30:~# cat > etcd-statefulset.yaml <<'EOF'
apiVersion: apps/v1                              # apps/v1 是 StatefulSet 稳定版 API
kind: StatefulSet                                # 资源类型:有状态副本集
metadata:
  name: etcd                                     # StatefulSet 名字
  namespace: statefulset                         # 命名空间
spec:
  serviceName: etcd                              # 关键!绑定上面那个 Headless Service 的名字
  replicas: 3                                    # 起 3 个副本,组成 Etcd 集群
  selector:
    matchLabels:
      app: etcd                                 # 管带 app=etcd 标签的 Pod
  template:                                      # Pod 模板
    metadata:
      labels:
        app: etcd                                # Pod 标签,必须和上面 matchLabels 一致
    spec:
      containers:
      - name: etcd
        image: hub.laoma.cloud/kubernetes/etcd:3.5.12-0   # Etcd 3.5 版本
        command:
        - /usr/local/bin/etcd                    # 启动命令
        ports:
        - containerPort: 2379                     # 客户端端口(别人连 Etcd 用的)
          name: client
        - containerPort: 2380                     # 节点间通信端口(集群内部同步用的)
          name: peer
        env:
        - name: ETCD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name            # 把 Pod 自己的名字注入环境变量(etcd-0/1/2)
        - name: ETCD_DATA_DIR
          value: /var/lib/etcd                    # Etcd 数据存这个目录
        # ↓↓↓ 下面是 Etcd 集群配置的核心环境变量 ↓↓↓
        - name: ETCD_INITIAL_ADVERTISE_PEER_URLS
          # 告诉集群其他节点:"我是谁,来找我连集群用这个地址"
          # 格式:http://(Pod名).etcd.statefulset.svc:2380
          # 比如 etcd-0 就是 http://etcd-0.etcd.statefulset.svc:2380
          value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2380"
        - name: ETCD_LISTEN_PEER_URLS
          # 监听所有网卡的 2380 端口,接收其他节点的集群同步请求
          value: "http://0.0.0.0:2380"
        - name: ETCD_LISTEN_CLIENT_URLS
          # 监听所有网卡的 2379 端口,接收客户端连接
          value: "http://0.0.0.0:2379"
        - name: ETCD_ADVERTISE_CLIENT_URLS
          # 告诉客户端:"访问我用这个地址"(通过固定 DNS 名)
          value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2379"
        - name: ETCD_INITIAL_CLUSTER
          # 关键!列出集群所有节点的地址(初始化时用)
          # 三个节点的 2380 端口都列出来
          value: "etcd-0=http://etcd-0.etcd.statefulset.svc.cluster.local:2380,etcd-1=http://etcd-1.etcd.statefulset.svc.cluster.local:2380,etcd-2=http://etcd-2.etcd.statefulset.svc.cluster.local:2380"
        - name: ETCD_INITIAL_CLUSTER_TOKEN
          value: "etcd-token"                      # 集群令牌,同一个集群用同一个 token
        - name: ETCD_INITIAL_CLUSTER_STATE
          value: "new"                             # new = 全新集群(已有的集群要改成 existing)
        volumeMounts:
        - name: etcd-data
          mountPath: /var/lib/etcd                # 把数据卷挂到 Etcd 数据目录
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
  volumeClaimTemplates:                           # 关键!PVC 模板,每个 Pod 自动建专属 PVC
  - metadata:
      name: etcd-data                             # PVC 名字会自动变成 etcd-data-etcd-0/1/2
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"            # 用 NFS 动态存储
      resources:
        requests:
          storage: 10Gi                           # 每个节点 10G 数据
EOF

root@master30:~# kubectl apply -f etcd-statefulset.yaml

root@master30:~# kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          77s
etcd-1   1/1     Running   0          74s
etcd-2   1/1     Running   0          71s

root@master30:~# kubectl get sts
NAME   READY   AGE
etcd   3/3     99s

步骤 3:验证部署

bash 复制代码
root@master30:~# kubectl exec etcd-0 -- etcdctl member list
8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false
881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false
b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false

root@master30:~# kubectl delete pod etcd-2
root@master30:~# kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          3m26s
etcd-1   1/1     Running   0          3m23s
etcd-2   1/1     Running   0          11s

root@master30:~# kubectl exec etcd-0 -- etcdctl member list
8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false
881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false
b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false

验证数据读写:

bash 复制代码
root@master30:~# kubectl exec etcd-0 -- etcdctl put /users/user1/name laowang
OK

root@master30:~# kubectl exec etcd-2 -- etcdctl get /users/user1/name
/users/user1/name
laowang

步骤 4:清理环境

bash 复制代码
root@master30:~# kubectl delete sts etcd
root@master30:~# kubectl delete svc etcd
root@master30:~# kubectl delete pvc etcd-data-etcd-{0..2}
root@master30:~# rm -fr /shares/statefulset-etcd-data*

六、示例 3:部署 Redis 集群(1 主 2 从 + 哨兵)

需求:部署 3 节点 Redis 集群,实现固定网络标识、数据持久化、集群自动组网,支持故障自动切换。

步骤 1:创建 Headless Service

bash 复制代码
root@master30:~# cat > redis-service.yaml <<'EOF'
apiVersion: v1                         
kind: Service                           
metadata:
  name: redis                           # Service 名字(StatefulSet 里 serviceName 要和这个对应)
  namespace: statefulset
  labels:
    app: redis
spec:
  ports:
  - port: 6379                          # 第一个端口:Redis 数据端口(客户端连 Redis 用的)
    name: redis
  - port: 26379                         # 第二个端口:Sentinel 哨兵端口(哨兵之间互相通信+通知主从切换用的)
    name: sentinel
  clusterIP: None                       # 关键!Headless Service,给每个 Redis Pod 发固定 DNS
  selector:
    app: redis                          # 管带 app=redis 标签的 Pod

root@master30:~# kubectl apply -f redis-service.yaml

root@master30:~# kubectl get svc
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)              AGE
redis   ClusterIP   None         <none>        6379/TCP,26379/TCP   4s

步骤 2:创建 StatefulSet

bash 复制代码
root@master30:~# cat > redis-statefulset.yaml <<'EOF'
apiVersion: apps/v1                              
kind: StatefulSet                               
metadata:
  name: redis
  namespace: statefulset
spec:
  serviceName: redis                             # 绑定上面那个 Headless Service
  replicas: 3                                    # 3 个节点:1主2从 + 3个哨兵
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7.0-alpine                  # Redis 7.0 精简版镜像
        ports:
        - containerPort: 6379                    # Redis 数据端口
          name: redis
        - containerPort: 26379                   # Sentinel 哨兵端口
          name: sentinel
        command: ["/bin/sh", "-c"]               # 用 shell 执行后面的脚本
        args:
        - |
          # ↓↓↓ 第一步:从 Pod 名字里提取序号(redis-0 → 0, redis-1 → 1...)
          NODE_ID=$(hostname | awk -F'-' '{print $2}')
          
          # ↓↓↓ 第二步:生成 redis.conf 配置文件
          cat > /data/redis.conf << EOF
          bind 0.0.0.0                           # 监听所有网卡
          protected-mode no                      # 关闭保护模式(集群内部用)
          port 6379
          dir /data                              # 数据存 /data
          appendonly yes                         # 开启 AOF 持久化
          requirepass "redis123"                 # 访问密码
          masterauth "redis123"                  # 主从同步时的认证密码
          EOF
          
          # ↓↓↓ 第三步:如果不是 0 号节点,就当从节点,指向 redis-0
          if [ "$NODE_ID" != "0" ]; then
            echo "replicaof redis-0.redis.statefulset.svc.cluster.local 6379" >> /data/redis.conf
          fi
          
          # 启动 Redis 主进程(后台运行)
          redis-server /data/redis.conf &

          # ↓↓↓ 等 DNS 解析出来(redis-0 的地址能解析到了再继续)
          until nslookup redis-0.redis.statefulset.svc.cluster.local; do
            echo "Waiting for DNS resolution..."
            sleep 2
          done

          # ↓↓↓ 第四步:生成 sentinel.conf 哨兵配置文件
          cat > /data/sentinel.conf << EOF
          bind 0.0.0.0
          protected-mode no
          port 26379                             # 哨兵监听端口
          sentinel resolve-hostnames yes         # 允许用域名而不是 IP
          sentinel announce-hostnames yes
          # 监控名叫 mymaster 的主节点,地址是 redis-0,投票数=2(2个哨兵同意才算主挂了)
          sentinel monitor mymaster redis-0.redis.statefulset.svc.cluster.local 6379 2
          sentinel auth-pass mymaster redis123   # 连主节点的密码
          sentinel down-after-milliseconds mymaster 5000   # 5秒没响应就算主挂了
          sentinel failover-timeout mymaster 10000         # 故障转移超时时间
          EOF
          
          # 启动 Sentinel 哨兵进程(前台运行,容器不退出)
          redis-sentinel /data/sentinel.conf
        volumeMounts:
        - name: redis-data
          mountPath: /data                     # Redis 数据 + 配置文件都存这个卷
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

root@master30:~# kubectl apply -f redis-statefulset.yaml

root@master30:~# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
redis-0   1/1     Running   0          4m49s
redis-1   1/1     Running   0          4m46s
redis-2   1/1     Running   0          4m44s

root@master30:~# kubectl get sts
NAME    READY   AGE
redis   3/3     4m52s

步骤 3:验证部署

验证主从状态(redis-0 为主,2 个从节点):

bash 复制代码
root@master30:~# kubectl exec -it redis-0 -- redis-cli -a redis123 info replication
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
role:master
connected_slaves:2
slave0:ip=10.224.113.181,port=6379,state=online,offset=106182,lag=1
slave1:ip=10.224.19.24,port=6379,state=online,offset=106011,lag=1
master_failover_state:no-failover
master_replid:7048070355085a34084baef93981a849ae20db1c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:106353
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:106353

验证哨兵状态(能识别主节点):

bash 复制代码
root@master30:~# kubectl exec -it redis-0 -n statefulset -- redis-cli -p 26379 sentinel master mymaster
 1) "name"
 2) "mymaster"
 3) "ip"
 4) "redis-0.redis.statefulset.svc.cluster.local"
 5) "port"
 6) "6379"
 7) "runid"
 8) "21cc9a4d3a506fa9b04734b62c22f051c7fb9806"
 9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "43"
19) "last-ping-reply"
20) "43"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "9677"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "461721"
29) "config-epoch"
30) "0"
31) "num-slaves"
32) "2"
33) "num-other-sentinels"
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "10000"
39) "parallel-syncs"
40) "1"

验证主从数据同步:

bash 复制代码
root@master30:~# kubectl exec -it redis-0 -- redis-cli -a redis123 set test-key "redis-cluster-test"

root@master30:~# kubectl exec -it redis-1 -- redis-cli -a redis123 get test-key
"redis-cluster-test"

验证故障切换:

bash 复制代码
root@master30:~# kubectl delete pod redis-0

root@master30:~# kubectl exec -it redis-0 -- redis-cli -a redis123 info replication
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
role:slave
master_host:10.224.19.24
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:210705
slave_repl_offset:210705
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:72c5eb5bf94433adb77857bea5d3c631841d5cfb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210705
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:209970
repl_backlog_histlen:736

哨兵已完成主从切换,原主节点重启后自动降级为从节点。

步骤 4:清理环境

bash 复制代码
root@master30:~# kubectl delete svc redis
root@master30:~# kubectl delete sts redis
root@master30:~# kubectl delete pvc redis-data-redis-{0..2}
root@master30:~# kubectl delete pv redis-pv-{0..2}
root@master30:~# rm -fr /shares/redis/data-*/*

七、示例 4:部署 Cassandra 集群

需求:部署 3 节点 Cassandra 集群,实现固定网络标识、数据持久化、集群自动组网。

步骤 1:创建 Headless Service

bash 复制代码
root@master30:~# cat > cassandra-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: cassandra
  namespace: statefulset
  labels:
    app: cassandra
spec:
  ports:
  - port: 9042
    name: cql
  - port: 7000
    name: intra-node
  clusterIP: None
  selector:
    app: cassandra
EOF

root@master30:~# kubectl apply -f cassandra-service.yaml

root@master30:~# kubectl get svc
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)               AGE
cassandra   ClusterIP   None         <none>        9042/TCP,7000/TCP     4s

步骤 2:创建 StatefulSet

bash 复制代码
root@master30:~# cat > cassandra-statefulset.yaml <<'EOF'
apiVersion: apps/v1                              
kind: StatefulSet                                
metadata:
  name: cassandra
  namespace: statefulset
spec:
  serviceName: cassandra                         # 绑定 Headless Service(必须有)
  replicas: 3                                    # 3 个节点组成 Cassandra 集群
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      containers:
      - name: cassandra
        image: bitnami/cassandra:4.1.3           # Bitnami 打包的 Cassandra 镜像
        ports:
        - containerPort: 9042                     # CQL 端口(客户端连 Cassandra 用的)
          name: cql
        - containerPort: 7000                     # 节点间通信端口(gossip 协议同步集群信息)
          name: intra-node
        env:
        - name: CASSANDRA_CLUSTER_NAME
          value: "my-cassandra"                   # 集群名字(同一个集群必须同名)
        - name: CASSANDRA_SEEDS
          # 关键!种子节点------新节点加入集群时先找它报到
          # 这里指定 cassandra-0 当种子节点
          value: "cassandra-0.cassandra.statefulset.svc.cluster.local"
        - name: CASSANDRA_LISTEN_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name            # 监听地址 = Pod 名字(自动变成 DNS 名)
        - name: CASSANDRA_BROADCAST_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name            # 告诉其他节点"来找我用这个地址"
        - name: CASSANDRA_BROADCAST_RPC_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name            # 告诉客户端"连我用这个地址"
        - name: CASSANDRA_RPC_ADDRESS
          value: "0.0.0.0"                        # 监听所有网卡的客户端端口
        - name: CASSANDRA_NUM_TOKENS
          value: "256"                            # 每个节点分 256 个 token(数据分片)
        - name: CASSANDRA_REPLICATION_FACTOR
          value: "3"                              # 每份数据存 3 份(3 个节点各存一份)
        volumeMounts:
        - name: cassandra-data
          mountPath: /bitnami/cassandra           # 数据目录
        resources:
          requests:
            cpu: 500m
            memory: 2Gi
          limits:
            cpu: 1000m
            memory: 4Gi
        livenessProbe:
          exec:
            command: ["nodetool", "status"]       # 用 nodetool status 检查节点活没活
          initialDelaySeconds: 60                 # Cassandra 启动慢,等 60 秒再开始查
          periodSeconds: 10
        readinessProbe:
          exec:
            command: ["cqlsh", "-e", "DESCRIBE KEYSPACES;"]   # 能执行 CQL 查询才算 Ready
          initialDelaySeconds: 30
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

root@master30:~# kubectl apply -f cassandra-statefulset.yaml
root@master30:~# kubectl get pods
root@master30:~# kubectl get sts

步骤 3:验证部署

bash 复制代码
root@master30:~# kubectl exec -it cassandra-0 -- nodetool status

root@master30:~# kubectl exec -it cassandra-0 -- nodetool info

root@master30:~# kubectl exec -it cassandra-0 -- cqlsh -e "CREATE KEYSPACE IF NOT EXISTS test WITH replication = {'class':'SimpleStrategy','replication_factor':3};"
root@master30:~# kubectl exec -it cassandra-0 -- cqlsh -e "CREATE TABLE IF NOT EXISTS test.users (id int PRIMARY KEY, name text);"
root@master30:~# kubectl exec -it cassandra-0 -- cqlsh -e "INSERT INTO test.users (id, name) VALUES (1, 'laoma');"
root@master30:~# kubectl exec -it cassandra-1 -- cqlsh -e "SELECT * FROM test.users;"

root@master30:~# kubectl delete pod cassandra-0
root@master30:~# kubectl get pods -l app=cassandra
root@master30:~# kubectl exec -it cassandra-1 -- nodetool status

步骤 4:清理环境

bash 复制代码
root@master30:~# kubectl delete svc cassandra
root@master30:~# kubectl delete sts cassandra
root@master30:~# kubectl delete pvc cassandra-data-cassandra-{0..2}
root@master30:~# rm -fr /shares/statefulset-cassandra-data-cassandra-*

八、示例 5:部署 MySQL 集群

需求:部署 2 节点 MySQL,实现数据持久化、固定网络标识、Pod 重建后数据不丢失。

步骤 1:创建 Headless Service

bash 复制代码
root@master30:~# cat > mysql-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: statefulset
spec:
  selector:
    app: mysql  ## 匹配 StatefulSet 管理的 Pod 标签
  clusterIP: None  ## 无头服务,ClusterIP 设为 None
  ports:
  - port: 3306
    targetPort: 3306
    name: mysql-port  ## 端口名称,便于识别
EOF

root@master30:~# kubectl apply -f mysql-service.yaml

root@master30:~# kubectl get svc
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
mysql   ClusterIP   None         <none>        3306/TCP   2s

步骤 2:创建 StatefulSet

bash 复制代码
root@master30:~# cat > mysql-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: statefulset
spec:
  serviceName: mysql   # 关联上面创建的 Headless Service
  replicas: 2          # 2 个节点(1 主 1 从)
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        ports:
        - containerPort: 3306
          name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "123456"
        volumeMounts:
        - name: mysql-data            # 挂载存储卷,名称与 volumeClaimTemplates 一致
          mountPath: /var/lib/mysql   # MySQL 数据存储路径
  volumeClaimTemplates:               # PVC 模板,为每个 Pod 自动创建 PVC
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"  # 与动态卷置备的 StorageClass 一致
      resources:
        requests:
          storage: 10Gi
EOF

root@master30:~# kubectl apply -f mysql-statefulset.yaml

root@master30:~# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
mysql-0   1/1     Running   0          87s
mysql-1   1/1     Running   0          32s

root@master30:~# kubectl get sts
NAME    READY   AGE
mysql   2/2     4m9s

步骤 3:验证部署

bash 复制代码
root@master30:~# kubectl run test-pod --image busybox -- sleep 3600
root@master30:~# kubectl exec test-pod -- ping -c2 mysql-1.mysql
PING mysql-1.mysql (10.224.113.148): 56 data bytes
64 bytes from 10.224.113.148: seq=0 ttl=62 time=1.586 ms

--- mysql-1.mysql ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.586/1.586/1.586 ms

root@master30:~# kubectl exec -it mysql-1 -- mysql -u root -p123456 -e 'create database wordpress;'

root@master30:~# kubectl delete pod mysql-1
root@master30:~# kubectl get pods mysql-1
NAME      READY   STATUS    RESTARTS   AGE
mysql-1   1/1     Running   0          82s

root@master30:~# kubectl exec test-pod -- ping -c1 mysql-1.mysql
PING mysql-1.mysql (10.224.113.149): 56 data bytes
64 bytes from 10.224.113.149: seq=0 ttl=62 time=0.584 ms

--- mysql-1.mysql ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.584/0.584/0.584 ms

root@master30:~# kubectl exec -it mysql-1 -- mysql -u root -p123456 -e 'show databases;'
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| wordpress          |
+--------------------+

结论 :Pod IP 变了,但名字 mysql-1.mysql 没变,数据也没丢 ------ 这就是 StatefulSet 的价值。

步骤 4:配置主从(补充)

在主节点(mysql-0)上创建复制账号并查看 binlog 位置:

bash 复制代码
root@master30:~# kubectl exec -it mysql-0 -- mysql -u root -p123456 -e "
CREATE USER 'repl'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
SHOW MASTER STATUS;"

在从节点(mysql-1)上指向主节点并启动复制:

bash 复制代码
root@master30:~# kubectl exec -it mysql-1 -- mysql -u root -p123456 -e "
CHANGE MASTER TO
  MASTER_HOST='mysql-0.mysql.statefulset.svc.cluster.local',
  MASTER_USER='repl',
  MASTER_PASSWORD='123456',
  MASTER_LOG_FILE='mysql-bin.000003',
  MASTER_LOG_POS=769;
START SLAVE;
SHOW SLAVE STATUS\G"

预期输出:Slave_IO_Running 和 Slave_SQL_Running 均为 Yes ,说明主从同步正常。

其中 MASTER_LOG_FILE / MASTER_LOG_POS 需替换为主节点 SHOW MASTER STATUS 的实际输出。

步骤 5:清理环境

bash 复制代码
root@master30:~# kubectl delete pod test-pod
root@master30:~# kubectl delete svc mysql
root@master30:~# kubectl delete sts mysql
root@master30:~# kubectl delete pvc mysql-data-mysql-{0..1}
root@master30:~# rm -fr /shares/statefulset-mysql-data-mysql-*

九、StatefulSet 避坑总结

  1. 敏感信息用 Secret:生产环境中 MySQL 根密码、Redis 密码等必须用 Kubernetes Secret 管理,禁止明文写在 YAML 里。
  2. 缩容会删 Pod 不删 PVC :StatefulSet 缩容严格按序号从大到小删除 Pod;PVC 默认保留,若需释放存储要手动 kubectl delete pvc,操作前务必备份。
  3. PVC 模板改了不生效 :修改 volumeClaimTemplates(容量、storageClassName)后直接 kubectl apply 不会生效,需删除 PVC 和 Pod 重建(PV 为 Retain 时数据保留)。
  4. 存储选型 :本文统一使用 NFS(共享路径 /shares/),测试环境够用;生产环境建议高可用 NFS 集群或分布式存储(如 Ceph / Rook),避免 NFS 单点故障。
  5. 核心字段改不了 :StatefulSet 不支持动态修改 serviceName、PVC 模板等核心配置,需删除 StatefulSet 后重建(Pod 和 PVC 可保留,数据不丢失)。
  6. Headless Service 不能少 :没有它,Pod 就没有稳定的 DNS 记录,nginx-0.nginx 这种访问方式会直接失败。

环境清理

bash 复制代码
root@master30:~# kubectl delete ns statefulset

StatefulSet 故障排查速查

bash 复制代码
# Pod 卡在 Pending:多半是 PVC 没绑上
kubectl get pvc -n statefulset
kubectl describe pvc <claim-name> -n statefulset

# 有序部署卡住:看序号最小的那个 Pod 为什么没 Ready
kubectl describe pod <sts-name>-0 -n statefulset
kubectl logs <sts-name>-0 -n statefulset

# DNS 解析不通:确认 Headless Service 的 selector 与 Pod 标签一致
kubectl get svc -n statefulset
kubectl run dns-test --image busybox:1.36 -it --rm -- nslookup nginx-0.nginx.statefulset.svc.cluster.local

# 想暂停自动重建做排查(StatefulSet 特有的好功能)
kubectl scale sts <sts-name> --replicas=0 -n statefulset
# 排查完毕恢复
kubectl scale sts <sts-name> --replicas=3 -n statefulset

十、总结

StatefulSet 解决的其实是三个"稳定":

  • 名字稳定 :永远是 nginx-0、nginx-1、nginx-2,重建后编号不变;
  • 网络稳定 :靠 Headless Service,nginx-0.nginx 这个域名永远指向它,哪怕 IP 换了;
  • 存储稳定:靠 PVC 模板,每个 Pod 有专属卷,Pod 删了卷还在,重建自动重新挂载。

再加上有序(扩容 0→N、缩容 N→0、滚动更新倒序),数据库、中间件、消息队列这几类应用就有了在 K8s 上跑的基础。

最后提醒一句:状态尽量往外扔。 能用云厂商托管数据库就别自己 StatefulSet 跑 MySQL ------ 除非你真的清楚自己在做什么,以及备份、监控、故障转移方案都ready了。

相关推荐
acd120092 小时前
域名解析缓存带来的隐性故障,很多排查都漏掉了这一层
java·运维·nginx
mengge.cloud3 小时前
10-29-Kubernetes动态卷供应StatefulSet与ETCD
容器·kubernetes·etcd
小哈里3 小时前
【推理】NVIDIA DLI培训:大规模部署与优化 AI 推理(推理原理,K8S,网关层,pd分离,MoE,Dynamo)
人工智能·容器·kubernetes·nvidia·推理
枫叶轻语18 小时前
【Nginx 从入门到实战 07】root 与 alias:资源路径到底怎样对应?
nginx·全栈
acd120091 天前
Nginx 反向代理的缓冲区坑:后端响应大时接口偶发 502
java·运维·nginx
shangjian0071 天前
K8S学习-基础组件
学习·容器·kubernetes
Thanks_ks1 天前
常见 HTTP 状态码的坑:301/302、401/403、500/502 之间差的那一个词
计算机网络·nginx·http·面试·状态码·后端开发·浏览器缓存
wzq11_6661 天前
Kubernetes集群——工作负载
云原生·容器·kubernetes
Lsetea2 天前
Certbot webroot申请SSL证书总是404:Nginx挑战目录与路径映射检查
运维·nginx·https·ssl证书·certbot