简介
Deployment 会将各个副本视为可以相互替换的实例。而有状态应用通常需要稳定的 Pod 名称、可预测的启动顺序,以及在 Pod 被替换后仍能保留的存储。StatefulSet 将这些稳定身份与每个 Pod 独立的存储声明结合起来。
你将检查动态存储,部署一个包含两个副本的 StatefulSet,写入数据,替换一个 Pod,并验证其身份和数据都能保留。
请分别关注三个身份:StatefulSet 管理期望的副本数;每个 Pod 会获得一个稳定的序号,例如 -0;每个序号又会对应一个独立的 PersistentVolumeClaim。Pod 可以被替换,但对应的声明和其中存储的数据仍会保留。
检查动态存储
在本步骤中,先检查集群的 StorageClass,再申请存储。
PersistentVolumeClaim 描述工作负载需要什么样的存储;StorageClass 描述集群如何提供这种存储。先查看可用的存储类,可以避免创建一个永远无法绑定的声明。
cd /home/labex/project/stateful-storage
kubectl get storageclasses
kubectl get storageclass -o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'
当声明中省略 storageClassName 时,默认 StorageClass 会动态创建 PersistentVolume。这样,实验清单就不需要依赖节点特定的 hostPath 配置。
请在普通表格中查找 (default),或在自定义的 DEFAULT 列中查找 true。Provisioner 名称属于当前环境的实现细节;这里真正重要的是集群中存在默认存储类。
在本环境中,结果如下:
NAME PROVISIONER ...
standard (default) k8s.io/minikube-hostpath ...
NAME PROVISIONER DEFAULT
standard k8s.io/minikube-hostpath true
因此,standard 是默认存储类。由于 StatefulSet 中会省略 storageClassName,Kubernetes 将自动选择这个存储类。
创建 StatefulSet 和无头 Service
在本步骤中,创建一个用于提供稳定 DNS 的无头 Service,以及一个为每个副本创建独立声明的 StatefulSet。
这个清单包含多个相互关联的字段。应用之前,请先阅读这些字段的作用:
clusterIP: None会将 Service 设置为无头 Service,使 DNS 能够表示各个 Pod。serviceName: stateful-web将 StatefulSet 与该 Service 关联起来。volumeClaimTemplates会根据模板名称和 Pod 序号,为每个副本创建一个声明。volumeMounts会将每个 Pod 自己的声明挂载到 NGINX 的文档目录。
这些标签仍然使用你在第一门课程中学到的 Service 到 Pod 的选择机制。
cd /home/labex/project/stateful-storage
cat <<'EOF' > stateful-web.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-stateful}
---
apiVersion: v1
kind: Service
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
clusterIP: None
selector: {app: stateful-web}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
serviceName: stateful-web
replicas: 2
selector: {matchLabels: {app: stateful-web}}
template:
metadata: {labels: {app: stateful-web}}
spec:
containers:
- name: web
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports: [{name: http, containerPort: 80}]
volumeMounts: [{name: web-data, mountPath: /usr/share/nginx/html}]
volumeClaimTemplates:
- metadata: {name: web-data}
spec:
accessModes: [ReadWriteOnce]
resources: {requests: {storage: 100Mi}}
EOF
应用清单并等待按顺序完成的发布。StatefulSet 通常会先创建序号为 0 的 Pod,再创建序号为 1 的 Pod,因此第二个 Pod 可能会稍晚出现:
kubectl apply -f stateful-web.yaml
kubectl rollout status statefulset/stateful-web -n practice-stateful --timeout=90s
kubectl get pods,pvc -n practice-stateful
Pod 名称使用稳定的序号:stateful-web-0 和 stateful-web-1。每个声明的名称都包含模板名称、Pod 名称和序号。
你应该会看到 web-data-stateful-web-0 和 web-data-stateful-web-1 两个声明,并且它们的状态都是 Bound。Bound 表示 Kubernetes 已找到或动态创建了满足相应请求的存储。
发布过程中的消息会显示有序创建过程,随后列出两个声明:
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
pod/stateful-web-0 1/1 Running ...
pod/stateful-web-1 1/1 Running ...
persistentvolumeclaim/web-data-stateful-web-0 Bound ... 100Mi RWO standard
persistentvolumeclaim/web-data-stateful-web-1 Bound ... 100Mi RWO standard
每次运行时生成的卷 ID 都可能不同。请重点关注稳定的声明名称、Bound 状态、请求的容量以及存储类。
写入 Pod 专属的持久化数据
在本步骤中,向每个 Pod 挂载的声明写入不同的内容。
这里使用 kubectl exec ... -- sh -c,是因为输出重定向符号(>)必须在容器内部执行。如果不使用 sh -c,本地 Shell 会尝试在虚拟机上重定向输出,而不是在 Pod 内部执行。
cd /home/labex/project/stateful-storage
kubectl exec stateful-web-0 -n practice-stateful -- sh -c 'echo persistent-zero > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-1 -n practice-stateful -- sh -c 'echo persistent-one > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl exec stateful-web-1 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pods -n practice-stateful -o custom-columns='POD:.metadata.name,HOSTNAME:.spec.hostname,CLAIM:.spec.volumes[0].persistentVolumeClaim.claimName'
两个 Pod 运行的是同一个镜像,但挂载了不同的声明,因此它们的数据并不共享,也不能相互替代。
两个 cat 命令应该返回不同的字符串。随后显示的自定义表格会将每个稳定的 Pod 名称与其声明对应起来,从而直观展示存储之间的关联关系。
persistent-zero
persistent-one
POD HOSTNAME CLAIM
stateful-web-0 stateful-web-0 web-data-stateful-web-0
stateful-web-1 stateful-web-1 web-data-stateful-web-1
序号会出现在这三列中。重复出现的 0 或 1 就是将 Pod 与其专属存储连接起来的身份标识。
替换 Pod 并恢复数据
在本步骤中,删除序号为 0 的 Pod,让 StatefulSet 重新创建具有相同身份的 Pod。
删除由控制器管理的 Pod,并不等同于减少期望的副本数。StatefulSet 仍然要求运行两个 Pod,因此它会创建一个名为 stateful-web-0 的替代 Pod,并重新挂载为该序号保留的声明。
cd /home/labex/project/stateful-storage
kubectl delete pod stateful-web-0 -n practice-stateful --wait=true
kubectl wait --for=condition=Ready pod/stateful-web-0 -n practice-stateful --timeout=90s
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pod stateful-web-0 -n practice-stateful -o jsonpath='pod={.metadata.name} claim={.spec.volumes[0].persistentVolumeClaim.claimName}{"\n"}'
虽然 Pod 对象是新创建的,但其名称和声明关联关系保持不变。文件能够恢复,证明数据属于 PersistentVolume,而不是已删除容器的文件系统。
pod "stateful-web-0" deleted from practice-stateful namespace
pod/stateful-web-0 condition met
persistent-zero
pod=stateful-web-0 claim=web-data-stateful-web-0
最重要的一行是 persistent-zero:它在删除 Pod 之前写入,并从替换后的 Pod 中成功读取。
如果文件写在挂载目录之外,它就会随着旧容器的文件系统一同消失。数据能否持久化取决于挂载路径,而不仅仅是 StatefulSet 的名称。
总结
你已经将默认 StorageClass、动态 PVC 配置、无头 Service、稳定的 StatefulSet 序号以及 Pod 独立存储连接起来。替换 Pod 后,其身份和数据都得以保留。


