StatefulSet - 有状态应用

StatefulSet - 有状态应用

为什么需要 StatefulSet

Deployment 适合无状态应用,但对于有状态应用(如数据库、消息队列)存在问题:

无状态 vs 有状态

无状态应用(Stateless)

  • Pod 可以随意替换
  • Pod 之间完全等价
  • 适合使用 Deployment

有状态应用(Stateful)

  • 每个 Pod 有唯一身份
  • Pod 之间不完全等价
  • 需要稳定的网络标识
  • 需要持久化存储
  • 需要有序部署和扩展

StatefulSet 的特点

  • 稳定的网络标识:Pod 名称固定
  • 稳定的持久化存储:每个 Pod 独立的 PVC
  • 有序部署和扩展:按顺序创建和删除
  • 有序滚动更新:可控的更新过程

基础示例

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None  # Headless Service
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"  # 关联的 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
          name: web
kubectl apply -f statefulset.yaml

# 查看 Pod
kubectl get pods -l app=nginx

# 输出
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          10s
web-1   1/1     Running   0          8s
web-2   1/1     Running   0          6s

注意 Pod 名称的规律:<statefulset-name>-<ordinal>

StatefulSet 特性

1. 稳定的网络标识

每个 Pod 有固定的 DNS 名称:

<pod-name>.<service-name>.<namespace>.svc.cluster.local

示例

# Pod web-0 的 DNS
web-0.nginx.default.svc.cluster.local

# 从集群内访问
ping web-0.nginx
curl http://web-0.nginx

2. 有序部署

Pod 按序号顺序创建:

1. 创建 web-0,等待 Running 和 Ready
2. 创建 web-1,等待 Running 和 Ready
3. 创建 web-2,等待 Running 和 Ready

3. 有序删除

缩容时按逆序删除:

kubectl scale statefulset web --replicas=1

# 删除顺序
1. 删除 web-2
2. 删除 web-1
3. 保留 web-0

4. 持久化存储

每个 Pod 有独立的 PVC:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
# 查看创建的 PVC
kubectl get pvc

# 输出
NAME        STATUS   VOLUME    CAPACITY   ACCESS MODES
www-web-0   Bound    pv-001    1Gi        RWO
www-web-1   Bound    pv-002    1Gi        RWO
www-web-2   Bound    pv-003    1Gi        RWO

部署 MySQL 集群

完整示例

