Для начала на двух хостах (лучше железных) 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
Затем соберём образ с помощью докер-файла:
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
Теперь запускаем по сервису на каждом хосте
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 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 # берём готовый образ
Итак, вход в конейнер
#!/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 "$@" # и выполняем команду
Супервизор запускает следующие сервисы
#!/bin/sh exec 2>&1 realpath "$0" set -ex ssh-keygen -A # генерируем ключи хоста exec /usr/sbin/sshd -D -e # запускаем ssh-сервер
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
#!/bin/sh exec 2>&1 exec sed 's|^|ssh: |'
#!/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 # запускаем базу
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
#!/bin/sh exec 2>&1 exec sed 's|^|postgres: |'
Создаём директорию базы не мастера так
#!/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" # клонируем базу из мастера
Переключаем старый мастер на новый так
#!/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")" # переключаем базу на новый мастер (в случае неудачи - сохраняем базу и будем клонировать из мастера)
Создаём директорию базы мастера так
#!/bin/sh exec 2>&1 realpath "$0" set -ex /etc/service/postgres/init # инициализируем базу /etc/service/repmgr/init # инициализируем repmgr
Инициализируем базу так
#!/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 # дополняем файл доступов
Дополняем конфигурацию базы так
#!/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
Дополняем файл доступов так
#!/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"
#!/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
Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)
#!/bin/sh exec 2>&1 exec sed 's|^|repmgr: |'
Задаём конфигурацию не мастера так
#!/bin/sh exec 2>&1 realpath "$0" set -ex chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # задаём конфигурацию
Задаём конфигурацию мастера так
#!/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 # задаём конфигурацию
Превращаем не мастер в мастер так
#!/bin/sh exec 2>&1 realpath "$0" set -ex chpst -u "$USER":"$GROUP" repmgr standby promote --config-file="$HOME/repmgr.conf" --verbose # превращаем не мастер в мастер
Задаём конфигурацию так
#!/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 так
#!/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
Дополняем файл доступа базы так
#!/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

