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 без разбора - но инструментарий для этого есть, и он работает.