使用持久化存储运行有状态应用

KubernetesBeginner
立即练习

简介

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 后,其身份和数据都得以保留。