Пару месяцев назад мы переехали из Амазон на свои выделенные сервера(Hetzner), одна из причин тому была высокая стоимость RDS. Встала задача настроить и запустить master-slave кластер на выделенных серверах. После гугления и прочтения официальной документации, было принято решение собрать свое собственное решение высокодоступного асинхронного кластера Postgres.
Цели
- Использовать как можно меньше инструментов и зависимостей.
- Стремится к прозрачности, никакой магии!
- Не использовать комбайны all-included типа pg-pool, stolon etc.
- Использовать докер и его плюшки.
Итак, начнём. Собственно нам понадобится сам Postgres и такой замечательный инструмент как repmgr, который занимается управлением репликаций и мониторингом кластера.
Проект называется pg-dock, состоит из 3 частей, каждая часть лежит на гитхабе, их можно брать и видоизменять как заблагорассудится.
- pg-dock-config готовый набор файлов конфигурации, сейчас там прописано 2 нода, мастер-слейв.
- pg-dock занимается упаковкой конфигов и доставкой их на ноды, в нужном виде и в нужное место.
- pg-dock-base это базовый докер образ который и будет запускаться на нодах.
Давайте детально разберем каждую часть:
pg-dock-config
Конфигурация кластера, имеет следующую структуру
В репозитории уже прописаны два нода (n1, n2), если нод у Вас больше, то просто создаем еще одну папку с названием новой ноды. Для каждый ноды свои файлы конфигурации. Мне кажется тут всё довольно просто, например папка env это переменные окружения которые будут подхватываться docker-compose'ом, папка postgres соответственно конфиги постгреса и.т.д. Например файл pg-dock-conf/n1/env/main
Говорит нам о том что при первичной инициализации постгреса будет создан юзер postgres и база testdb. Так же тут прописаны переменные для failover-ip скрипта который меняет ip на новую мастер ноду в случае если старая стала не доступной. pg-dock-conf/n1/env/backup Переменные окружения для интервального бекапа базы на s3, подхватывается docker-compose'ом при старте сервиса. |
Если у нас есть общие файлы конфигурации, то что бы не дублировать их по нодам, будем класть их в папку shared.
Пройдемся по ее структуре:
- failover
В моем случае там скрипт для Hetzner failover-ip, который меняет ip на новый мастер. В Вашем случае это может быть скрипт keepalived или еще что то подобное. - initdb
Все инициализирующие sql запросы надо положить в эту папку. - ssh
Тут лежат ключи подключения к другому ноду, в нашем примере, ключи на всех нодах одни и те же, поэтому они лежат в папке shared. ??Ssh нужен repmgr что бы делать такие манипуляции как switchover и.т.п - sshd
Файл конфигурации ssh сервера, ssh у нас будет работать на порту 2222 что бы не пересекаться с дефолтным портом на хосте (22)
pg-dock
Тут собственно происходит упаковка конфигурации для каждой ноды.
Суть заключается в том что бы запаковать конфигурацию ноды в докер образ, запушить его в хаб или свой registry, и потом на ноде сделать обновление.
Для работы есть базовые операции, создать билд конфига (build.sh), обновить конфиг на ноде (update.sh) и запустить сам кластер (docker-compose.yml)
|
При запуске:
PG_DOCK_NODE=n1 PG_DOCK_CONF_IMAGE=n1v1 ./build.sh
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
n1v1 latest 712e6b2ace1a 6 minutes ago 1.17MB
Конфигурация pg-dock-conf/n1 скопируется в папку pg-dock/pg-dock-conf-n1, затем запустится docker build со всеми зависимостями, на выходе получаем образ с именем n1v1 в котором хранится наша конфигурация для нода n1.
При запуске:
PG_DOCK_CONF_IMAGE=n1v1 ./update.sh
Запустится контейнер, который обновит все файлы конфигурации на хосте. Таким образом мы можем иметь несколько образов конфигурации, делать rollback на разные версии и.т.п
pg-docker-base
Базовый docker образ в котором установлены все пакеты для работы кластера: repmgr, rsync, openssh-server, supervisor (Dockerfile). Сам образ базируется на последней версии postgres 9.6.3, но можно использовать любой другой билд. Компоненты запускаются supervisor'ом из под юзера postgres. Этот образ мы и будем запускать на наших серверах (rsync, openssh-server требуются для работы repmgr).
Давайте запустим кластер!
Для удобства, в этой статье все манипуляции будут происходит при помощи docker-machine.
Клонируем проекты pg-dock и pg-dock-conf в рабочую папку (для примера lab)
mkdir ~/lab && cd ~/lab
git clone https://github.com/xcrezd/pg-dock
git clone https://github.com/xcrezd/pg-dock-conf
Создаем ноды, группу и пользователя postgres (uid, gid должен быть 5432 на хосте и в контейнере)
docker-machine create n1
docker-machine ssh n1 sudo addgroup postgres --gid 5432
docker-machine ssh n1 sudo adduser -u 5432 -h /home/postgres --shell /bin/sh -D -G postgres postgres
#для debian/ubuntu
#sudo adduser --uid 5432 --home /home/postgres --shell /bin/bash --ingroup postgres --disabled-password postgres
docker-machine create n2
docker-machine ssh n2 sudo addgroup postgres -g 5432
docker-machine ssh n2 sudo adduser -u 5432 -h /home/postgres --shell /bin/sh -D -G postgres postgres
Добавляем ip нод в /etc/hosts
docker-machine ip n1
#192.168.99.100
docker-machine ip n2
#192.168.99.101
# в ноду n1
docker-machine ssh n1 "sudo sh -c 'echo 192.168.99.100 n1 >> /etc/hosts'"
docker-machine ssh n1 "sudo sh -c 'echo 192.168.99.101 n2 >> /etc/hosts'"
# в ноду n2
docker-machine ssh n2 "sudo sh -c 'echo 192.168.99.100 n1 >> /etc/hosts'"
docker-machine ssh n2 "sudo sh -c 'echo 192.168.99.101 n2 >> /etc/hosts'"
Если IP ваших машин отличаются от IP в статье, то дополнительно надо добавить их в
- pg-dock-config/n1/postgres/pg_hba.conf
- pg-dock-config/n2/postgres/pg_hba.conf
Создаем образы конфигураций и сразу обновляем их на нодах
cd pg-dock
docker-machine use n1
PG_DOCK_NODE=n1 PG_DOCK_CONF_IMAGE=n1v1 ./build.sh
PG_DOCK_CONF_IMAGE=n1v1 ./update.sh
docker-machine use n2
PG_DOCK_NODE=n2 PG_DOCK_CONF_IMAGE=n2v1 ./build.sh
PG_DOCK_CONF_IMAGE=n2v1 ./update.sh
Обратите внимание на команду docker-machine use (как сделать), при каждом ее применении, мы меняем контекст докер клиента, то есть в первом случае все манипуляции с докером будут на ноде n1 а потом на n2.
Запускаем контейнеры
docker-machine use n1
PG_DOCK_NODE=n1 docker-compose up -d
docker-machine use n2
PG_DOCK_NODE=n2 docker-compose up -d
docker-compose так же запустит контейнер pg-dock-backup, который будет делать переодический бекап на s3.
Теперь посмотрим, где хранятся нужные нам файлы:
Файлы |
Хост |
Контейнер |
БД |
/opt/pg-dock/data |
/var/lib/postgresql/data |
Логи |
/opt/pg-dock/logs |
/var/log/supervisor |
Конфигурация и скрипты |
/opt/pg-dock/scripts |
** изучите docker-compose.yml |
Идем дальше, настраиваем кластер
docker-machine use n1
#Регестрируем как мастер ноду
docker exec -it -u postgres pg-dock repmgr master register
docker-machine use n2
#Клонируем данные из нода n1
docker exec -it -u postgres -e PG_DOCK_FROM=n1 pg-dock manage/repmgr_clone_standby.sh
#Регестрируем ноду как слейв
docker exec -it -u postgres pg-dock repmgr standby register
Вот и все, кластер готов
docker exec -it -u postgres pg-dock repmgr cluster show
Role | Name | Upstream | Connection String
----------+------|----------|--------------------------------------------
* master | n1 | | host=n1 port=5432 user=repmgr dbname=repmgr
standby | n2 | n1 | host=n2 port=5432 user=repmgr dbname=repmgr
Давайте проверим его роботоспособность. В папке pg-dock-config/shared/tests у нас есть такие вот заготовки для тестирования нашего кластера:
#Создает тестовую таблицу
cat tests/prepare.sh
CREATE TABLE IF NOT EXISTS testtable (id serial, data text);
GRANT ALL PRIVILEGES ON TABLE testtable TO postgres;
#Добавляет 100000 записей
cat tests/insert.sh
insert into testtable select nextval('testtable_id_seq'::regclass), md5(generate_series(1,1000000)::text);
#Считает сколько записей в таблице
cat tests/select.sh
select count(*) from testtable;
Cоздаем тестовую таблицу, набиваем ее данными и проверяем если они есть на слейве:
docker-machine use n1
#Создаем тестовую таблицу для проверки репликации
docker exec -it -u postgres pg-dock config/tests/prepare.sh
#Добавляем записи для проверки
docker exec -it -u postgres pg-dock config/tests/insert.sh
INSERT 0 1000000
docker-machine use n2
#Проверяем что записи находятся на n2 (репликация)
docker exec -it -u postgres pg-dock config/tests/select.sh
count
---------
1000000
(1 row)
Профит!
Теперь давайте рассмотрим сценарий падения мастера:
#Останавливаем мастер ноду
docker-machine use n1
docker stop pg-dock
#Смотрим логи repmgr у слейва
docker-machine use n2
docker exec -it pg-dock tailf /var/log/supervisor/repmgr-stderr.log
#NOTICE: STANDBY PROMOTE successful
Is the server running on host «n1» (192.168.99.100) and accepting
TCP/IP connections on port 5432?
[2017-07-12 12:51:49] [ERROR] connection to database failed: could not connect to server: Connection refused
Is the server running on host «n1» (192.168.99.100) and accepting
TCP/IP connections on port 5432?
[2017-07-12 12:51:49] [WARNING] connection to master has been lost, trying to recover… 60 seconds before failover decision
[2017-07-12 12:51:59] [WARNING] connection to master has been lost, trying to recover… 50 seconds before failover decision
[2017-07-12 12:52:09] [WARNING] connection to master has been lost, trying to recover… 40 seconds before failover decision
[2017-07-12 12:52:19] [WARNING] connection to master has been lost, trying to recover… 30 seconds before failover decision
[2017-07-12 12:52:29] [WARNING] connection to master has been lost, trying to recover… 20 seconds before failover decision
[2017-07-12 12:52:39] [WARNING] connection to master has been lost, trying to recover… 10 seconds before failover decision
[2017-07-12 12:52:49] [ERROR] unable to reconnect to master (timeout 60 seconds)…
[2017-07-12 12:52:54] [NOTICE] this node is the best candidate to be the new master, promoting…
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 171 100 143 0 28 3 0 0:00:47 0:00:39 0:00:08 31
ERROR: connection to database failed: could not connect to server: Connection refused
Is the server running on host «n1» (192.168.99.100) and accepting
TCP/IP connections on port 5432?
NOTICE: promoting standby
NOTICE: promoting server using '/usr/lib/postgresql/9.6/bin/pg_ctl -D /var/lib/postgresql/data promote'
NOTICE: STANDBY PROMOTE successful
Смотрим статус кластера:
docker exec -it -u postgres pg-dock repmgr cluster show
Role | Name | Upstream | Connection String
---------+------|----------|--------------------------------------------
FAILED | n1 | | host=n1 port=5432 user=repmgr dbname=repmgr
* master | n2 | | host=n2 port=5432 user=repmgr dbname=repmgr
Теперь новый мастер у нас n2, failover ip тоже указывает на него.
Теперь давайте вернем старый мастер уже как новый слейв
docker-machine use n1
#Поднимаем контейнеры
PG_DOCK_NODE=n1 docker-compose up -d #как демон
#Клонируем данные из ноды n2
docker exec -it -u postgres -e PG_DOCK_FROM=n2 pg-dock manage/repmgr_clone_standby.sh
#Регестрируем ноду как слейв
docker exec -it -u postgres pg-dock repmgr standby register -F
Смотрим статус кластера:
docker exec -it -u postgres pg-dock repmgr cluster show
Role | Name | Upstream | Connection String
---------+------|-----------|--------------------------------------------
* master | n2 | | host=n2 port=5432 user=repmgr dbname=repmgr
standby| n1 | n2 | host=n1 port=5432 user=repmgr dbname=repmgr
Готово! И вот, что у нас получилось сделать; Мы уронили мастер, сработало автоматическое назначение слейва новым мастером, поменялся failover IP. Система продолжает функционировать. Потом мы реанимировали ноду n1, сделали ее новым слейвом. Теперь уже ради интереса, мы сделаем swithover — то есть вручную сделаем n1 мастером а n2 слейвом, как было раньше. Вот как раз для этого repmgr и нужен ssh, слейв подключается по ssh к мастеру и скриптами делает нужные манипуляции.
switchover:
docker-machine use n1
docker exec -it -u postgres pg-dock repmgr standby switchover
#NOTICE: switchover was successful
Warning: Permanently added '[n2]:2222,[192.168.99.101]:2222' (ECDSA) to the list of known hosts.
NOTICE: 1 files copied to /tmp/repmgr-n2-archive
NOTICE: current master has been stopped
ERROR: connection to database failed: could not connect to server: Connection refused
Is the server running on host «n2» (192.168.99.101) and accepting
TCP/IP connections on port 5432?
NOTICE: promoting standby
NOTICE: promoting server using '/usr/lib/postgresql/9.6/bin/pg_ctl -D /var/lib/postgresql/data promote'
server promoting
NOTICE: STANDBY PROMOTE successful
NOTICE: Executing pg_rewind on old master server
Warning: Permanently added '[n2]:2222,[192.168.99.101]:2222' (ECDSA) to the list of known hosts.
Warning: Permanently added '[n2]:2222,[192.168.99.101]:2222' (ECDSA) to the list of known hosts.
NOTICE: 1 files copied to /var/lib/postgresql/data
Warning: Permanently added '[n2]:2222,[192.168.99.101]:2222' (ECDSA) to the list of known hosts.
Warning: Permanently added '[n2]:2222,[192.168.99.101]:2222' (ECDSA) to the list of known hosts.
NOTICE: restarting server using '/usr/lib/postgresql/9.6/bin/pg_ctl -w -D /var/lib/postgresql/data -m fast restart'
pg_ctl: PID file "/var/lib/postgresql/data/postmaster.pid" does not exist
Is server running?
starting server anyway
NOTICE: replication slot «repmgr_slot_1» deleted on node 2
NOTICE: switchover was successful
Смотрим статус кластера:
docker exec -it -u postgres pg-dock repmgr cluster show
Role | Name | Upstream | Connection String
----------+------|----------|--------------------------------------------
standby | n2 | | host=n2 port=5432 user=repmgr dbname=repmgr
* master | n1 | | host=n1 port=5432 user=repmgr dbname=repmgr
Вот и все, в следующий раз когда нам надо обновить конфигурацию ноды, будь то конфиг postgres, repmgr или supervisor'a, мы просто пакуем ее и обновляем:
PG_DOCK_NODE=n1 PG_DOCK_CONF_IMAGE=n1v1 ./build.sh
PG_DOCK_CONF_IMAGE=n1v1 ./update.sh
После обновления новой конфигурации:
#Обновляем конфигурацию postgres
docker exec -it -u postgres pg-dock psql -c "SELECT pg_reload_conf();"
#Обновляем конфигурацию supervisor
docker exec -it -u postgres pg-dock supervisorctl reread
#перезапускаем отдельный процесс
docker exec -it -u postgres pg-dock supervisorctl restart foo:sshd
* Приятный бонус, supervisor имеет функцию ротацию логов, так что и за это нам не надо переживать.
* Контейнеры работают напрямую через сеть хоста, тем самым избегая задержек виртуализации сети.
* Рекомендую добавить уже существующие продакшен ноды в docker-machine, это сильно упростит Вам жизнь.
Теперь давайте коснемся темы балансировки запросов. Не хотелось усложнять (то есть использовать pg-pool, haproxy, stolon) поэтому балансировку мы будем делать на стороне приложения, тем самым снимая с себя обязанности по организации высокодоступности уже самого балансировщика. Наши бекенды написаны на руби, поэтому выбор пал на гем makara. Гем умеет разделять запросы на выборку и модификацию данных (insert/update/delete/alter), запросы на выборку можно балансировать между несколькими нодами (слейвами). В случае отказа одного из нод, гем умеет временно исключать его из пула.
Пример файла конфигурации database.yml:
production:
adapter: 'postgresql_makara'
makara:
# the following are default values
blacklist_duration: 5
master_ttl: 5
master_strategy: failover
sticky: true
connections:
- role: master
database: mydb
host: 123.123.123.123
port: 6543
weight: 3
username: <%= ENV['DATABASE_USERNAME'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
- role: slave
database: mydb
host: 123.123.123.124
port: 6543
weight: 7
username: <%= ENV['DATABASE_USERNAME'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
Библиотеки на других языках/фреймворках:
> laravel
> Yii2
> Node.js
Заключение
Итак, что мы получили в итоге:
- Самодостаточный кластер master-standby готовый к бою.
- Прозрачность всех компонент, легкая заменимость.
- Автоматический failover в случае отказа мастера (repmgr)
- Балансировка нагрузки на клиенте, тем самым снимая ответственность за доступность самого балансировщика
- Отсутствие единой точки отказа, repmgr запустит скрипт который перенесет IP адрес на новую ноду, которая была повышена до мастера в случае отказа. В темплейте есть скрипт для hetzner, но ничего не мешает добавить keepalived, aws elasticIp, drdb, pacemaker, corosync.
- Контроль версий, возможность делать rollback в случае неполадок / ab testing.
- Возможность настроить систему под себя, добавлять ноды, repmgr witness, например, гибкость конфигурации и ее изменений.
- Периодический бекап на S3
В следующей статье я расскажу, как на одной ноде разместить pg-dock и PgBouncer не теряя при этом в высокодоступности, всем спасибо за внимание!
Рекомендации к ознакомлению:
Комментарии (16)
ushliy
31.07.2017 11:59+3Вот не знаю, как реагировать.
Т.е., Pgpool или классический скрипт свитчовера — это неверный путь, Pgpool — это, значит, комбайн, все в одном, а Докер — нет?
Думаю, от докера оверхеда больше. Да еще и ssh в контейнерах. Тем более, балансировку вы сделали на стороне приложения.
Зачем все это?
Разложить конфиги по серверам можно Ансиблом, либо его аналогом, бэкапы тоже обычным скриптом из крона можно делатьcrezd
31.07.2017 12:12+1Спасибо, хороший комментарий.
Pgpool — это, значит, комбайн, все в одном, а Докер — нет?
Докер тут используется как упаковщик софта, он не имеет отношения к самой репликации кластера, поэтому головной боли меньше.
Зачем все это?
Разложить конфиги по серверам можно Ансиблом, либо его аналогом, бэкапы тоже обычным скриптом из крона можно делать
Можно, а можно еще как то, не суть, преследовалась цель быстро развернуть постгрес кластер, будь то локалка, стейдж или прод. Сделать готовый темплейт в котором можно просто что то поменять, локально протестировать и сразу в бой. Во вторых нормальных готовых к употреблению готовых конфигураций просто нет, поэтому пришлось сделать своё.
Получился весьма рабочий вариант, чем и делюсь с сообществом. Решение имеет право на жизнь как и решения Ансиблом например.
dos
31.07.2017 12:22А если смоделировать такую ситуацию — потерялась связь между мастером и слейвом и слейв стал мастером. Таким образом мы получили два рабочих мастера, которые не знают друг о друге. Мы делаем ряд разных записей в одну таблицу в оба мастера. Далее связь восстановилась — как восстановить кластер?
crezd
31.07.2017 12:30+1Мы делаем ряд разных записей в одну таблицу в оба мастера.
В Вашей ситуации, при разрыве связи между мастером и слейвом, слейв станет мастером и поменяет failover IP на свой. Таким образом запись будет идти только в новый мастер.
Далее связь восстановилась — как восстановить кластер?
Так как написано в статье, сделать из упавшено мастера новый слейв и жить дальше.
neenik
31.07.2017 13:46stolon гораздо лучше своего велосипеда.
crezd
31.07.2017 14:07+1stolon — это ПО со своими багами и замарочками — то есть дополнительная точка отказа, а тут просто конструктор из минимального количества компонент, с которыми достаточно запустить высокодоступный кластер.
neenik
31.07.2017 14:12Странно, что с таким подходом вы к докеру пришли (как уже выше писали). Ну а stolon — тогда не одна точка отказа, там ещё и etcd/consul (проверенные временем и продакшеном).
gibson_dev
31.07.2017 21:20+1Странно видеть когда в докер пихают ssh (init.d и прочее), а потом спрашивают — какого ляда оно не работает? Докер — это контейнер для приложения (причем одного), а не виртуальная машина. Его задача упаковать приложение с зависимостями — а как этим управлять — задача хоста. А вообще как по мне держать бд в проде в докере — моветон. В деве или стейджах — ради бога.
crezd
31.07.2017 21:32Вы серьезно? :)
Докер — это контейнер для приложения (причем одного), а не виртуальная машина.
Запускать несколько процессов в одном контейнере это НЕ антипатерн, как хотите так и делаете, 1 процесс, 2, 3, 100, не важно. Всё нормально.
а как этим управлять — задача хоста
Это как? Управление контейнерами идет через докер.
А вообще как по мне держать бд в проде в докере — моветон.
Окей, но есть много тех кто успешно держит бд в продакшене, поищите хотя бы по хабру. Главное знать как его готовить, использовать сеть хоста и.т.д
neumeika
31.07.2017 21:41Не могу не поддержать. У меня тут под боком продовый master-slave mysql и mongo в докере (поднимал не я, я знаю что такое docker stateless). При нормальной боевой нагрузке эти штуки ведут себя крайне интересно, хочется взять и разломать всё к чертям =).
grossws
01.08.2017 00:02+1А вообще как по мне держать бд в проде в докере — моветон.
Кто вам сказал? Depends on, как и всегда. При использовании bind mount ничего страшного не происходит, а netns и прочие позволяют более-менее унифицировать конфигурации, безболезненно тестировать миграцию на новые версии БД и т. п.
acmnu
Это не сценарий падения. Это сценарий ручного файловера у вас получился. Запустите продакшен лайк нагрузку и кильните с помощью kill -9 процесс postgres. Что выйдет? Или оборовите сетку между активной нодой и repmgr. Не получится ли split mind?
crezd
Тут всё нормально, выйдет как и в статье, проверяли. Но вы совершенно правы, проверять такой сценарий перед релизом в прод надо обязательно.
Так это проблема нескольких слейвов, статья то не об этом. А проблема эта решается witness сервером.