Рецепты PostgreSQL: auto-failover и auto-rejoin в docker swarm

—

от автора

Для приготовления auto-failover и auto-rejoin в docker swarm нам понадобится docker, postgres, repmgr, pgbouncer, runit и gluster. Можно также воспользоваться готовым образом.

Для начала на двух хостах (лучше железных) 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

Супервизор выполняет файл

/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 

ссылка на оригинал статьи https://habr.com/ru/post/498132/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *