Когда почта ложится — ложится всё. Переписка, задачи, согласования, доступы — всё завязано на почтовый сервер. А если под ним единственный PostgreSQL без резервирования, то между вами и катастрофой — один сбойный диск или зависший процесс.

В этой статье — пошаговый рецепт: собираем отказоустойчивый кластер PostgreSQL на базе Patroni, используем etcd для консенсуса и HAProxy как единую точку входа. Три виртуальные машины — и ваша почта RuPost переживёт падение ноды без потери писем и без ручного вмешательства.

Архитектура решения

Схема проста и проверена в продакшене: два инстанса PostgreSQL с потоковой репликацией, etcd как распределённое хранилище состояния кластера, HAProxy для маршрутизации клиентских подключений к текущему мастеру. Это позволяет избежать split-brain и обеспечивает прозрачный failover.

Что делает каждый компонент

Компонент

Роль

Patroni

Управляет кластером PostgreSQL: автоматический failover, промоушен реплики в мастер, синхронизация конфигураций

etcd

Распределённое key-value хранилище. Хранит состояние кластера и определяет лидера. Нужен минимум на 3 нодах для кворума

HAProxy

Reverse proxy и единая точка входа. Проверяет через REST API, какая нода — мастер, и направляет трафик только к ней

PostgreSQL

База данных с потоковой репликацией между мастером и репликой

Шаг 1. Установка etcd на все ноды

etcd нужен на всех трёх ВМ. Он обеспечивает консенсус: при падении мастера PostgreSQL узлы голосуют за нового лидера.

Скачиваем и устанавливаем приложения. Затем удаляем временные файлы командами на всех ВМ

curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1
rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz

Проверяем корректность установки и перемещаем исполняемый файл в каталог /usr/local/bin/:

/tmp/etcd-download-test/etcd --version
/tmp/etcd-download-test/etcdctl version
mv /tmp/etcd-download-test/etcd* /usr/local/bin/

Создаём группу и пользователя в этой группе, от имени которого будет осуществляться работа etcd командами:

sudo groupadd --system etcd
sudo useradd -s /sbin/nologin --system -g etcd etcd

Создаём каталог /var/lib/etcd и предоставляем права на этот каталог пользователю etcd командами:

sudo mkdir -p /var/lib/etcd/
sudo mkdir /etc/etcd
sudo chown -R etcd:etcd /var/lib/etcd/
sudo chmod -R 700 /var/lib/etcd/

Следующие операции нужно выполнить на всех серверах, где будет установлен etcd.

Создаём файл службы /etc/systemd/system/etcd.service со следующим содержанием:

[Unit]
Description=etcd service
Documentation=https://github.com/coreos/etcd
[Service]
User=etcd
Type=notify
ExecStart=/usr/local/bin/etcd \
 --name ${ETCD_NAME} \
 --enable-v2=true \
 --data-dir /var/lib/etcd \
 --initial-advertise-peer-urls http://${ETCD_HOST_IP}:2380 \
 --listen-peer-urls http://${ETCD_HOST_IP}:2380 \
 --listen-client-urls http://${ETCD_HOST_IP}:2379,http://127.0.0.1:2379 \
 --advertise-client-urls http://${ETCD_HOST_IP}:2379 \
 --initial-cluster-token etcd-cluster-1 \
 --initial-cluster haproxy=http://192.168.42.9:2380,postgresql1=http:// 192.168.42.7:2380,postgresql2=http://192.168.42.8:2380 \
 --initial-cluster-state new \
 --heartbeat-interval 1000 \
 --election-timeout 5000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
  1. ${ETCD_NAME} — это имя хоста где запущен etcd

  2. ${ETCD_HOST_IP} — это ip адрес хоста где запущен etcd

Имена haproxy, postgresql1 и postgresql2 — это хостнеймы наших reverse proxy, первого инстанса postgresql и второго инстанса postgresql соответственно.

IP адреса 192.168.42.9, 192.168.42.7 и 192.168.42.8 — это ip адреса наших reverse proxy, первого инстанса postgresql и второго инстанса postgresql соответственно.

После этого запускаем etcd на всех ВМ командами:

systemctl daemon-reload
systemctl enable etcd
systemctl start etcd.service

Первый запущенный сервер будет сервером начальной загрузки. После того как выполнится запуск всех серверов, выберется лидер.

Проверить состояние кластера после запуска можно командами:

systemctl status -l etcd.service
etcdctl member list
etcdctl endpoint status --write-out=table
etcdctl endpoint health

Устанавливаем Postgresql на две ВМ, которые будут выступать в роли кластера postgresql, командами:

sudo apt install postgresql-14
dpkg -l | grep postgresql

Создаём пользователя для репликации в базе данных postgresql с именем replicator. Также в дальнейшем нам понадобится пользователь postgres. 

Останавливаем службу postgresql. Управление кластером будет осуществлять patroni:

sudo systemctl stop postgresql
sudo systemctl disable postgresql

Устанавливаем patroni командой:

apt install patroni

Создаём файл конфигурации для кластера patroni /etc/patroni.yml:

scope: postgres
name: postgresql2
 
