Доступно в редакциях:  Ultimate/EE

Входит в расширения:  Продвинутое хранение данных

Стадия жизненного цикла модуля: Экспериментальная версия

Перед включением модуля необходимо разрешить использование экспериментальных модулей в ModuleConfig deckhouse. Для этого установите параметр allowExperimentalModules в true.

Модуль находится в стадии Experimental. API и поведение могут меняться без предупреждения; не используйте его для продуктивных нагрузок.

Модуль sds-nfs превращает один том ReadWriteOnce в хранилище ReadWriteMany. Для ресурса NFSServer он заказывает том выбранного StorageClass, форматирует его в XFS, раздаёт по NFS из одного пода и публикует NFSStorageClass модуля csi-nfs с именем ресурса. Каждый PVC этого класса получает на томе свой каталог, ограниченный размером PVC проектной квотой XFS; том модуль увеличивает сам, когда PVC просят больше.

Модуль доступен в редакциях EE и FE. Нужен включённый модуль csi-nfs версии 0.4.14 или новее: она выпускает клиентский сертификат mTLS для узлов и публикует его CA в NFSStorageClass (см. «Шифрование» в разделе «Как это работает»). Ему принадлежит ресурс NFSStorageClass, и он монтирует тома этих классов. NFSStorageClass каждого NFSServer создаёт и поддерживает sds-nfs: вручную его не меняйте — изменения перезапишутся, а удаляется он вместе с NFSServer. NFSServer нельзя создать с именем существующего StorageClass или NFSStorageClass: запрос будет отклонён, потому что класс сервера получил бы это имя.

Ресурсы NFSServer читают пользователи с ролью viewer подсистемы storage или уровнем доступа User в user-authz; создают, изменяют и удаляют — с ролью manager подсистемы storage или уровнем ClusterEditor и выше.

Пример

apiVersion: storage.deckhouse.io/v1alpha1
kind: NFSServer
metadata:
  name: shared
spec:
  backend:
    size: 50Gi

Том бэкенда получает класс кластера по умолчанию, если spec.backend.storageClassName не задаёт другой, а сервер работает на системных узлах, если spec.nodeSelector не задаёт другие (см. «Размещение» в разделе «Как это работает»). spec.backend.storageClassName и spec.storageClass.volumeBindingMode после создания сервера не меняются: класс тома не меняется, а режим привязки опубликованного NFSStorageClass csi-nfs изменить не может. spec.backend.size можно только увеличить.

PVC затем используют класс shared:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: [ReadWriteMany]
  storageClassName: shared
  resources:
    requests:
      storage: 5Gi

kubectl get nfsserver shared показывает размер тома бэкенда, сколько просят PVC вместе, узел сервера и условие Ready; остальные условия говорят, что не готово и почему.

