Неубиваемый Postgresql cluster внутри Kubernetes cluster
Если вы когда-нибудь задумывались о доверии и надежде, то скорее всего, не испытывали этого ни к чему так же сильно, как к системам управления базами данных. Ну и действительно, это же База Данных! В названии содержится весь смысл — место, где хранятся данные, основная задача ХРАНИТЬ. И что самое печальное, как всегда, однажды, эти убеждения разбиваются об останки такой одной умершей БД на 'проде'.
И что же делать? — спросите вы. Не деплоить на сервера ничего, — отвечаем мы. Ничего, что не умеет само себя чинить, хотя бы временно, однако надежно и быстро!
В этой статье я попробую рассказать о своем опыте настройки почти бессмертного Postgresql кластера внутри другого отказоустойчивого решения от Google — Kubernates (aka k8s)
Содержание
Статья вышла немного больше чем ожидалось, а потому в коде много ссылок, в основном на код, в контексте которого идет речь.
Дополнительно прилагаю оглавление, в том числе для нетерпеливых, чтобы сразу перейти к результатам:
Задача
Потребность иметь хранилище с данными для почти любого приложения — необходимость. А иметь это хранилище устойчивым к невзгодам в сети или на физических серверах — хороший тон грамотного архитектора. Другой аспект — высокая доступность сервиса даже при больших конкурирующих запросах на обслуживание, что означает легкое масштабирование при необходимости.
Итого получаем проблемы для решения:
- Физически распределенный сервис
- Балансировка
- Не ограниченное масштабирование путем добавление новых узлов
- Автоматическое восстановление при сбоях, уничтожении и потери связи узлами
- Отсутствие единой точки отказа
Дополнительные пункты, обусловленные спецификой религиозных убеждений автора:
- Postgres (наиболее академическое и консистентное решение для РСУБД среди бесплатных доступных)
- Docker упаковка
- Kubernetes описание инфраструктуры
На схеме это будет выглядеть примерно так:
master (primary node1) --\
|- slave1 (node2) ---\ / balancer \
| |- slave2 (node3) ----|---| |----client
|- slave3 (node4) ---/ \ balancer /
|- slave4 (node5) --/
При условии входных данных:
- Большее число запросов на чтение (по отношению к записи)
- Линейный рост нагрузки при пиках до x2 от среднего
Решение методом «Гугления»
Будучи, искушенным в решении IT проблем, человеком, я решил спросить у коллективного разума: «postgres cluster kubernetes» — куча мусора, «postgres cluster docker» — куча мусора, «postgres cluster» — несколько вариантов, из которых пришлось воять.
Что меня расстроило, так это отсутствие вменяемых Docker сборок и описание любого варианта для кластеризации. Не говоря уже о Kubernetes. Кстати говоря, для Mysql вариантов было не много, но все же были. Как минимум понравился пример в официальном репозитории k8s для Galera (Mysql cluster)
Гугл дал ясно понять, что проблемы придется решать самому и в ручном режиме…«но хоть с помощью разрозненных советов и статей» — выдохнул я.
Плохие и непреемлимые решения
Сразу замечу, что все пункты в этом параграфе могут быть субъективными и, вполне даже, жизнеспособными. Однако, полагаясь на свой опыт и чутье, мне пришлось их отсечь.
Когда кто-то делает универсальное решение (для чего бы то ни было), мне всегда кажется, что такие вещи громоздки, неповоротливы и тяжелы в обслуживании. Так же вышло и с Pgpool, который умеет почти все:
- Балансировка
- Хранение пачки коннектов для оптимизации соединения и скорости доступа к БД
- Поддержка разных вариантов репликации (stream, slony)
- Авто определение Primary сервера для записи, что важно при реорганизации ролей в кластере
- Поддержка failover/failback
- Cобственная репликация master-master
- Согласованная работа нескольких узлов Pgpool-ов для искоренения единой точки отказа.
Первые четыре пункта я нашел полезными, и остановился на том, поняв и обдумав проблемы остальных:
- Восстановление с помощью Pgpool2 не предлагает никакой системы принятия решения о следующем мастере — вся логика должна быть описана в командах failover/failback
- Время записи, при репликации master-master, сводится к удвоенному по отношению варианта без него, вне зависимости от количества узлов… ну хоть не растет линейно
- Как построить каскадный кластер (когда один slave читает с предыдущего slave) — вообще не понятно
- Конечно же хорошо, что Pgpool знает о своих братьях и может оперативно становиться активным звеном при проблемах на соседнем узле, но эту проблему для меня решает Kubernetes, который гарантирует аналогичное поведение для, вообще, любого сервиса установленного в него.
Собственно, тоже почитав и сравнив найденное с уже знакомой и работающей «из коробки» поточной репликацией (Streaming Replication), легко далось решение даже не думать о Слонах.
Да и ко всему прочему, на первой же странице сайта проекта ребята пишут что, с postgres 9.0+ вам не нужны Slony при условии отсутствия неких специфичных требований к системе:
- частичная репликация
- интеграция с другими решениями («Londiste and Bucardo»)
- дополнительное поведение при репликации
Вообщем, мне кажется Slony не торт… как минимум если у вас нет этих самых трех специфичных задач.
Осмотревшись вокруг и разобравшись в вариантах подхода идеальной двусторонней репликации, оказалось, что жертвы не совместимы с жизнью некоторых приложений. Не говоря о скорости, есть ограничения в работе с транзакциями, сложными запросами (SELECT FOR UPDATE
и другие).
Вполне вероятно, я не так искушен именно в этом вопросе, но увиденного мне хватило, чтобы тоже оставить эту идею. И еще раскинув мозгами, мне показалось, что для системы с усиленной операцией записи нужны совершенно другие технологии, а не реляционные базы данных.
Консолидация и сборка решения
В примерах я буду говорить о том, как принципиально должно выглядеть решение, а в коде — как это вышло у меня. Для создания кластера вовсе не обязательно иметь Kubernetes (есть пример docker-compose) или Docker в принципе. Просто тогда, все описанное будет полезно, не как решение типа CPM (Copy-Paste-Modify), а как руководство по установке cо снипетами.
Primary и Standby вместо Master и Slave
Почему же коллеги из Postgresql отказались от терминов «Master» и «Slave»?… хм, могу ошибаться, но ходил слух, что из-за не-полит-корректности, мол, что рабство это плохо. Ну и правильно.
Первое что нужно сделать — включить Primary сервер, за ним первый слой Standby, а за тем второй — все согласно поставленной задачи. Отсюда получаем простую процедуру по включению обычного Postgresql сервера в режиме Primary/Standby с конфигурацией для включения Streaming Replication
wal_level = hot_standby
max_wal_senders = 5
wal_keep_segments = 5001
hot_standby = on
Все параметры в комментариях имеют краткое описание, но если вкратце, эта конфигурация дает серверу понять, что он отныне часть кластер и, в случае чего, нужно позволить читать WAL логи другим клиентам. Плюс разрешить запросы во время восстановления. Отличное описание по подробной настройке такого рода репликации можно найти на Postgresql Wiki.
Как только мы получили первый сервер кластера, можем включать Stanby, который знает, где находится его Primary.
Моя задача здесь свелась к сборке универсального образа Docker Image, который включается в работу, в зависимости от режима, как то так:
- Для Primary:
- Конфигурирует Repmgr (о нем немного позже)
- Создает базу и пользователя для приложения
- Создает базу и пользователя для мониторинга и поддержки репликации
- Обновляет конфиг (
postgresql.conf
) и открывает доступ пользователям извне (pg_hba.conf
) - Запускает Postgresql сервис в фоне
- Регистрируется как Master в Repmgr
- Запускает
repmgrd
— демон от Repmgr для мониторинга репликации (о нем тоже позже)
- Для Standby:
- Клонирует Primary сервер с помощью Repmgr (между прочим со всеми конфигами, так как просто копирует
$PGDATA
директорию) - Конфигурирует Repmgr
- Запускает Postgresql сервис в фоне — после клонирования сервис вменяемо осознает, что является standby и покорно следует за Primary
- Регистрируется как Slave в Repmgr
- Запускает
repmgrd
- Клонирует Primary сервер с помощью Repmgr (между прочим со всеми конфигами, так как просто копирует
Для всех этих операций важна последовательность, и посему в коде напиханы sleep
. Знаю — не хорошо, но так удобно конфигурировать задержки через ENV переменные, когда нужно разом стартануть все контейнеры (например через docker-compose up
)
Все переменные к этому образу описаны в docker-compose
файле.
Вся разница первого и второго слоя Standby сервисов в том, что для второго мастером является любой сервис из первого слоя, а не Primary. Не забываем, что второй эшелон должен стартовать после первого с задержкой во времени.
Split-brain и выборы нового лидера в кластере
Split brain — ситуация, в которой разные сегменты кластера могут создать/избрать нового Master-a и думать, что проблема решена.
Это одна, но далеко не единственная проблема, которую мне помог решить Repmgr.
По сути это менеджер, который умеет делать следующее:
- Клонировать Master (master — в терминах Repmgr) и автоматически настоить вновьрожденный Slave
- Помочь реанимировать кластер при смерти Master-а.
- В автоматическом или ручном режиме Repmgr может избрать нового Master-а и перенастроить все Slave сервисы следовать за новым лидером.
- Выводить из кластера узлы
- Мониторить здоровье кластера
- Выполнять команды при событиях внутри кластера
В нашем случае на помощь приходит repmgrd
который запускается основным процессом в контейнере и следит за целостностью кластера. При ситуации, когда пропадает доступ к Master серверу, Repmgr пытается проанализировать текущую структуру кластера и принять решение о том, кто станет следующим Master-ом. Естественно Repmgr достаточно умен чтобы не создать ситуацию Split Brain и выбрать единственно правильного Master-а.
Pgpool-II — swiming pool of connections
Последняя часть системы — Pgpool. Как я писал в разделе о плохих решениях, сервис все же делает свою работу:
- Балансирует нагрузку между всеми уздами кластера
- Храннит дескрипторы соединений для оптимизации скорости доступа к БД
- При нашем случае Streaming Replication — автоматически находит Master и использует его для запросов на запись.
Как исход, у меня получился довольно простой Docker Image, который при старте конфигурирует себя на работу с набором узлов и пользователей, у которых будет возможность проходить md5 авторизацию сквозь Pgpool (c этим тоже, как оказалось, не все просто)
Очень часто возникает задача избавиться от единой точки отказа, и в нашем случае — этой точкой является pgpool сервис, который проксирует все запросы и может стать самым слабым звеном на пути доступа к данным.
К счастью, в этом случае нашу проблему решает k8s и позволяет сделать столько репликаций сервиса сколько нужно.
В примере для Kubernetes к сожалению этого нет, но если вы знакомы c тем как работает Replication Controller и/или Deployment, то провернуть вышеописанное вам не составит труда.
Результат
Эта статья — не пересказ скриптов для решения задачи, но описание структуры решения этой самой задачи. Что означает — для более глубокого понимания и оптимизации решения придется почитать код, как минимум README.md в github, который пошагово и дотошно рассказывает как запустить кластер для docker-compose и Kubernetes. Ко всему прочему, для тех, кто проникнется и решится с этим двигаться дальше, я готов протянуть виртуальную руку помощи.
Документация и использованный материал
PS:
Надеюсь, что изложенный материал будет полезен и подарит немного позитива перед началом лета! Удачи и хорошего настроения, коллеги ;)