# Headless Service
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None
  selector:
    app: mysql
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      # 初始化容器:生成服务器 ID
      - name: init-mysql
        image: mysql:8.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          # 从 Pod 序号生成 server-id
          [[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "password"
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: conf
        emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

访问 MySQL

# 访问 master(通常是 mysql-0)
mysql -h mysql-0.mysql -uroot -ppassword

# 访问特定实例
mysql -h mysql-1.mysql -uroot -ppassword
mysql -h mysql-2.mysql -uroot -ppassword

更新策略

RollingUpdate(默认)

逐个更新 Pod,从最大序号开始:

spec:
  updateStrategy:
    type: RollingUpdate
# 更新镜像
kubectl set image statefulset/web nginx=nginx:1.22

# 更新顺序
1. 更新 web-2
2. 更新 web-1
3. 更新 web-0

OnDelete

手动删除 Pod 才会更新:

spec:
  updateStrategy:
    type: OnDelete
# 删除 Pod 触发更新
kubectl delete pod web-0
kubectl delete pod web-1
kubectl delete pod web-2

分区更新

只更新序号 >= partition 的 Pod:

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2  # 只更新 web-2
# 设置 partition
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'

# 此时只有 web-2 会更新

灰度发布流程:

# 1. 只更新 web-2(金丝雀)
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image statefulset/web nginx=nginx:1.22

# 2. 观察 web-2,确认无问题

# 3. 更新 web-1
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}'

# 4. 更新全部
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'

部署 Redis 集群

apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
data:
  redis.conf: |
    cluster-enabled yes
    cluster-config-file /data/nodes.conf
    cluster-node-timeout 5000
    appendonly yes
---
apiVersion: v1
kind: Service
metadata:
  name: redis-cluster
spec:
  clusterIP: None
  ports:
  - port: 6379
    name: client
  - port: 16379
    name: gossip
  selector:
    app: redis-cluster
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
spec:
  serviceName: redis-cluster
  replicas: 6
  selector:
    matchLabels:
      app: redis-cluster
  template:
    metadata:
      labels:
        app: redis-cluster
    spec:
      containers:
      - name: redis
        image: redis:7.0-alpine
        ports:
        - containerPort: 6379
          name: client
        - containerPort: 16379
          name: gossip
        command:
        - redis-server
        - /conf/redis.conf
        - --cluster-announce-ip
        - $(POD_IP)
        env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: conf
          mountPath: /conf
        - name: data
          mountPath: /data
      volumes:
      - name: conf
        configMap:
          name: redis-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

初始化 Redis 集群:

# 等待所有 Pod 就绪
kubectl wait --for=condition=ready pod -l app=redis-cluster --timeout=300s

# 创建集群
kubectl exec -it redis-cluster-0 -- redis-cli --cluster create \
  redis-cluster-0.redis-cluster:6379 \
  redis-cluster-1.redis-cluster:6379 \
  redis-cluster-2.redis-cluster:6379 \
  redis-cluster-3.redis-cluster:6379 \
  redis-cluster-4.redis-cluster:6379 \
  redis-cluster-5.redis-cluster:6379 \
  --cluster-replicas 1

管理 StatefulSet

扩缩容

# 扩容
kubectl scale statefulset web --replicas=5

# 缩容
kubectl scale statefulset web --replicas=2

注意:缩容不会删除 PVC,需要手动清理。

删除 Pod

# 删除 Pod(会自动重建)
kubectl delete pod web-0

# 级联删除(删除 StatefulSet 和 Pod)
kubectl delete statefulset web

# 仅删除 StatefulSet(保留 Pod)
kubectl delete statefulset web --cascade=orphan

清理资源

# 删除 StatefulSet
kubectl delete statefulset web

# 删除 Service
kubectl delete service nginx

# 删除 PVC
kubectl delete pvc www-web-0 www-web-1 www-web-2

常用命令

# 创建
kubectl apply -f statefulset.yaml

# 查看
kubectl get statefulset
kubectl describe statefulset <name>
kubectl get pods -l app=<label>

# 扩缩容
kubectl scale statefulset <name> --replicas=<count>

# 更新
kubectl set image statefulset/<name> <container>=<image>
kubectl rollout status statefulset/<name>

# 回滚
kubectl rollout undo statefulset/<name>
kubectl rollout history statefulset/<name>

# 删除
kubectl delete statefulset <name>

# 查看 PVC
kubectl get pvc -l app=<label>

最佳实践

1. 使用 Headless Service

StatefulSet 需要配合 Headless Service:

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  clusterIP: None  # 必须设置为 None
  selector:
    app: nginx

2. 配置 Pod Management Policy

spec:
  podManagementPolicy: Parallel  # 并行创建 Pod
  # 或
  podManagementPolicy: OrderedReady  # 顺序创建(默认)

3. 设置合适的更新策略

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 0

4. 配置 PVC 保留策略

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain  # StatefulSet 删除时保留 PVC
    whenScaled: Delete   # 缩容时删除 PVC

5. 健康检查

spec:
  template:
    spec:
      containers:
      - name: app
        livenessProbe:
          tcpSocket:
            port: 3306
          initialDelaySeconds: 30
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - mysqladmin ping -h 127.0.0.1

6. 资源限制

spec:
  template:
    spec:
      containers:
      - name: mysql
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 1000m
            memory: 2Gi

StatefulSet vs Deployment

特性 StatefulSet Deployment
Pod 名称 固定有序 随机
网络标识 稳定 不稳定
存储 独立 PVC 共享或无
部署顺序 有序 并行
扩缩容 有序 并行
适用场景 有状态应用 无状态应用

小结

StatefulSet 是管理有状态应用的核心资源:

核心特性

  • 稳定标识:Pod 名称和网络标识固定
  • 持久化存储:每个 Pod 独立的 PVC
  • 有序操作:按顺序部署、扩展、更新
  • 可控更新:支持分区更新和灰度发布

适用场景

  • 数据库(MySQL、PostgreSQL、MongoDB)
  • 消息队列(Kafka、RabbitMQ)
  • 分布式存储(Ceph、GlusterFS)
  • 缓存集群(Redis、Memcached)

关键配置

  • Headless Service 提供网络标识
  • volumeClaimTemplates 提供持久化存储
  • updateStrategy 控制更新策略

下一章我们将学习 DaemonSet 和 Job,处理特殊工作负载。