Для приготовления auto-failover и auto-rejoin в docker swarm нам понадобится docker, postgres, repmgr, pgbouncer, runit и gluster. Можно также воспользоваться готовым образом.
Для начала на двух хостах (лучше железных) docker1 и docker2 организуем docker swarm так, чтобы оба были менеджерами.
Также, на оба хоста установим распределённую файловую систему glusterfs и подмонтируем его на обоих хостах в fstab так
Потом на обоих хостах создадим раздел
и сеть
Затем соберём образ с помощью докер-файла:
Теперь запускаем по сервису на каждом хосте
На втором хосте запускаем второй сервис также, только вместо docker1 пишем docker2 и вместо repmgr1 пишем repmgr2
Итак, вход в конейнер
Супервизор запускает следующие сервисы
Для начала на двух хостах (лучше железных) docker1 и docker2 организуем docker swarm так, чтобы оба были менеджерами.
Также, на оба хоста установим распределённую файловую систему glusterfs и подмонтируем его на обоих хостах в fstab так
localhost:/gfs /mnt/gfs glusterfs defaults,_netdev,backupvolfile-server=localhost 0 0
Потом на обоих хостах создадим раздел
docker volume create repmgr
и сеть
docker network create --attachable --driver overlay docker
Затем соберём образ с помощью докер-файла:
Dockerfile
FROM alpine
RUN set -ex && apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --virtual .locales-rundeps musl-locales \ # добавлям локали (это не является необходимым, но для красоты картины пусть будет)
&& apk add --no-cache --virtual .postgresql-rundeps openssh-client \ # добавляем ssh-клиента (это не является необходимым, но может понадобиться для генерации ключей)
openssh-server \ # добавляем ssh-сервер (это не является необходимым, но требуется для switchover)
pgbouncer \ # добавляем балансировщик
postgresql \ # добавляем базу
postgresql-contrib \ # добавляем различные полезные расширения (это не является необходимым, но для красоты картины пусть будет)
repmgr \ # добавляем repmgr
repmgr-daemon \ # и сервис для него
rsync \ # добавляем синхронизатор (это не является необходимым, но для красоты картины пусть будет)
runit \ # добавляем лёгкий супервизор
shadow \ # добавляем управление пользователями
tzdata \ # добавляем зоны (это не является необходимым, но для красоты картины пусть будет)
&& echo done
ADD bin /usr/local/bin # добавляем скрипты
ADD service /etc/service # добавляем сервисы для супервизора
CMD [ "runsvdir", "/etc/service" ] # будем запускать супервизор
ENTRYPOINT [ "docker_entrypoint.sh" ] # но после входа
ENV HOME=/var/lib/postgresql
ENV GROUP=postgres PGDATA="${HOME}/pg_data" USER=postgres
VOLUME "${HOME}"
WORKDIR "${HOME}"
RUN set -ex && sed -i -e 's|#PasswordAuthentication yes|PasswordAuthentication no|g' /etc/ssh/sshd_config \ # запрещаем вход по паролю
&& sed -i -e 's|# StrictHostKeyChecking ask| StrictHostKeyChecking no|g' /etc/ssh/ssh_config \ # не будем ничего проверять
&& echo " UserKnownHostsFile=/dev/null" >>/etc/ssh/ssh_config \ # и не будем ничего никуда добавлять
&& sed -i -e 's|postgres:!:|postgres::|g' /etc/shadow \ # разрешаем вход пользователю базы
&& chmod -R 0755 /etc/service /usr/local/bin && echo done
Теперь запускаем по сервису на каждом хосте
service1.sh
docker service create --constraint node.hostname==docker1 \ # размещаем контейнер на первом хосте
--env GROUP_ID="$(id -g)" \ # задаём идентификатор группы (это не является необходимым и сделано для удобства)
--env LANG=ru_RU.UTF-8 \ # задаём кодировку (это не является необходимым, но для красоты картины пусть будет)
--env TZ=Asia/Yekaterinburg \ # задаём зону (это не является необходимым, но для красоты картины пусть будет)
--env USER_ID="$(id -u)" \ # задаём идентификатор пользователя (это не является необходимым и сделано для удобства)
--hostname tasks.repmgr1 \ # задаём хост
--mount type=bind,source=/etc/certs,destination=/etc/certs,readonly \ # монтируем папку с сертификатами (это не является необходимым, но для красоты картины пусть будет)
--mount type=bind,source=/mnt/gfs/repmgr,destination=/var/lib/postgresql/gfs \ # монтируем распределённую файловую систему
--mount type=volume,source=repmgr,destination=/var/lib/postgresql \ # монтируем раздел
--name repmgr1 \ # задаём имя сервиса
--network name=docker \ # используем сеть
--publish target=5432,published=5432,mode=host \ # публикуем порты базы
--publish target=5433,published=5433,mode=host \ # публикуем порты балансировщика
--replicas-max-per-node 1 \ # разрешаем только один конейнер на хосте
rekgrpth/repmgr # берём готовый образ
На втором хосте запускаем второй сервис также, только вместо docker1 пишем docker2 и вместо repmgr1 пишем repmgr2
Если научить docker резолвить hostname конейеров
так, то можно запустить один сервис сразу на обоих хостах
docker service create --env GROUP_ID="$(id -g)" \ # задаём идентификатор группы (это не является необходимым и сделано для удобства)
--env LANG=ru_RU.UTF-8 \ # задаём кодировку (это не является необходимым, но для красоты картины пусть будет)
--env TZ=Asia/Yekaterinburg \ # задаём зону (это не является необходимым, но для красоты картины пусть будет)
--env USER_ID="$(id -u)" \ # задаём идентификатор пользователя (это не является необходимым и сделано для удобства)
--hostname repmgr-{{.Node.Hostname}} \ # задаём хост
--mode global \ # запускаем по одному контейнеру на каждом хосте
--mount type=bind,source=/etc/certs,destination=/etc/certs,readonly \ # монтируем папку с сертификатами (это не является необходимым, но для красоты картины пусть будет)
--mount type=bind,source=/mnt/gfs/repmgr,destination=/var/lib/postgresql/gfs \ # монтируем распределённую файловую систему
--mount type=volume,source=repmgr,destination=/var/lib/postgresql \ # монтируем раздел
--name repmgr \ # задаём имя сервиса
--network name=docker \ # используем сеть
--publish target=5432,published=5432,mode=host \ # публикуем порты базы
--publish target=5433,published=5433,mode=host \ # публикуем порты балансировщика
rekgrpth/repmgr # берём готовый образ
Итак, вход в конейнер
/usr/local/bin/docker_entrypoint.sh
#!/bin/sh
exec 2>&1
set -ex
if [ -n "$GROUP" ] && [ -n "$GROUP_ID" ] && [ "$GROUP_ID" != "$(id -g "$GROUP")" ]; then # если задана группа и задан идентификатор группы и он другой, то
groupmod --gid "$GROUP_ID" "$GROUP" # заменим идентификатор группы
chgrp "$GROUP_ID" "$HOME" # и обновим группу у домашней директории
fi
if [ -n "$USER" ] && [ -n "$USER_ID" ] && [ "$USER_ID" != "$(id -u "$USER")" ]; then # если задан пользователь и задан идентификатор пользователя и он другой, то
usermod --uid "$USER_ID" "$USER" # заменим идентификатор пользователя
chown "$USER_ID" "$HOME" # и обновим пользователя у домашней директории
fi
exec "$@" # и выполняем команду
Супервизор запускает следующие сервисы
1) /etc/service/ssh
Этот сервис не является необходимым, но требуется для switchover. Супервизор выполняет файл
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
/etc/service/ssh/run
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
ssh-keygen -A # генерируем ключи хоста
exec /usr/sbin/sshd -D -e # запускаем ssh-сервер
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
/etc/service/ssh/log/run
#!/bin/sh
exec 2>&1
exec sed 's|^|ssh: |'
2) /etc/service/postgres
Супервизор выполняет файл
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
Создаём директорию базы не мастера так
Переключаем старый мастер на новый так
Создаём директорию базы мастера так
Инициализируем базу так
Дополняем конфигурацию базы так
Дополняем файл доступов так
/etc/service/postgres/run
#!/bin/sh
exec 2>&1
install -d -m 0750 -o "$USER" -g "$GROUP" "$PGDATA" # устанавливаем домашнюю директорию базы
install -d -m 1775 -o "$USER" -g "$GROUP" /run/postgresql /var/log/postgresql # устанавливаем директории запуска и логов
rm -f /run/postgresql/postgres.run # удаляем файл блокировки
realpath "$0"
set -ex
chmod 755 supervise
chown "$USER":"$GROUP" supervise/ok supervise/control supervise/status # разрешаем управлять сервисом пользователю
rm -f "$PGDATA/postmaster.pid" # удаляем pid
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" || echo -n "$(hostname)" >"$HOME/gfs/primary" # если мастер не задан, то мастер - текущий хост
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что теперь мастер задан
chown "$USER":"$GROUP" "$HOME/gfs/primary" # правим права
if [ "$primary" != "$(hostname)" ]; then # если текущий хост не мастер, то
test -d "$PGDATA/base" || /etc/service/postgres/standby # если нет директории базы - создаём её
test -f "$PGDATA/standby.signal" || /etc/service/postgres/rejoin # если текущий хост раньше был мастером, то переключаем его на новый мастер
else # иначе (текущий хост - мастер)
test -d "$PGDATA/base" || /etc/service/postgres/primary # если нет директории базы - создаём её
fi
test -d "$PGDATA/base" # удостоверяемся, что теперь есть директория базы
exec chpst -u "$USER":"$GROUP" -L /run/postgresql/postgres.run postmaster # запускаем базу
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
/etc/service/postgres/log/run
#!/bin/sh
exec 2>&1
exec sed 's|^|postgres: |'
Создаём директорию базы не мастера так
/etc/service/postgres/standby
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # создаём конфигурацию для repmgr
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
chpst -u "$USER":"$GROUP" repmgr standby clone --config-file="$HOME/repmgr.conf" --verbose --fast-checkpoint --dbname="host=$primary user=repmgr dbname=repmgr connect_timeout=2" # клонируем базу из мастера
Переключаем старый мастер на новый так
/etc/service/postgres/rejoin
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
chpst -u "$USER":"$GROUP" pg_ctl --options="-c listen_addresses=''" --wait start # запускаем базу локально (это нужно для pg_rewind)
chpst -u "$USER":"$GROUP" pg_ctl --wait --mode=fast stop # выключаем базу (это нужно для pg_rewind)
chpst -u "$USER":"$GROUP" repmgr node rejoin --config-file="$HOME/repmgr.conf" --verbose --force-rewind --no-wait --dbname="host=$primary user=repmgr dbname=repmgr connect_timeout=2" || mv -f "$PGDATA" "${PGDATA}_$(date "+%F %T")" # переключаем базу на новый мастер (в случае неудачи - сохраняем базу и будем клонировать из мастера)
Создаём директорию базы мастера так
/etc/service/postgres/primary
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
/etc/service/postgres/init # инициализируем базу
/etc/service/repmgr/init # инициализируем repmgr
Инициализируем базу так
/etc/service/postgres/init
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
cd "$PGDATA" && chpst -u "$USER":"$GROUP" initdb # инициализируем базу
chpst -u "$USER":"$GROUP" /etc/service/postgres/conf # дополняем конфигурацию базы
chpst -u "$USER":"$GROUP" /etc/service/postgres/hba # дополняем файл доступов
Дополняем конфигурацию базы так
/etc/service/postgres/conf
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
cat >>"$PGDATA/postgresql.conf" <<EOF
archive_command = '/bin/true' # фейковая архивация
archive_mode = on # но всё-таки включена
datestyle = 'iso, dmy' # задаём стиль даты
listen_addresses = '*' # слушаем всё
max_logical_replication_workers = 0 # отключаем логическую репликацию
max_sync_workers_per_subscription = 0 # отключаем логическую репликацию
ssl_ca_file = '/etc/certs/ca.pem' # задаём корневой сертификат
ssl_cert_file = '/etc/certs/cert.pem' # задаём сертификат
ssl_key_file = '/etc/certs/key.pem' # задаём приватный ключ
ssl = on # включаем ssl
EOF
Дополняем файл доступов так
/etc/service/postgres/hba
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
echo >>"$PGDATA/pg_hba.conf"
ip -o -f inet addr show | awk '/scope global/ {print $4}' | sed -E 's|.\d+/|.0/|' | while read -r net; do # для всех локальных сетей
echo "host all all $net trust" # разрешаем все соединения
done >>"$PGDATA/pg_hba.conf"
3) /etc/service/repmgr
Супервизор выполняет файл
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
Задаём конфигурацию не мастера так
Задаём конфигурацию мастера так
Превращаем не мастер в мастер так
Задаём конфигурацию так
Инициализируем repmgr так
Дополняем файл доступа базы так
/etc/service/repmgr/run
#!/bin/sh
exec 2>&1
install -d -m 1775 -o "$USER" -g "$GROUP" /run/postgresql # устанавливаем директории запуска
rm -f /run/postgresql/repmgr.run # удаляем файл блокировки
test -f /run/postgresql/postgres.run || exit $? # удостоверяемся, что запущен сервис базы
realpath "$0"
set -ex
chmod 755 supervise
chown "$USER":"$GROUP" supervise/ok supervise/control supervise/status # разрешаем управлять сервисом пользователю
chpst -u "$USER":"$GROUP" pg_ctl status # удостоверяемся, что запущен база
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
if [ "$primary" != "$(hostname)" ]; then # если текущий хост не мастер, то
test -f "$HOME/repmgr.conf" || /etc/service/repmgr/standby # если не задана конфигурация, то задаём её
chpst -u "$USER":"$GROUP" repmgr standby register --config-file="$HOME/repmgr.conf" --verbose --force # принудительно регистрируемся как не мастер
else # иначе (текущий хост - мастер)
test -f "$HOME/repmgr.conf" || /etc/service/repmgr/primary # если не задана конфигурация, то задаём её
test ! -f "$PGDATA/standby.signal" || /etc/service/repmgr/promote # если текущий хост раньше был не мастером, то превращаем его в мастер
chpst -u "$USER":"$GROUP" repmgr primary register --config-file="$HOME/repmgr.conf" --verbose --force # принудительно регистрируемся как мастер
fi
exec chpst -u "$USER":"$GROUP" -L /run/postgresql/repmgr.run repmgrd --config-file="$HOME/repmgr.conf" --verbose --daemonize=false # запускаем сервис repmgr
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
/etc/service/repmgr/log/run
#!/bin/sh
exec 2>&1
exec sed 's|^|repmgr: |'
Задаём конфигурацию не мастера так
/etc/service/repmgr/standby
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # задаём конфигурацию
Задаём конфигурацию мастера так
/etc/service/repmgr/primary
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" createuser --superuser repmgr # создаём пользователя
chpst -u "$USER":"$GROUP" createdb repmgr --owner=repmgr # создаём базу
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # задаём конфигурацию
Превращаем не мастер в мастер так
/etc/service/repmgr/promote
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" repmgr standby promote --config-file="$HOME/repmgr.conf" --verbose # превращаем не мастер в мастер
Задаём конфигурацию так
/etc/service/repmgr/conf
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
cat >"$HOME/repmgr.conf" <<EOF
conninfo='host=$(hostname) user=repmgr dbname=repmgr connect_timeout=2' # задаём строку подключения к локальной базе
data_directory='$(echo -n "$PGDATA")' # задаём директорию локальной базы
event_notification_command='/etc/service/repmgr/event "%n" "%e" "%s" "%t" "%d"' # задаём команду уведомлений
failover='automatic' # задаём автоматический failover
failover_validation_command='/etc/service/repmgr/valid "%n" "%a"' # задаём команду проверки
follow_command='repmgr standby follow --config-file="$(echo -n "$HOME")/repmgr.conf" --wait --upstream-node-id=%n' # задаём команду следования
node_id=$(hostname | grep -oE '\d+') # задаём номер узла
node_name='$(hostname)' # задаём имя узла
promote_command='repmgr standby promote --config-file="$(echo -n "$HOME")/repmgr.conf"' # задаём команду промотки
repmgrd_service_start_command='sv start repmgr' # задаём команду запуска сервиса
repmgrd_service_stop_command='sv force-stop repmgr' # задаём команду остановки сервиса
service_promote_command='pg_ctl --wait promote' # задаём команду промотки базы
service_reload_command='sv reload postgres' # задаём команду обновления конфигурации базы
service_restart_command='sv force-restart postgres' # задаём команду перезагрузки базы
service_start_command='sv start postgres' # задаём команду запуска базы
service_stop_command='sv force-stop postgres' # задаём команду остановки базы
ssh_options='-q -o ConnectTimeout=10' # задаём опции ssl
standby_disconnect_on_failover=true # отключаемся при failover
use_replication_slots=true # используем слоты репликации
EOF
Инициализируем repmgr так
/etc/service/repmgr/init
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/hba # дополняем файл доступа базы
chpst -u "$USER":"$GROUP" cat >>"$PGDATA/postgresql.conf" <<EOF # дополняем файл конфигурации базы
hot_standby = on # включаем горячий резерв
max_replication_slots = 10 # задаём максимальное количество слотов репликации
max_wal_senders = 10 # задаём максимальное количество отправщиков
shared_preload_libraries = 'repmgr' # загружаем repmgr
wal_level = 'hot_standby' # задаём уровень горячий резерв
wal_log_hints = on # включаем подсказки для pg_rewind
EOF
Дополняем файл доступа базы так
/etc/service/repmgr/hba
#!/bin/sh
exec 2>&1
realpath "$0"
set -ex
echo >>"$PGDATA/pg_hba.conf"
ip -o -f inet addr show | awk '/scope global/ {print $4}' | sed -E 's|.\d+/|.0/|' | while read -r net; do # для всех локальных сетей
echo "host replication repmgr $net trust" # разрешаем репликацию для repmgr
echo "host repmgr repmgr $net trust" # разрешаем repmgr для repmgr
done >>"$PGDATA/pg_hba.conf"
cat >>"$PGDATA/pg_hba.conf" <<EOF
local replication repmgr trust # разрешаем репликацию для repmgr
host replication repmgr 127.0.0.1/32 trust # разрешаем репликацию для repmgr
host replication repmgr ::1/128 trust # разрешаем репликацию для repmgr
local repmgr repmgr trust # разрешаем repmgr для repmgr
host repmgr repmgr 127.0.0.1/32 trust # разрешаем repmgr для repmgr
host repmgr repmgr ::1/128 trust # разрешаем repmgr для repmgr
EOF