Введение
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'
StorageClass по умолчанию динамически создаёт PersistentVolume, если в заявке не указан storageClassName. Благодаря этому в учебных манифестах не требуется настраивать зависящий от узла hostPath.
В обычной таблице найдите пометку (default) или значение true в столбце DEFAULT пользовательского формата. Имя provisioner является деталью реализации конкретной среды; важно лишь наличие класса по умолчанию.
В этой среде результат выглядит так:
NAME PROVISIONER ...
standard (default) k8s.io/minikube-hostpath ...
NAME PROVISIONER DEFAULT
standard k8s.io/minikube-hostpath true
Таким образом, standard — это класс по умолчанию. Поскольку StatefulSet не указывает storageClassName, Kubernetes выберет этот класс автоматически.
Создание StatefulSet и бессерверного Service
На этом этапе создайте бессерверный Service для стабильного DNS-имени и StatefulSet с отдельной заявкой для каждой реплики.
Этот манифест содержит несколько взаимосвязанных полей. Изучите их перед применением:
clusterIP: Noneделает Service бессерверным, чтобы DNS мог обращаться к отдельным Pod.serviceName: stateful-webсвязывает StatefulSet с этим Service.volumeClaimTemplatesсоздаёт отдельную заявку, имя которой формируется из имени шаблона и порядкового номера Pod.volumeMountsподключает собственную заявку каждого Pod к каталогу документов NGINX.
Метки по-прежнему обеспечивают знакомый выбор Pod для Service, с которым вы познакомились в первом курсе.
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 сначала создаёт Pod с порядковым номером 0, а затем Pod с номером 1, поэтому второй 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. Это означает, что Kubernetes нашёл или динамически выделил хранилище, соответствующее каждой заявке.
Сообщения о развёртывании показывают последовательное создание Pod, после чего отображаются две заявки:
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
Идентификаторы созданных томов при каждом запуске отличаются. Обратите внимание на стабильные имена заявок, статус Bound, запрошенный объём и класс хранилища.
Запись данных для конкретных Pod
На этом этапе запишите разные данные в заявку, подключённую к каждому Pod.
Конструкция kubectl exec ... -- sh -c нужна потому, что перенаправление вывода (>) должно выполняться внутри контейнера. Без sh -c локальная оболочка попыталась бы перенаправить вывод на виртуальной машине, а не внутри 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 с его заявкой, превращая связь между 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 и восстановление его данных
На этом этапе удалите Pod с порядковым номером 0 и дождитесь, пока StatefulSet восстановит тот же идентификатор.
Удаление Pod, которым управляет контроллер, не равно уменьшению требуемого числа реплик. StatefulSet по-прежнему должен поддерживать два Pod, поэтому он создаёт замену с именем stateful-web-0 и повторно подключает заявку, закреплённую за этим порядковым номером.
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.
Если бы файл находился за пределами подключённого каталога, он исчез бы вместе с файловой системой старого контейнера. Сохранность данных зависит от пути монтирования, а не только от имени StatefulSet.
Итоги
Вы связали StorageClass по умолчанию, динамическое создание PVC, бессерверный Service, стабильные порядковые номера StatefulSet и отдельное хранилище для каждого Pod. После замены Pod сохранились и его идентификатор, и данные.


