Запуск приложений с сохранением состояния и постоянным хранилищем

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'

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 сохранились и его идентификатор, и данные.