Модуль 2. Хранение данных: персистентность и производительность
Что важно помнить про данные в k8s
- Kubernetes не хранит данные сам. Он просит у StorageClass/CSI выдать том (PV) под запрос (PVC).
- Производительность и надежность определяются не k8s, а конкретным бекендом (локальные диски, NFS, Ceph и т. п.) и его настройкой.
- Разные нагрузки — разные типы хранилищ: блоковые RWO для СУБД/журналов; RWX для совместного доступа приложений; объектное S3 для lakehouse, файлов экспорта, бэкапов.
Режимы доступа: RWO/RWX/ROX — когда что использовать
-
RWO (ReadWriteOnce) — том может быть смонтирован на одну ноду. Это стандарт для блоковых устройств (Ceph RBD, локальные NVMe).
Где применять: Postgres/ClickHouse/Patroni primary, журналы БД, каталог данных Trino (coordinator), кэши с высокой записью. -
RWX (ReadWriteMany) — том доступен с нескольких нод одновременно. Обычно это файловые системы (NFS, CephFS).
Где применять: общие каталоги BI-приложений (загрузки, статики), экспорт отчетов, staging под артефакты. Не для БД. -
ROX (ReadOnlyMany) — одновременное чтение, запись запрещена.
Где применять: разделяемые справочники/статические данные.
Анти-паттерн: ставить СУБД на RWX (NFS/CephFS). Будут проблемы с блокировками/метаданными и латентностью.
Динамические провиженеры и StorageClass: основы
StorageClass описывает как и где создавать PV. Важные поля:
- provisioner (CSI-драйвер), parameters (тип пула/репликаций/EC),
- reclaimPolicy (Retain/Delete),
- volumeBindingMode (лучше WaitForFirstConsumer — привязка к ноде с учетом топологии/таинтов),
- allowVolumeExpansion (горячее расширение),
- mountOptions (например, noatime).
Ceph/Rook: RBD (RWO, блок) и CephFS (RWX, файлы)
Когда выбирать Ceph/Rook: нужен общий, масштабируемый и отказоустойчивый сторидж в кластере без внешних СХД.
RBD (блок, RWO) — для БД:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: rook-ceph-block provisioner: rook-ceph.rbd.csi.ceph.com parameters: clusterID: rook-ceph pool: replicapool imageFormat: "2" csi.storage.k8s.io/fstype: xfs reclaimPolicy: Retain allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
CephFS (файлы, RWX) — для общих каталогов:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: rook-cephfs provisioner: rook-ceph.cephfs.csi.ceph.com parameters: clusterID: rook-ceph fsName: cephfs pool: cephfs-data0 reclaimPolicy: Retain allowVolumeExpansion: true mountOptions: ["noatime"] volumeBindingMode: WaitForFirstConsumer
Производительность: RBD даёт предсказуемые IOPS, CephFS — удобный RWX, но дороже по метаданным/латентности. Для журналов БД — только блок.
NFS (rwx): когда можно, а когда нельзя
Подходит как «универсальный» RWX (например, nfs-subdir-external-provisioner).
Плюсы: простота, дешевизна. Минусы: узкое горлышко по метаданным/латентности, нет гарантий для БД.
StorageClass для NFS-провиженера:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-rwx
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
pathPattern: "${.PVC.namespace}/${.PVC.name}"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
Локальные диски (NVMe/SSD)
Лучшая латентность для БД и «горячих» WAL/журналов. Минус — отсутствие встроенной отказоустойчивости: потеря ноды = потеря PV.
Варианты:
- local-path-provisioner (простая автоматизация).
- Статические PV на нужных нодах (под задачи с репликацией на уровне БД).
Пример статического PV (локальный диск):
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nvme-1
spec:
capacity: { storage: 200Gi }
volumeMode: Filesystem
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
local: { path: /mnt/nvme1 }
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: ["worker-db-1"]
Практика: держите Primary БД на NVMe (локально) с синхронной репликой на RBD, либо используйте несколько реплик на локал-PV на разных нодах (и автоматическое переключение через Patroni). Это архитектурно сложнее, но быстрее.
Объектное хранилище: MinIO/S3 для BI/DWH
Объектное S3-совместимое хранилище нужно для:
- Lakehouse/файловых слоёв (Parquet, Iceberg/Delta/Hudi).
- Хранения бэкапов (WAL, base backups, снапшоты).
- Экспортов BI (файлы, архивы), обмена с внешними системами.
MinIO в k8s (коротко)
- Разворачивается как StatefulSet (standalone или distributed).
- Настройки: erasure coding, атомарность/консистентность (MinIO — strong consistent), версионирование бакетов, bucket-policy (read/write), KMS при необходимости.
Мини-манифест (одиночный инстанс для лаборатории):
apiVersion: v1
kind: Secret
metadata: { name: minio-secret }
type: Opaque
stringData:
accesskey: minioadmin
secretkey: minioadmin
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: minio }
spec:
serviceName: minio
replicas: 1
selector: { matchLabels: { app: minio } }
template:
metadata: { labels: { app: minio } }
spec:
containers:
- name: minio
image: quay.io/minio/minio:latest
args: ["server", "/data"]
env:
- { name: MINIO_ROOT_USER, valueFrom: { secretKeyRef: { name: minio-secret, key: accesskey } } }
- { name: MINIO_ROOT_PASSWORD, valueFrom: { secretKeyRef: { name: minio-secret, key: secretkey } } }
ports: [{ containerPort: 9000 }, { containerPort: 9001 }]
volumeMounts: [{ name: data, mountPath: /data }]
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 100Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: minio }
spec:
selector: { app: minio }
ports:
- { name: api, port: 9000, targetPort: 9000 }
- { name: console, port: 9001, targetPort: 9001 }
type: ClusterIP
Структура бакетов в BI/DWH (рекомендация):
- raw/ — сырые выгрузки/слои ingestion.
- staging/ — промежуточные файлы.
- curated/ — очищенные слои, parquet/iceberg.
-
backups/ — бэкапы БД и WAL.
Включите версионирование на backups и object lock (если требования по неизменности).
Бэкапы: уровни и инструменты
Есть два уровня защиты и восстановления:
-
Уровень платформы k8s (объекты + PV):
- Velero: бэкап манифестов, секретов, конфигов; с Restic/CSI — содержимое PVC.
- Задача: восстановить сервисный контур при потере namespace/кластера.
- Не даёт PITR для СУБД, только crash-consistent копии.
- Уровень приложения (СУБД):
- Stash (оператор бэкапов для БД), либо pgBackRest/WAL-G для Postgres, clickhouse-backup для ClickHouse и т. п.
- Задача: application-aware бэкапы + PITR (восстановление «на момент времени»).
Velero с S3/MinIO (контур кластера)
Установка (примерно):
velero install \ --provider aws \ --plugins velero/velero-plugin-for-aws:v1.9.0 \ --bucket k8s-backups \ --secret-file ./credentials-velero \ --backup-location-config region=minio,s3ForcePathStyle=true,s3Url=http://minio.minio:9000 \ --use-restic
- credentials-velero — ключ/секрет MinIO в формате AWS.
- Включите restic или CSI snapshots для PVC.
Бэкап/restore namespace:
velero backup create bi-namespace --include-namespaces bi velero restore create --from-backup bi-namespace
Замечание: Velero — не PITR, а «страховка» кластера/неймспейса. Не путать с бэкапами БД.
Stash (AppsCode): Postgres-aware бэкапы (в MinIO)
Идея: CRD-объекты описывают Repository (куда бэкапить), BackupConfiguration (когда/что), RestoreSession (как восстановить).
Repository (S3→MinIO):
apiVersion: stash.appscode.com/v1alpha1
kind: Repository
metadata: { name: repo-pg }
spec:
backend:
s3:
endpoint: http://minio.minio:9000
bucket: backups
prefix: postgres/demo
insecureSkipTLSVerify: true
region: minio
s3ForcePathStyle: true
accessKeyID: { name: minio-secret, key: accesskey }
secretAccessKey: { name: minio-secret, key: secretkey }
BackupConfiguration (ежечасно, плюс архивация WAL):
apiVersion: stash.appscode.com/v1beta1
kind: BackupConfiguration
metadata: { name: pg-backup }
spec:
schedule: "0 * * * *"
target:
ref:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
name: pg # AppBinding, описывающий доступ к Postgres (создается Stash'ем или вручную)
repository:
name: repo-pg
retentionPolicy:
name: "keep-last-24"
keepLast: 24
prune: true
RestoreSession:
apiVersion: stash.appscode.com/v1beta1
kind: RestoreSession
metadata: { name: pg-restore }
spec:
target:
ref:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
name: pg
repository:
name: repo-pg
rules:
- snapshots: ["latest"]
Плюсы Stash: декларативно, удобно, есть «аддоны» для разных БД. Минус: дополнительный оператор/сложность. Альтернатива — WAL-G/pgBackRest руками (см. ниже).
Postgres PITR с WAL-G (в MinIO)
Идея: делаем периодические «base backups» и непрерывную архивацию WAL в S3 (MinIO). При восстановлении выбираем момент времени (PITR).
В StatefulSet Postgres включите:
- wal_level=replica, archive_mode=on, archive_timeout=60s.
- archive_command='wal-g wal-push %p'.
-
Переменные окружения WAL-G для S3:
- WALG_S3_PREFIX=s3://backups/postgres/demo
- AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
- AWS_ENDPOINT=http://minio.minio:9000, AWS_S3_FORCE_PATH_STYLE=true, AWS_REGION=minio
- (опционально) WALG_COMPRESSION_METHOD=lz4
Фрагмент контейнера с WAL-G (sidecar или внутрь основного):
env:
- name: WALG_S3_PREFIX
value: s3://backups/postgres/demo
- name: AWS_ACCESS_KEY_ID
valueFrom: { secretKeyRef: { name: minio-secret, key: accesskey } }
- name: AWS_SECRET_ACCESS_KEY
valueFrom: { secretKeyRef: { name: minio-secret, key: secretkey } }
- name: AWS_ENDPOINT
value: http://minio.minio:9000
- name: AWS_S3_FORCE_PATH_STYLE
value: "true"
- name: AWS_REGION
value: minio
- name: WALG_DISABLE_S3_SSE
value: "true"
Базовый бэкап и удаление старых:
# внутри Pod'а БД (или через kubectl exec) wal-g backup-push /var/lib/postgresql/data wal-g delete retain FULL 7 --confirm # держим 7 полных бэкапов
PITR (восстановление «на момент времени»):
- Остановить Pod, очистить datadir, выставить recovery.conf (или recovery.signal для PG ≥ 12):
# env: export PGDATA=/var/lib/postgresql/data export PGUSER=postgres # восстановить последнюю базу wal-g backup-fetch $PGDATA LATEST # задать целевое время echo "restore_command = 'wal-g wal-fetch %f %p'" >> $PGDATA/postgresql.auto.conf echo "recovery_target_time = '2025-08-15 09:30:00+00'" >> $PGDATA/postgresql.auto.conf touch $PGDATA/recovery.signal
- Запустить Postgres и дождаться конца восстановления (перейдет в обычный режим).
Практический момент: храните инструкцию PITR рядом с БД (Runbook) и регулярно тренируйтесь.
Производительность: что реально влияет
- Латентность важнее «сырых» IOPS для OLTP/журналов. Журналы — на локальные NVMe или RBD с быстрым бекендом.
- Сеть: Ceph-кластерам нужна быстрая сеть (25–40–100GbE), иначе «узким местом» станет сеть.
- Разделение пулов: под журналы и данные — разные пула (или разные классы дисков).
- Mount options: noatime, nobarrier (использовать осторожно, понимая риски), xfs для больших файлов.
- Topology & Affinity: не кладите БД вместе с нагруженными стейтлес-воркерами — вы получите конкуренцию по IO.
- WaitForFirstConsumer в StorageClass — спасает от «промаха» с топологией (том создастся там, где Pod будет запланирован).
Быстрый тест fio в Pod (ориентировочно):
fio --name=randwrite --filename=/data/testfile --rw=randwrite --bs=4k --iodepth=32 --size=2G --numjobs=4 --time_based --runtime=60 fio --name=randread --filename=/data/testfile --rw=randread --bs=4k --iodepth=32 --size=2G --numjobs=4 --time_based --runtime=60
Риски и как их снижать
-
Ставим БД на RWX (NFS/CephFS). → высокий лаг/«подвисания».
Решение: блок RWO (Ceph RBD, локальные NVMe). -
PV создаются без учета топологии. → том на «дальней» ноде/медленном пуле.
Решение: WaitForFirstConsumer, nodeAffinity, отдельные StorageClass. -
Нет копий/версионирования бэкапов. → инцидент с MinIO уничтожит историю.
Решение: версионирование + периодическая репликация бакета (site-to-site). -
Velero вместо PITR. → иллюзия безопасности: база не восстанавливается «на момент».
Решение: app-aware бэкапы (Stash/pgBackRest/WAL-G). -
Смешанная нагрузка на одних нодах. → перегрев IO, скачки latency.
Решение: отдельные пуллы нод и taints/tolerations. -
Нет регулярных тренировок восстановления. → «бэкапы есть, восстановления нет».
Решение: квартальные DR-учения, контроль RTO/RPO.
Практика (лабораторные)
Лаб 1. Развернуть MinIO
- Применить манифест из раздела 3.1, дождаться Pod в статусе Running.
- Порт-форвард консоли:
kubectl port-forward svc/minio 9000:9000 9001:9001 # открыть http://localhost:9001, логин/пароль из секрета
- Создать бакеты backups, raw, staging, curated (через UI или mc).
Лаб 2. Развернуть Postgres (StatefulSet + PVC RWO)
- Используйте StorageClass для блокового хранилища (Ceph RBD/локальный PV).
-
Примените StatefulSet Postgres (из Модуля 1) и добавьте:
- wal_level=replica, archive_mode=on, archive_timeout=60s в postgresql.conf (через ConfigMap или env).
- sidecar или env для WAL-G (раздел 4.3).
- Проверка:
kubectl exec -it pg-0 -- psql -U demo -d demo -c "select pg_is_in_recovery();"
Лаб 3. Настроить PITR с WAL-G в MinIO
- Сделать первый базовый бэкап:
kubectl exec -it pg-0 -- wal-g backup-push /var/lib/postgresql/data
- Сгенерировать нагрузку (создать таблицы/данные), дождаться архивации WAL.
- Смоделировать аварию: удалить таблицу.
- Выполнить PITR (раздел 4.3) на момент «до удаления». Проверить, что таблица вернулась.
Лаб 4. Velero: бэкап/restore namespace
- Установить Velero с S3 на MinIO (раздел 4.1).
- Создать бэкап неймспейса с Postgres/MinIO.
- Удалить неймспейс, восстановить из бэкапа.
Убедитесь, что сервисный контур поднялся, но данные Postgres после PITR — из MinIO (уровень приложения).
Чек-лист перед продом
- План размещения: RBD (RWO) для БД, RWX только для шаринга файлов.
- StorageClass с WaitForFirstConsumer, нужным reclaimPolicy, allowVolumeExpansion.
- Отдельные пулы нод для stateful/стейтлес, taints/tolerations, affinity.
- S3 (MinIO) с версионированием и планом репликации бакетов.
- Velero для объектов/NS + application-aware бэкапы (Stash/pgBackRest/WAL-G).
- Документированный PITR-runbook, расписание тренировок DR, контроль RPO/RTO.
- Мониторинг IO/латентности (node_exporter, storage-экспортеры, алерты на p95/p99).
- Тест fio и нагрузочные прогоны ETL/запросов до и после.
Выводы
- Для производительности критично сопоставить тип нагрузки и тип хранилища (RWO-блок для БД, RWX для шаринга, S3 для lakehouse/бэкапов).
- MinIO/S3 — опорный слой файлов и бэкапов; включайте версионирование и продумывайте DR.
- Velero — про кластер и namespace, не про PITR. PITR делается на уровне СУБД.
- Любая стратегия бэкапов ничего не стоит без регулярных восстановлений и метрик RPO/RTO.



