ADG Оставить заявку
Блог DevOps 6 мин чтения

PostgreSQL в Kubernetes StatefulSet: первый production-запуск с Ceph RBD PVC

Kubernetes 1.12 и зрелый инструментарий для stateful workloads: мигрировали первый production-PostgreSQL в K8s с Ceph RBD. Миф «Kubernetes только для stateless» проверен на практике.

Контекст момента

Kubernetes 1.12: WaitForFirstConsumer в beta, volume expansion в beta - production stateful workloads получили зрелый инструментарий

С тех пор как Kubernetes появился в нашей практике, один вопрос звучал от клиентов стабильно: а базы данных туда можно? Стандартный осторожный ответ был - технически можно, но лучше не надо, Kubernetes для stateless. Kubernetes 1.12 закрыл несколько открытых вопросов для production stateful workloads: volume expansion вышел в beta, WaitForFirstConsumer появился в beta. Мы взяли это как сигнал и в декабре перенесли первый production-PostgreSQL клиента в K8s на managed-обслуживании. Вот что получилось.

Почему именно сейчас

StatefulSet достиг GA ещё в Kubernetes 1.9, но стабильностью GA-статуса история не заканчивается. PVC volume expansion и binding mode WaitForFirstConsumer дошли до beta в 1.11-1.12 - это уже другой уровень зрелости для production. PersistentVolume в части динамического provisioning, reclaim policy и расширения томов тоже дозрел именно в этом цикле.

В 1.11 появилось расширение PVC без пересоздания пода - мы его потрогали в июле. В 1.12 volume binding mode WaitForFirstConsumer вышел в beta: теперь provisioner ждёт пока под не будет размещён, и только тогда создаёт том - в той же зоне доступности, где поселился под. Для нас, у кого Ceph RBD и ноды в одном датацентре, это не критично, но логика правильная.

Клиент попросил перенести non-critical production PostgreSQL - внутренняя база для одного из сервисов, не mission-critical, но и не игрушечная: несколько гигабайт данных, несколько сотен запросов в минуту в пике. Хороший кандидат для первого реального теста.

Стек и архитектура

Ceph у клиента уже стоит - мы его переводили на BlueStore с Luminous раньше. RBD-провайдер в Kubernetes настроен через StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd
provisioner: kubernetes.io/rbd
parameters:
  monitors: 10.0.1.10:6789,10.0.1.11:6789,10.0.1.12:6789
  adminId: admin
  adminSecretName: ceph-admin-secret
  adminSecretNamespace: kube-system
  pool: k8s-volumes
  userId: kube
  userSecretName: ceph-kube-secret
  fsType: ext4
  imageFormat: "2"
  imageFeatures: layering
reclaimPolicy: Retain

reclaimPolicy: Retain - принципиальный момент. По умолчанию было бы Delete, и удаление PVC удалило бы и сам RBD-образ с данными. Для базы данных это неприемлемо.

StatefulSet для PostgreSQL:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:11.1
        env:
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: ceph-rbd
      resources:
        requests:
          storage: 50Gi

Одна реплика - это осознанный выбор для первого запуска. High availability PostgreSQL в Kubernetes через Patroni или Stolon - отдельная история с дополнительной сложностью. Здесь цель была: убедиться что stateful workload вообще живёт нормально, и разобраться с поведением при перезапуске пода и перепланировании.

Миграция данных

Данные переносили через pg_dump / pg_restore. Старая база крутилась на виртуалке рядом. Порядок:

  • Снять дамп с работающей базы в момент с минимальной нагрузкой.
  • Залить дамп в PostgreSQL в Kubernetes через временный Job.
  • Сверить checksums таблиц через pg_dump --schema-only и ручной счётчик строк на критических таблицах.
  • Переключить приложение на новый endpoint (Headless Service с именем postgres.namespace.svc.cluster.local).
  • Держать старую базу в readonly ещё несколько часов как резерв.

Сам перенос прошёл без сюрпризов. Неожиданность была в другом.

Что пошло не так - и почему это поучительно

При первой попытке запустить StatefulSet под завис в состоянии ContainerCreating. Логи kubelet показывали ошибку при маунте RBD-образа: rbd: failed to map image. Ceph при этом был здоровым, образ существовал.

Покопались в деталях: проблема оказалась в том, что на нодах Kubernetes не были установлены пакеты ceph-common. RBD-провайдер в Kubernetes использует утилиту rbd с хоста для маппинга образов в блочные устройства. Без ceph-common на ноде - ничего не работает, и ошибка при этом не самая очевидная.

После установки ceph-common на все ноды кластера - под стартанул, том примонтировался, PostgreSQL поднялся. Это один из тех случаев когда понимаешь: инфраструктурные зависимости между компонентами нужно прописывать в документацию явно, не считая их само собой разумеющимися.

Второй момент был с /var/lib/postgresql/data: PostgreSQL отказывался стартовать с ошибкой could not change directory to "/var/lib/postgresql/data": Permission denied. Причина - RBD-образ примонтировался с root:root, а процесс postgres запускается от пользователя postgres (uid 999). Решили через init-контейнер:

initContainers:
- name: fix-permissions
  image: busybox
  command: ["sh", "-c", "chown -R 999:999 /var/lib/postgresql/data"]
  volumeMounts:
  - name: data
    mountPath: /var/lib/postgresql/data

Не самое изящное решение, но рабочее. Часть образов PostgreSQL умеет это делать сама через entrypoint, часть - нет. Версия 11.1 не умела.

Поведение при перезапуске и перепланировании

Это было самое интересное для проверки. StatefulSet гарантирует что под получит тот же PVC при перезапуске - это его ключевое отличие от Deployment. Проверили несколько сценариев.

Перезапуск пода (kill контейнера вручную): под пересоздаётся, RBD-образ отмаппируется со старого хоста и замаппируется на новый, PostgreSQL стартует с теми же данными. Время восстановления - около 30 секунд. Приложение, которое пишет в базу, в этот момент получало connection refused и reconnect по своей логике. Ничего не потерялось.

Перезапланирование на другую ноду (drain ноды с работающим подом): аналогичная картина, только Ceph сначала ждёт unmount на первой ноде, потом маппирует на второй. RBD - блочное устройство, ReadWriteOnce - монтируется только к одному хосту одновременно. Это ограничение, с которым нужно жить: нет параллельного чтения с разных нод. Для одиночного PostgreSQL это нормально.

Рестарт самого PostgreSQL процесса внутри контейнера: PGDATA не теряется, база поднимается и выполняет crash recovery если нужно. Это стандартное поведение PostgreSQL, Kubernetes его не меняет.

Что получили на выходе

PostgreSQL в StatefulSet с Ceph RBD PVC работает как production-база. Это уже не эксперимент на стенде - живые данные, живой трафик, несколько недель без инцидентов.

Несколько практических наблюдений:

  • Retention политика PVC не прощает ошибок. Удалить StatefulSet случайно проще, чем кажется. PVC после этого остаётся (если Retain), но под связи между StatefulSet и конкретными PVC восстанавливать вручную - то ещё удовольствие.
  • Мониторинг нужен и на уровне Kubernetes (состояние пода, PVC), и на уровне PostgreSQL (pg_stat_activity, replication lag если появится). Одного без другого недостаточно.
  • Backup не делегируется Kubernetes. PVC - это диск, не snapshot-система. pg_dump по расписанию через CronJob, бэкап в S3, проверка восстановления - всё это остаётся на нас.

Миф о том, что Kubernetes только для stateless, разобрался о практику. Это не значит что stateful-нагрузку следует тащить в K8s без разбора - но инструментарий для этого есть, и он работает.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.