Kubernetes: PersistentVolume и PersistentVolumeClaim — обзор и примеры

Автор: | 07/31/2020
 

Для работы с дисками для данных, которые должны храниться постоянно, Kubenetes предоставляет два типа ресурсов — PersistentVolume и PersistentVolumeClaim.

PersistentVolume — непосредственно система хранения, раздел на жёстком диске, например AWS EBS, подключенном к одному из AWS EC2, и с точки зрения этого кластера является таким же ресурсом как, например, Kubernetes Worker Node.

PersistentVolumeClaim — запрос от пользователя на использование такого диска, аналог Kubernetes pod — поды используют ресурсы Worker Node, а PVC — ресурсы PersistentVolume. По аналогии же с подами — поды запрашивают у рабочей ноды ЦПУ и память, а PVC — необходимый им размер диска, и тип доступа — ReadWriteOnce, ReadOnlyMany или ReadWriteMany, см. AccessModes.

PersistentVolume могут быть созданы двумя способами — динамически (рекомендуемый способ), и статически.

При статичном методе сначала создаётся набор дисков, например EBS, которые затем используются кластером для PersistentVolumeClaim.

В случе, если для PersistentVolumeClaim не удалось найди подходящий PV — кластер может создать отдельный PV конкретно для этого PVC — это и будет динамическое создание PVC.

При этом в PVC должен быть задан Storage Class, и такой класс должен поддерживаться кластером.

К примеру, для AWS EKS по-умолчанию создан StorageClass gp2:

kubectl get storageclass
NAME            PROVISIONER             AGE
gp2 (default)   kubernetes.io/aws-ebs   64d

Типы дисков

Для понимания роли PersistentVolume — рассмотрим доступные системы хранения:

  • Node-local хранение (emptyDir и hostPath)
  • Cloud volumes (например, awsElasticBlockStore, gcePersistentDisk и azureDiskVolume)
  • File-sharing volumes, такие как Network File System
  • Distributed-file systems (например, CephFS, RBD и GlusterFS)
  • специальные типы разделов, такие как PersistentVolumeClaim, secret и gitRepo

emptyDir и hostPath подключаются к подам, и хранят данные или в памяти, или на диске. Но т.к. они зависимы от подов, их содержимое доступно только пока работает контейнер, и когда контейнере останавливается — данные удаляются.

Cloud volumes, NFS и PersistentVolume разделы независимы от подов, и располагаются вне их.

Создание PersistentVolumeClaim

Static PersistentVolume provisioning

Создание EBS

При Static provisioning — сначала создаётся AWS EBS, и затем объект PersistentVolume, которому явно указывается использование этого EBS.

Создаём EBS:

aws ec2 --profile arseniy --region us-east-2 create-volume --availability-zone us-east-2a --size 50
{
"AvailabilityZone": "us-east-2a",
"CreateTime": "2020-07-29T13:10:12.000Z",
"Encrypted": false,
"Size": 50,
"SnapshotId": "",
"State": "creating",
"VolumeId": "vol-0928650905a2491e2",
"Iops": 150,
"Tags": [],
"VolumeType": "gp2"
}

Сохраняем его ID — «vol-0928650905a2491e2».

Создание PersistentVolume

Описываем манифест, назовём его pv-static.yaml:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-static
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: gp2
  awsElasticBlockStore:
    fsType: ext4
    volumeID: vol-0928650905a2491e2

Тут:

  • capacity: размер диска
  • accessModes: типа доступа, тут ReadWriteOnce — раздел может быть смонтирован только к одной рабочей ноде с правами чтения/записи
  • storageClassName: тип хранилища, см. ниже
  • awsElasticBlockStore: тип используемого диска
    • fsType: тип файловой системы
    • volumeID: ID AWS EBS диска

Создаём PersistentVolume:

kubectl apply -f pv-static.yaml
persistentvolume/pv-static created

Проверяем:

kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                                                                  STORAGECLASS   REASON   AGE
pv-static                                  5Gi        RWO            Retain           Available                                                                                                  69s

StorageClass

Параметр storageClassName определяет тип хранилища.