restapi:
    listen: 0.0.0.0:8008
    connect_address: 192.168.42.8:8008
 
etcd:
    host: haproxy:2379
 
bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        logging_collector: "on"
        max_wal_senders: 5
        max_replication_slots: 5
 
  initdb:
  - encoding: UTF8
  - data-checksums
 
  pg_hba:
  - host replication replicator 127.0.0.1/32 trust
  - host replication replicator 192.168.42.8/24 md5
  - host all all 192.168.42.1/24 md5
  - host all all 0.0.0.0/0 md5
 
  users:
    admin:
      password: ********************
      options:
        - createrole
        - createdb
 
postgresql:
  listen: 0.0.0.0:5432
  connect_address: 192.168.42.8:5432
  data_dir: "/data/patroni"
  pgpass: /tmp/pgpass
  authentication:
    replication:
      username: replicator
      password: ********************
    superuser:
      username: postgres
      password: ********************
  parameters:
    unix_socket_directories: '/var/run/postgresql'
 
tags:
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false

postgresql2 — это имя первого хоста где развернут postgresql

Haproxy — это имя хоста где развернут reverse proxy. Также здесь указан мой ip адрес postgresql1 192.168.42.8. У вас имена и ip адреса будут отличаться. Указываются два пользователя:

Replicator для создания репликаций

Postgres — полный админ базы данных postgresql

Также создаём аналогичный файл конфигурации patroni на второй ВМ, где развернут второй инстанс postgresql. Добавляем в файл /etc/hosts все узлы, которые принимают участие. Это нужно сделать на всех трёх ВМ. Пример моего файла /etc/hosts

192.168.42.7    postgresql1

192.168.42.9    haproxy

192.168.42.8    postgresql2

Проверяем конфигурацию командой на всех нодах:

patroni --validate-config /etc/patroni.yml

Подготавливаем директорию для работы patroni:

sudo mkdir -p /data/patroni
sudo chown postgres:postgres /data/patroni
sudo chmod 700 /data/patroni

Создаём службу для запуска patroni:

sudo nano /etc/systemd/system/patroni.service
[Unit]
Description=High availability PostgreSQL Cluster
After=syslog.target network.target
[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni.yml
KillMode=process
TimeoutSec=30
Restart=no 
[Install]
WantedBy=multi-user.target

Запускаем patroni. Проверяем службу на всех ВМ:

sudo systemctl start patroni
sudo systemctl status patroni

Подготавливаем HAproxy для единой точки входа в наш кластер. Информацию о том, куда направлять соединения, haproxy будет получать от patroni.

Устанавливаем HAproxy командой:

sudo apt -y install haproxy

Создаём бэкап конфигурационного файла HAproxy и редактируем его под наши нужды:

sudo cp -p /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg-orig
sudo nano /etc/haproxy/haproxy.cfg

Используйте свои IP-адреса

global
    maxconn 100
 
defaults
    log global
    mode tcp
    retries 2
    timeout client 30m
    timeout connect 4s
    timeout server 30m
    timeout check 5s
 
listen stats
    mode http
    bind *:7000
    stats enable
    stats uri /
 
listen postgres
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server patroni1 192.168.42.7:5432 maxconn 100 check port 8008
    server patroni2 192.168.42.8:5432 maxconn 100 check port 8008

Проверяем конфигурационный файл на наличие ошибок командой:

sudo /usr/sbin/haproxy -c -V -f /etc/haproxy/haproxy.cfg

Запускаем HAproxy командой и проверяем состояние:

sudo systemctl restart haproxy
sudo systemctl status haproxy

После этого мы сможет подключаться к нашему кластеру postgresql по порту 7000

Перейдём к особенностям настройки кластера patroni для почтовой системы rupost

При конфигурации почтовой системы rupost необходимо при конфигурации базы данных поставить галочку использовать patroni как на рис. 1

Рисунок 1
Рисунок 1

Далее вам нужно ввести ip адрес любой из нод postgresql и порт API. И также логин и пароль пользователя rupost (как на рис.2)

Рисунок 2
Рисунок 2

Далее для корректной работы rupost необходимо увеличить количество подключений к базе postgresql. Для этого:

sudo patronictl -c /etc/patroni/config.yml edit-config

Поправить на 500. Затем нажать последовательно ctrl затем O

Затем Enter

Затем надо нажать ctrl + x

Подтвердите вносимые изменения, нажав Y

Для применения вносимых изменений нужно перезагрузить кластер patroni командой:

patronictl -c /etc/patroni/config.yml restart patroni-psql15

Подтвердите перезагрузку, нажав Y.

После этого изменения вступят в силу.

Таким образом, в статье полностью описан процесс настройки кластера patroni с особенностями использования для rupost.

Готовый кластер обеспечивает:

  • Автоматический failover — при падении мастера Patroni промоутит реплику, HAProxy переключает трафик

  • Единую точку входа — приложениям не нужно знать топологию кластера

  • Защиту от split-brain — etcd с кворумом из 3 нод исключает конфликт мастеров

  • Мониторинг — веб-панель HAProxy на порту 7000 для визуального контроля

Три ВМ, свободный стек, воспроизводимая настройка. Почта работает — даже когда сервер не хочет.