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) |
三句话总结:
- 无状态核心:实例"无记忆",可随意替换/扩容,是云原生优先推荐的应用形态;
- 有状态核心:实例"有记忆",依赖持久化和固定身份,需通过 StatefulSet 等组件保障稳定性;
- 最佳实践:将应用拆分为「无状态业务层 + 有状态数据层」,既提升扩展能力,又保障数据安全。
三、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 避坑总结
- 敏感信息用 Secret:生产环境中 MySQL 根密码、Redis 密码等必须用 Kubernetes Secret 管理,禁止明文写在 YAML 里。
- 缩容会删 Pod 不删 PVC :StatefulSet 缩容严格按序号从大到小删除 Pod;PVC 默认保留,若需释放存储要手动
kubectl delete pvc,操作前务必备份。 - PVC 模板改了不生效 :修改
volumeClaimTemplates(容量、storageClassName)后直接kubectl apply不会生效,需删除 PVC 和 Pod 重建(PV 为 Retain 时数据保留)。 - 存储选型 :本文统一使用 NFS(共享路径
/shares/),测试环境够用;生产环境建议高可用 NFS 集群或分布式存储(如 Ceph / Rook),避免 NFS 单点故障。 - 核心字段改不了 :StatefulSet 不支持动态修改
serviceName、PVC 模板等核心配置,需删除 StatefulSet 后重建(Pod 和 PVC 可保留,数据不丢失)。 - 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了。