И для PVC, и для PV должен быть задан один и тот же класс, иначе PVC не подключит PV, и STATUS такого PVC будет Pending.

Если для PVC не задан StorageClass — будет использован дефолтный:

kubectl get storageclass -o wide
NAME            PROVISIONER             AGE
gp2 (default)   kubernetes.io/aws-ebs   65d

При этом если StorageClass не указан для PV — он будет создан без указания класса, и PVC с классом по-умолчанию его не подключит, а выдаст ошибку «Cannot bind to requested volume «pvname»: storageClassName does not match«:

...
Events:
Type       Reason          Age                  From                         Message
----       ------          ----                 ----                         -------
Warning    VolumeMismatch  12s (x17 over 4m2s)  persistentvolume-controller  Cannot bind to requested volume "pvname": storageClassName does not match
...

См. документацию тут>>> и тут>>>.

Создание PersistentVolumeClaim

Добавляем PersistentVolumeClaim, который будет использовать этот PV:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-static
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  volumeName: pv-static

Создаём его:

kubectl apply -f pvc-static.yaml
persistentvolumeclaim/pvc-static created

Проверяем:

kubectl get pvc pvc-static
NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-static   Bound    pv-static   5Gi        RWO            gp2            31s

Dynamic PersistentVolume provisioning

Динамическое создание PersistentVolume аналогично статическому с той разницей, что мы не создаём EBS, и не создаём отдельного ресурса PersistentVolume — вместо этого мы опишем PersistentVolumeClaim, который самостоятельно создаст EBS, и смонтирует его к EC2 WorkerNode:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-dynamic
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

Создаём PVC:

kubectl apply -f pvc-dynamic.yaml
persistentvolumeclaim/pvc-dynamic created

Проверяем:

kubectl get pvc pvc-dynamic
NAME          STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-dynamic   Pending                                      gp2            45s

Но почему статус Pending? Смотрим события:

kubectl describe pvc pvc-dynamic
...
Events:
Type       Reason                Age               From                         Message
----       ------                ----              ----                         -------
Normal     WaitForFirstConsumer  1s (x4 over 33s)  persistentvolume-controller  waiting for first consumer to be created before binding
Mounted By:  <none>

WaitForFirstConsumer

Проверим ещё раз наш дефолтный StorageClass:

kubectl describe sc gp2
Name:            gp2
IsDefaultClass:  Yes
...
Provisioner:           kubernetes.io/aws-ebs
Parameters:            fsType=ext4,type=gp2
...
VolumeBindingMode:     WaitForFirstConsumer
Events:                <none>

Тут VolumeBindingMode определяет, как именно будет создаваться PersistentVolume. При значении Immediate — PV будет создан сразу же при появлении соотвествующего PVC, а при WaitForFirstConsumer — кластер сначала ожидает появления пода, который запросит этот PV, и затем, в зависимости от AvailbiltyZone рабочей ноды, на которой был запущен под, создёт PV.

Давайте создадим поды, которые будут использовать эти PVC.

Использование PersistentVolumeClaim в Pod

Dynamic PersistentVolumeClaim

Опишем под, который будет использовать наш dynamic PVC:

apiVersion: v1
kind: Pod
metadata:
  name: pv-dynamic-pod
spec:
  volumes:
    - name: pv-dynamic-storage
      persistentVolumeClaim:
        claimName: pvc-dynamic
  containers:
    - name: pv-dynamic-container
      image: nginx
      ports:
        - containerPort: 80
          name: "nginx"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-dynamic-storage

Тут:

  • volumes:
    • persistentVolumeClaim:
      • claimName: указываем имя PVC, который будет запрошен создаваемым подом для создания раздела
  • containers:
    • volumeMounts: монтируем volume с именем pv-dynamic-storage в каталог /usr/share/nginx/html

Создаём под:

kubectl apply -f pv-pods.yaml
pod/pv-dynamic-pod created

Проверяем ещё раз наш PVC:

kubectl get pvc pvc-dynamic
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-dynamic   Bound    pvc-6d024b40-a239-4c35-8694-f060bd117053   5Gi        RWO            gp2            21h

Теперь у нас появился созданный Volume с ID pvc-6d024b40-a239-4c35-8694-f060bd117053 — проверяем его:

kubectl describe pvc pvc-dynamic
Name:          pvc-dynamic
Namespace:     default
StorageClass:  gp2
Status:        Bound
Volume:        pvc-6d024b40-a239-4c35-8694-f060bd117053
...
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      5Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Events:        <none>
Mounted By:    pv-dynamic-pod

Проверим раздел:

kubectl describe pv pvc-6d024b40-a239-4c35-8694-f060bd117053
Name:              pvc-6d024b40-a239-4c35-8694-f060bd117053
...
StorageClass:      gp2
Status:            Bound
Claim:             default/pvc-dynamic
Reclaim Policy:    Delete
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          5Gi
Node Affinity:
Required Terms:
Term 0:        failure-domain.beta.kubernetes.io/zone in [us-east-2b]
failure-domain.beta.kubernetes.io/region in [us-east-2]
Message:
Source:
Type:       AWSElasticBlockStore (a Persistent Disk resource in AWS)
VolumeID:   aws://us-east-2b/vol-040a5e004876f1a40
FSType:     ext4
Partition:  0
ReadOnly:   false
Events:         <none>

И vol-040a5e004876f1a40 в AWS:

aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-040a5e004876f1a40 --output json
{
"Volumes": [
{
"Attachments": [
{
"AttachTime": "2020-07-30T11:08:29.000Z",
"Device": "/dev/xvdcy",
"InstanceId": "i-0a3225e9fe7cb7629",
"State": "attached",
"VolumeId": "vol-040a5e004876f1a40",
"DeleteOnTermination": false
}
],
...

Проверим в самом поде:

kk exec -ti pv-dynamic-pod bash
root@pv-dynamic-pod:/# lsblk
NAME          MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1       259:0    0  50G  0 disk
|-nvme0n1p1   259:1    0  50G  0 part /etc/hosts
`-nvme0n1p128 259:2    0   1M  0 part
nvme1n1       259:3    0   5G  0 disk /usr/share/nginx/html

nvme1n1 — наш диск.

Запишем данные:

root@pv-dynamic-pod:/# echo Test > /usr/share/nginx/html/index.html

Убиваем под:

kk delete pod pv-dynamic-pod
pod "pv-dynamic-pod" deleted

Создадим заново:

kubectl apply -f pv-pods.yaml
pod/pv-dynamic-pod created

Проверяем данные:

kk exec -ti pv-dynamic-pod cat /usr/share/nginx/html/index.html
Test

Всё на месте.

Static PersistentVolumeClaim

Теперь попробуем использовать статичный PV.

Используем тот же манифест того же PV, который использовали ранее, pv-static.yaml:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-static
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: gp2
  awsElasticBlockStore:
    fsType: ext4
    volumeID: vol-0928650905a2491e2

И берём тот же PVC — pvc-static.yaml:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-static
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  volumeName: pv-static

Создаём PV:

kk apply -f pv-static.yaml
persistentvolume/pv-static created

Проверяем:

kk get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                                                                  STORAGECLASS   REASON   AGE
pv-static                                  5Gi        RWO            Retain           Available                                                                          gp2                     58s
...

Создаём PVC:

kk apply -f pvc-static.yaml
persistentvolumeclaim/pvc-static created

Проверяем:

kk get pvc pvc-static
NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-static   Bound    pv-static   5Gi        RWO            gp2            9s

STATUS Bound — значит PVC нашёл свой PV, и подключился к нему.

Pod nodeAffinity

Определяем, в какой AvailabilityZone расположен наш EBS:

aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-0928650905a2491e2 --query '[Volumes[*].AvailabilityZone]'  --output text
us-east-2a

us-east-2a — окей, значит, нам надо и под создавать в той же AvailabilityZone.

Пишем манифест для пода:

apiVersion: v1
kind: Pod
metadata:
  name: pv-static-pod
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: failure-domain.beta.kubernetes.io/zone
            operator: In
            values:
            - us-east-2a
  volumes:
    - name: pv-static-storage
      persistentVolumeClaim:
        claimName: pvc-static
  containers:
    - name: pv-static-container
      image: nginx
      ports:
        - containerPort: 80
          name: "nginx"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-static-storage

В отличии от Dynamic PVC — тут через nodeAffinity мы явно задаём поиск ноды для этого пода в зоне us-east-2a.

Создаём под:

kk apply -f pv-pod-stat.yaml
pod/pv-static-pod created

Проверяем события:

0s    Normal   Scheduled   Pod   Successfully assigned default/pv-static-pod to ip-10-3-47-58.us-east-2.compute.internal
0s    Normal   SuccessfulAttachVolume   Pod   AttachVolume.Attach succeeded for volume "pv-static"
0s    Normal   Pulling   Pod   Pulling image "nginx"
0s    Normal   Pulled   Pod   Successfully pulled image "nginx"
0s    Normal   Created   Pod   Created container pv-static-container
0s    Normal   Started   Pod   Started container pv-static-container

Разделы в самом поде:

kk exec -ti pv-static-pod bash
root@pv-static-pod:/# lsblk
NAME          MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1       259:0    0  50G  0 disk
|-nvme0n1p1   259:1    0  50G  0 part /etc/hosts
`-nvme0n1p128 259:2    0   1M  0 part
nvme1n1       259:3    0  50G  0 disk /usr/share/nginx/html

nvme1n1 смонтирован, всё работает.

PersistentVolume nodeAffinity

Другой вариант — PV nodeAffinity.

В таком случае, при создании пода, который использует этот PV — Kubernetes сначала проверит, к каким рабочим нодам этот PV можно подключить, и после запустит под на одной из доступных для этого PV ноде.

В описании пода убираем nodeAffinity:

apiVersion: v1
kind: Pod
metadata:
  name: pv-static-pod
spec:
  volumes:
    - name: pv-static-storage
      persistentVolumeClaim:
        claimName: pvc-static
  containers:
    - name: pv-static-container
      image: nginx
      ports:
        - containerPort: 80
          name: "nginx"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-static-storage

И добавляем его к самому PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-static
spec:
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: failure-domain.beta.kubernetes.io/zone
          operator: In
          values:
          - us-east-2a    
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: gp2
  awsElasticBlockStore:
    fsType: ext4
    volumeID: vol-0928650905a2491e2

Создаём PV:

kk apply -f pv-static.yaml
persistentvolume/pv-static created

Создаём PVC — там ничего не менялось:

kk apply -f pvc-static.yaml
persistentvolumeclaim/pvc-static created

Создаём под:

kk apply -f pv-pod-stat.yaml
pod/pv-static-pod created

Проверяем логи:

0s    Normal   Scheduled   Pod   Successfully assigned default/pv-static-pod to ip-10-3-47-58.us-east-2.compute.internal
0s    Normal   SuccessfulAttachVolume   Pod   AttachVolume.Attach succeeded for volume "pv-static"
0s    Normal   Pulling   Pod   Pulling image "nginx"
0s    Normal   Pulled   Pod   Successfully pulled image "nginx"
0s    Normal   Created   Pod   Created container pv-static-container
0s    Normal   Started   Pod   Started container pv-static-container

Удаление PersistentVolume и PersistentVolumeClaim

Когда пользователь удаляет PVC, который используется подом, этот PVC удаляется не сразу — удаление откладывается, пока не будет остановлен использующий его под.

Аналогично, при удалении PV, к которому есть binding от какого-либо PVC, этот PV тоже удаляется не сразу, до тех пор, пока существует связь между PVC и PV.

Reclaiming

Документация тут>>>.

Когда пользователь заканчивает работу с PersistentVolume, он может удалить его объект из кластера, что бы освободить ресурс AWS EBS (reclaim).

Reclaim policy для PersistentVolume указывает кластеру, что делать с овободившимся диском, и может иметь значение Retained, Recycled или Deleted.

Retain

Retain политика позволяет выполнять ручную очистку диска.

После удаления соответвующего PersistentVolumeClaim, PersistentVolume остаётся, и отмечается как «released«, однако становится недоступен для новых PersistentVolumeClaim, т.к. содержит данные предыдущего PersistentVolumeClaim.

Для того, что бы использовать такой ресурс повторно — можно либо удалить объект PersistentVolume, при этом AWS EBS останется доступен, био вручную удалить данные с диска.

Delete

При Delete — удаление PVC приводит к удалению и соответствующего устройсва, такого как AWS EBS, GCE PD или Azure Disk.

Разделы, созданные автоматически наследуют политику из StorageClass, которая по умолчанию задана в Delete.

Recycle

Устарела. Выполняет удаление с раздела обычным rm -rf.

Пример удаление PV и PVC

Итак, у нас имеется под:

kk get pod pv-static-pod
NAME            READY   STATUS    RESTARTS   AGE
pv-static-pod   1/1     Running   0          19s

К которому подключен PVC:

kk get pvc pvc-static
NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-static   Bound    pv-static   50Gi       RWO            gp2            19h

Который биндится на PV:

kk get pv pv-static
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
pv-static   50Gi       RWO            Retain           Bound    default/pvc-static   gp2                     19h

У нашего PV RECLAIM POLICY задана в Retain — значит, после удаления PVC и PV данные должны остаться на месте.

Пробуем — запишем данные:

kk exec -ti pv-static-pod bash
root@pv-static-pod:/# echo Test > /usr/share/nginx/html/test.txt
root@pv-static-pod:/# cat /usr/share/nginx/html/test.txt
Test

Выходим, и удаляем под, затем PVC:

kubectl delete pod pv-static-pod
pod "pv-static-pod" deleted
kubectl delete pvc pvc-static
persistentvolumeclaim "pvc-static" deleted

Проверяем статус PV:

kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                                                                  STORAGECLASS   REASON   AGE
pv-static                                  50Gi       RWO            Retain           Released   default/pvc-static                                                     gp2                     25s

STATUS == Released, и сейчас мы не сможем подключить его обратно к поду через PVC.

Попробуем — создадим ещё раз PVC:

kubectl apply -f pvc-static.yaml
persistentvolumeclaim/pvc-static created

Под:

kubectl apply -f pv-pod-stat.yaml
pod/pv-static-pod created

И проверяем статус PVC:

kubectl get pvc pvc-static
NAME         STATUS    VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-static   Pending   pv-static   0                         gp2            59s

Статус Pending.

Снова удалим под, PVC, и теперь и сам PV:

kubectl delete -f pv-pod-stat.yaml
pod "pv-static-pod" deleted
kubectl delete -f pvc-static.yaml
persistentvolumeclaim "pvc-static" deleted
kubectl delete -f pv-static.yaml
persistentvolume "pv-static" deleted

Создаём заново:

kubectl apply -f pv-static.yaml
persistentvolume/pv-static created
kubectl apply -f pvc-static.yaml
persistentvolumeclaim/pvc-static created
kubectl apply -f pv-pod-stat.yaml
pod/pv-static-pod created

Проверяем PVC:

kubectl get pvc pvc-static
NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-static   Bound    pv-static   50Gi       RWO            gp2            27s

И проверим данные в поде:

kubectl exec -ti pv-static-pod cat /usr/share/nginx/html/test.txt
Test

Данные на месте.

Изменение Reclaim Policy для PersistentVolume

Документация тут>>>.

Сейчас наш PV имеет политику Retain:

kubectl get pv pv-static -o jsonpath='{.spec.persistentVolumeReclaimPolicy}'
Retain

Патчим его — меням политику на Delete:

kubectl patch pv pv-static -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
persistentvolume/pv-static patched

Проверяем:

kubectl get pv pv-static -o jsonpath='{.spec.persistentVolumeReclaimPolicy}'
Delete

Удаляем под и PVC:

kubectl delete -f pv-pod-stat.yaml
pod "pv-static-pod" deleted
kubectl delete -f pvc-static.yaml
persistentvolumeclaim "pvc-static" deleted

Проверяем PV:

kubectl get pv pv-static
Error from server (NotFound): persistentvolumes "pv-static" not found

И AWS EBS, который использовался для этого PV:

aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-0928650905a2491e2
An error occurred (InvalidVolume.NotFound) when calling the DescribeVolumes operation: The volume 'vol-0928650905a2491e2' does not exist.

В целом, на этом всё.

Ссылки по теме