Как это работает

  • Отказоустойчивость задаёт StorageClass бэкенда. Подходит любой класс, который выдаёт тома ReadWriteOnce с volumeMode: Block. С реплицируемым классом сервер переезжает на другой узел, когда его узел потерян; с локальным диском тома класса ждут возвращения узла.
  • Размещение. Без spec.nodeSelector сервер работает на системных узлах (node-role.deckhouse.io/system) и допускает их taint dedicated.deckhouse.io=system, как системные компоненты Deckhouse; в кластере без системных узлов — на любом узле, кроме control plane. spec.nodeSelector заменяет этот выбор. Когда системные узлы появляются или уходит последний из них, сервер переезжает — с паузой переезда.
  • Для переезда нужен фенсинг. Под сервера уходит с потерянного узла только после фенсинга узла, поэтому фенсинг должен быть включён на NodeGroup, где может работать сервер (spec.fencing.mode: Watchdog); условие FencingReady перечисляет группы без него и узлы, на которых агент фенсинга не взвёл watchdog.
  • Потеря узла сервера. Когда узел сервера NotReady, модуль ждёт, пока смерть узла будет доказана:
    • Node удалён фенсингом платформы или узел пробыл NotReady без продления lease дольше таймаута watchdog своей NodeGroup плюс 30 секунд при взведённом watchdog: на узле работает агент фенсинга платформы в режиме Watchdog, и узел не на обслуживании (update.node.deckhouse.io/approved, update.node.deckhouse.io/disruption-approved или node-manager.deckhouse.io/fencing-disable — с ними агент снимает watchdog).
    • После этого модуль удаляет под сервера, освобождает том бэкенда от узла, и сервер запускается на другом узле; клиенты мёртвого узла, найденные по адресу, с которого они монтировали том, забываются, чтобы остальные не ждали, пока те восстановят блокировки. Узел, который просто недоступен, сервер не теряет.
    • status.interruption показывает, чего ждёт сервер (WaitingForNodeFencing, ReleasingVolume, Scheduling, Starting, Recovering — сервер работает, а клиенты восстанавливают состояние в grace period NFSv4, это тоже часть паузы), а status.lastInterruption — сколько длилось последнее прерывание. Прерывание начинается, когда платформа помечает узел NotReady, примерно через 40 секунд после потери узла, поэтому пауза у клиентов на столько же длиннее.
    • Переезд может ещё до шести минут ждать на шаге Starting: контроллер подключения томов Kubernetes (attach-detach controller) держит том бэкенда подключённым к потерянному узлу, пока не отключит его принудительно (maxWaitForUnmountDuration), если на узле нет taint node.kubernetes.io/out-of-service, а фенсинг платформы удаляет Node, не ставя его.
    • В тестах модуля status.lastInterruption переезда с потерянного узла составлял 110–160 секунд, когда том отпускался сразу, и около 430 секунд, когда нет.
  • Один сервер на все тома класса. Перезапуск или переезд сервера приостанавливает ввод-вывод всех томов класса, пока сервер не вернётся: около 15–30 секунд при перезапуске на месте, дольше при переезде. Приложения не получают ошибок ввода-вывода (монтирование hard): их запись ждёт.
  • Обновление модуля. Обновление, которое меняет образ сервера, перезапускает серверы всех классов одновременно, а не по очереди: каждый класс приостанавливает свои тома на время перезапуска, около 15–30 секунд. Планируйте обновление модуля как такую паузу всех NFS-томов кластера.
  • Обслуживание узла. Перед обслуживанием узла сервера освободите его от нагрузки (kubectl drain или прерывающее обновление Deckhouse, которое по умолчанию делает drain само). Под сервера вытесняется, StatefulSet запускает его на другом узле размещения, а тома класса ждут, как при любом переезде; status.interruption это показывает. Освобождённый узел остаётся закрытым для планирования (cordon), пока его не вернут командой uncordon, поэтому сервер на него не возвращается и после обслуживания остаётся там, куда переехал.
  • Квоты. df в поде показывает размер PVC. Запись сверх него завершается ошибкой ENOSPC (XFS сообщает о заполненном проекте как о заполненной ФС). Сервер отклоняет запись сверх лимита, поэтому приложение должно проверять результат write, fsync и close.
  • Синхронная запись медленная. Запись, которую клиент отправляет стабильной — O_DIRECT, O_SYNC или fsync после каждой мелкой записи, как делают базы данных, — заставляет сервер синхронизировать файловую систему и том бэкенда ради одной этой записи. В нагрузочном тесте модуля случайная запись блоками 4 КиБ с direct=1 давала около 250–1600 IOPS на сервер при p99 200–800 мс, а та же запись с буферизацией на клиенте — около 2800 IOPS. Модуль подходит для общих файлов и умеренной записи, а не для баз данных. Сервер в разы быстрее, когда на его узле лежит полная копия тома бэкенда.
  • Рост. Модуль заказывает для тома бэкенда сумму размеров PVC с запасом и расширяет ФС на ходу. Когда PVC просят больше, чем есть у бэкенда, условие CapacityReady равно False и объясняет причину; PVC к этому моменту уже показывает новый размер.
  • Доступ. У каждого PersistentVolume класса свой экспорт, и узел может смонтировать его, только пока на нём работает под, который использует этот том; узлам контроллера csi-nfs доступны все тома и корень экспорта, потому что они создают и удаляют каталоги томов. Остальным узлам корень доступен только для обхода, на чтение и без данных: тома, которые им смонтировать нельзя, от них скрыты. Под из сети подов ничего смонтировать не может, а inline-тома серверов кластера csi-nfs отклоняет.
    • Узел получает том через несколько секунд после того, как на него запланирован под этого тома, и теряет его, когда последний такой под удалён; kubelet всё это время повторяет монтирование. Каталог получает экспорт в момент, когда csi-nfs его создаёт, ещё до появления тома, и теряет его, когда удалён PVC тома, — чтобы csi-nfs мог удалить каталог.
    • Тома монтируются на узлах с nosuid и nodev, поэтому бинарь с set-user-ID или файл устройства на томе не действует на узле. В под среда выполнения контейнеров подключает том bind-монтированием без этих флагов; внутри подов повысить привилегии бинарю с set-user-ID не даёт securityContext.allowPrivilegeEscalation: false, которого требует стандарт Pod Security Restricted.
    • Root на узле или привилегированный под на нём может прочитать тома подов, которые на этом узле работают. Это предел любого хранилища, которое монтирует узел.
  • Шифрование и клиентский сертификат. Сервер принимает только соединения RPC-with-TLS (RFC 9289) и только от клиента, который предъявил клиентский сертификат узлов (mTLS): трафик между узлами и сервером всегда зашифрован, узел проверяет, что говорит именно с сервером, а сервер — что говорит именно с узлом. Под в сети узла делит с ним адрес, но без ключа узла, который может прочитать только root или под с hostPath, сервер его не пускает. Настраивать ничего не нужно.
    • Модуль хранит свой CA в Secret d8-sds-nfs/sds-nfs-ca и выпускает каждому серверу сертификат на адрес его Service. CA передаётся узлам в spec.tlsParameters.ca NFSStorageClass сервера; tlsParameters модуля csi-nfs для этого не нужны.
    • Клиентский сертификат выпускает csi-nfs для узлов; его CA csi-nfs публикует в status.clientCA NFSStorageClass сервера, и сервер доверяет этому CA. Узел предъявляет один и тот же клиентский сертификат всем NFS-серверам, которые монтирует с mTLS, поэтому клиентский сертификат, заданный в tlsParameters.mtls модуля csi-nfs, заменяет выпущенный и для серверов этого модуля, и ему нужен tlsParameters.mtls.clientCA; без него условие TLSReady равно False и говорит, что сервер ждёт клиентский CA.
    • Модуль доступен в редакциях EE и FE и требует csi-nfs с spec.tlsParameters.ca, status.clientCA и spec.mountOptions.nosuid у NFSStorageClass; со старым csi-nfs условие TLSReady равно False и говорит, что его надо обновить.
    • Каждому узлу, который монтирует тома, нужно ядро с RPC-with-TLS: 6.5 и новее, с CONFIG_TLS и CONFIG_NET_HANDSHAKE. Условие ClientNodesReady перечисляет узлы со старым ядром: поды на них тома не смонтируют. На таких узлах (например, Astra и Debian с ядром 6.1) не запускаются и поды csi-nfs, как только в кластере появляется CA.
    • Сертификаты действуют десять лет и не обновляются по расписанию: ganesha читает новый сертификат только при перезапуске, а перезапуск ставит на паузу все тома класса. Новый сертификат выпускается, когда меняется адрес Service, и сервер перезапускается с ним.
    • Сервер, созданный версией модуля без mTLS, после обновления не обслуживает свои тома: NFSStorageClass такого сервера опубликован без mTLS, его подключение изменить нельзя, и условие NFSStorageClassReady говорит, что сервер надо создать заново.
  • Удаление. NFSServer не удаляется, пока существуют PersistentVolume его класса: до удаления последнего из них сервер продолжает их обслуживать и переезжает с потерянного узла, а Ready=False с причиной DeletionBlocked. Удаление ресурса удаляет том бэкенда и данные на нём.
  • Мониторинг. Дашборд SDS-NFS / NFS servers в Grafana показывает состояние серверов, их прерывания и тома бэкенда, операции, задержки и пропускную способность NFS по метрикам ganesha и заполнение каждого тома (sds_nfs_volume_used_bytes против sds_nfs_volume_limit_bytes, по неймспейсу и PVC). Алерты SdsNfsServerInterrupted, SdsNfsServerNotReady, SdsNfsServerCapacityNotReady и SdsNfsServerFencingNotReady срабатывают по условиям серверов.