Отказоустойчивый кластер PostgreSQL: настройка Patroni, PgBouncer и репликации | AdminWiki

Отказоустойчивый кластер PostgreSQL: настройка Patroni, PgBouncer и репликации

27 июля 2026 12 мин. чтения
Содержание статьи

Отказоустойчивый кластер PostgreSQL строится на трех ключевых компонентах: Patroni для автоматического failover, PgBouncer для балансировки нагрузки и потоковая репликация для синхронизации данных. Это руководство содержит пошаговый план развертывания отказоустойчивой конфигурации, проверенной в production-средах. Вы выполните настройку синхронной и асинхронной репликации, добьетесь автоматического переключения при сбое мастера и интегрируете пулер соединений для обслуживания сотен клиентов без деградации производительности.

Каждая команда и параметр конфигурации протестированы на PostgreSQL 16 и Patroni 3.x. Материал ориентирован на практический результат: минимальное время простоя, нулевая потеря данных в синхронном режиме и прозрачное для приложений переключение узлов. Вы получите готовые конфиги, которые останется адаптировать под свои IP-адреса и требования к отказоустойчивости.

Архитектура отказоустойчивого кластера PostgreSQL

Кластер состоит из трех и более узлов PostgreSQL, управляемых демоном Patroni. Patroni отслеживает состояние каждого экземпляра, назначает роль primary одному узлу и переводит остальные в режим реплик. При отказе primary Patroni инициирует выборы нового лидера через распределенное хранилище конфигурации (DCS). PgBouncer располагается перед кластером и маршрутизирует клиентские подключения: запись уходит на primary, чтение распределяется между репликами. Такая архитектура исключает единую точку отказа и позволяет пережить потерю любого узла без вмешательства администратора.

Компоненты кластера: Patroni, PgBouncer, репликация и DCS

Patroni - это Python-демон, управляющий жизненным циклом экземпляра PostgreSQL. Он запускает и останавливает postgres, выполняет инициализацию реплики из бэкапа, переводит узел в режим чтения или записи. Patroni использует DCS для хранения информации о текущем лидере, конфигурации кластера и блокировках. Поддерживаются etcd, Consul и ZooKeeper. На практике чаще выбирают etcd - он легковесен, написан на Go и разворачивается за считанные минуты.

PgBouncer - легковесный пулер соединений. PostgreSQL создает отдельный процесс на каждое подключение, что при сотнях одновременных клиентов приводит к расходу памяти и падению пропускной способности. PgBouncer держит постоянный пул соединений к базе и мультиплексирует клиентские запросы через них. Поддерживаются три режима пулинга: session, transaction и statement. Для production рекомендуется transaction - соединение возвращается в пул после завершения транзакции, а не сессии.

Репликация в PostgreSQL реализована через потоковую передачу WAL (Write-Ahead Log). Primary пишет изменения в журнал, реплики стримят его и применяют к своим данным. Patroni управляет слот-репликацией и автоматически создает слоты на primary для каждой реплики. Это гарантирует, что primary не удалит сегменты WAL, пока реплика их не получила.

Сценарии использования и требования к инфраструктуре

Типовой сценарий - production-среда с требованием к доступности 99.99% и выше. Кластер Patroni с синхронной репликацией обеспечивает RPO (Recovery Point Objective) равный нулю: при отказе primary данные не теряются. Асинхронный режим подходит для проектов, где допустима потеря нескольких секунд транзакций, но критична производительность на запись. Еще один сценарий - балансировка нагрузки на чтение: приложения с интенсивным чтением направляют SELECT-запросы на реплики через PgBouncer, разгружая primary.

Минимальные требования к инфраструктуре: три физических или виртуальных сервера с идентичной конфигурацией. Два узла - это минимум для отказоустойчивости, но при split-brain без третьего голосующего узла DCS не сможет определить победителя. Рекомендуется размещать узлы в разных стойках или зонах доступности. Сетевые задержки между узлами не должны превышать 5-10 мс для синхронной репликации, иначе каждая транзакция будет ждать подтверждения от реплики. Версии ПО: PostgreSQL 14+, Patroni 3.0+, etcd 3.5+, PgBouncer 1.21+.

Подготовка среды для развертывания кластера

Подготовка начинается с синхронизации времени через NTP на всех узлах. Расхождение часов даже на секунду приводит к ошибкам в логах и усложняет диагностику. Настройте файрвол: открытые порты 5432 (PostgreSQL), 8008 (Patroni REST API), 2379-2380 (etcd peer и client), 6432 (PgBouncer). Имена узлов должны разрешаться через DNS или /etc/hosts на всех серверах. Используйте одинаковые версии ПО - Patroni проверяет совместимость и откажется работать при расхождении major-версий PostgreSQL на узлах.

Установка и базовая настройка PostgreSQL на всех узлах

Добавьте официальный репозиторий PostgreSQL и установите пакет:

sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-16

Остановите кластер PostgreSQL - Patroni будет управлять им самостоятельно:

sudo systemctl stop postgresql
sudo systemctl disable postgresql

Настройте аутентификацию в pg_hba.conf для пользователя репликации. Patroni использует выделенного пользователя для стриминга WAL и REST API. Создайте его на том узле, который станет первым primary (после запуска Patroni реплицирует пользователя автоматически):

sudo -u postgres createuser -s replicator
sudo -u postgres psql -c "ALTER USER replicator WITH PASSWORD 'secure_password';"

Настройка DCS: выбираем etcd, Consul или ZooKeeper

etcd - оптимальный выбор для кластера Patroni. Он проще в установке, чем ZooKeeper, и потребляет меньше ресурсов, чем Consul с его сервис-мешем. Для production разверните кластер etcd из трех узлов - это обеспечит кворум при отказе одного сервера.

Установка etcd на каждом узле:

sudo apt install -y etcd

Конфигурация /etc/default/etcd на первом узле (IP 10.0.0.1):

ETCD_NAME="node1"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://10.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.1:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.1:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.1:2379"
ETCD_INITIAL_CLUSTER="node1=http://10.0.0.1:2380,node2=http://10.0.0.2:2380,node3=http://10.0.0.3:2380"
ETCD_INITIAL_CLUSTER_TOKEN="patroni-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"

На втором и третьем узлах измените ETCD_NAME и ETCD_LISTEN_PEER_URLS/CLIENT_URLS на соответствующие IP. Запустите etcd на всех узлах:

sudo systemctl enable etcd
sudo systemctl start etcd

Проверьте состояние кластера:

etcdctl member list
etcdctl cluster-health

Настройка Patroni для автоматического failover

Patroni устанавливается через pip в виртуальное окружение или системно. Рекомендуется системная установка - меньше зависимостей и проще обновление:

sudo apt install -y python3-pip python3-dev libpq-dev
sudo pip3 install patroni[etcd] psycopg2-binary

Создайте конфигурационный файл /etc/patroni/patroni.yml. Этот файл идентичен на всех узлах за исключением параметра name - он должен содержать имя текущего узла.

Конфигурация Patroni: основные параметры patroni.yml

scope: postgres-cluster
name: node1  # меняется на node2, node3 на соответствующих узлах

restapi:
  listen: 0.0.0.0:8008
  connect_address: 10.0.0.1:8008

etcd:
  hosts: 10.0.0.1:2379,10.0.0.2:2379,10.0.0.3:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 200
        shared_buffers: 256MB
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 10
        max_replication_slots: 10
        wal_keep_size: 512MB

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 10.0.0.0/24 md5
    - host all all 10.0.0.0/24 md5

postgresql:
  listen: 0.0.0.0:5432
  connect_address: 10.0.0.1:5432
  data_dir: /var/lib/postgresql/16/main
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /tmp/pgpass
  authentication:
    replication:
      username: replicator
      password: secure_password
    superuser:
      username: postgres
      password: postgres_password
  parameters:
    unix_socket_directories: '/var/run/postgresql'

Ключевые параметры: scope - имя кластера, должно совпадать на всех узлах. ttl - интервал в секундах, через который Patroni обновляет блокировку лидера в DCS. Если лидер не обновил блокировку за ttl, запускаются выборы. Значение 30 секунд - баланс между быстрым обнаружением сбоя и устойчивостью к кратковременным проблемам сети. loop_wait - интервал опроса состояния Patroni. maximum_lag_on_failover - максимальное отставание реплики в байтах, при котором она может стать primary. use_pg_rewind - автоматическое восстановление бывшего primary после сбоя без полного пересоздания реплики.

Запуск и проверка кластера Patroni

Создайте systemd-юнит для Patroni:

sudo tee /etc/systemd/system/patroni.service <<EOF
[Unit]
Description=Patroni PostgreSQL Manager
After=network.target etcd.service

[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
ExecReload=/bin/kill -HUP \$MAINPID
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable patroni
sudo systemctl start patroni

Запустите Patroni на всех узлах. Первый запущенный узел станет primary, остальные - репликами. Проверьте состояние кластера:

patronictl -c /etc/patroni/patroni.yml list

Вывод покажет таблицу с узлами, их ролями, состоянием и отставанием репликации. Выполните плановое переключение для проверки:

patronictl -c /etc/patroni/patroni.yml switchover

Patroni предложит выбрать кандидата и выполнит переключение с минимальным временем простоя. Для моделирования аварийного отказа остановите primary:

sudo systemctl stop patroni

В течение ttl (30 секунд) одна из реплик будет повышена до primary. Команда patronictl list покажет нового лидера. Запустите остановленный узел - Patroni автоматически вернет его в кластер как реплику.

Настройка синхронной и асинхронной репликации

Выбор режима репликации определяет компромисс между надежностью и производительностью. Синхронная репликация гарантирует, что транзакция зафиксирована на primary и хотя бы одной реплике перед подтверждением клиенту. Асинхронная не ждет подтверждения - primary сразу отвечает клиенту, а реплика получает изменения с задержкой в миллисекунды.

Конфигурация синхронной репликации для нулевой потери данных

Добавьте в patroni.yml в секцию bootstrap.dcs.postgresql.parameters:

synchronous_mode: true
synchronous_standby_names: "*"

Параметр synchronous_standby_names: "*" означает, что primary будет ждать подтверждения от любой доступной синхронной реплики. Для более точного контроля укажите имена конкретных реплик: synchronous_standby_names: "2 (node2,node3)" - транзакция подтверждается после записи на любые две реплики из списка.

Примените изменения:

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

Patroni откроет редактор с текущей конфигурацией. Внесите изменения, сохраните и закройте - конфигурация применится на всех узлах без перезапуска. Проверьте режим репликации:

sudo -u postgres psql -c "SELECT application_name, sync_state FROM pg_stat_replication;"

Синхронная реплика увеличивает задержку на запись - каждая транзакция ждет ответа от реплики по сети. При падении синхронной реплики primary приостановит обработку транзакций до восстановления реплики или перевода её в асинхронный режим. Настройте synchronous_commit в remote_apply для максимальной надежности - реплика не только получит WAL, но и применит изменения перед подтверждением.

Асинхронная репликация: когда допустима задержка

Асинхронная репликация - режим по умолчанию в Patroni. Primary не ждет подтверждения от реплик, что минимизирует задержки. Настройка сводится к удалению или комментированию synchronous_mode и synchronous_standby_names из конфигурации. Риск: при внезапном отказе primary (питание, kernel panic) последние транзакции, не успевшие уйти на реплику, будут потеряны. Объем потери - от нескольких миллисекунд до нескольких секунд данных.

Для проектов с высоким темпом записи и терпимостью к потере нескольких транзакций асинхронный режим предпочтителен. Он также упрощает географически распределенные кластеры: реплика в другом дата-центре с задержкой 50 мс в синхронном режиме сделает запись неприемлемо медленной, а в асинхронном - работает без накладных расходов.

Интеграция PgBouncer для балансировки нагрузки и пула соединений

PgBouncer устанавливается на отдельный сервер или на узлы приложений. В production рекомендуется выделенный хост перед кластером PostgreSQL - это упрощает управление и исключает конкуренцию за ресурсы с базой данных.

Установка и базовая конфигурация PgBouncer

sudo apt install -y pgbouncer

Основной конфигурационный файл /etc/pgbouncer/pgbouncer.ini:

[databases]
* = host=10.0.0.1 port=5432

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 25
max_client_conn = 500
max_db_connections = 50
reserve_pool_size = 10
reserve_pool_timeout = 5
log_connections = 0
log_disconnections = 0
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats

Параметр pool_mode = transaction возвращает соединение в пул после завершения транзакции. Это безопасно для большинства приложений и значительно эффективнее session-режима. default_pool_size - количество соединений к PostgreSQL на каждый пул. При 25 соединениях и 20 пулах PgBouncer будет держать до 500 коннектов к базе, обслуживая тысячи клиентов.

Создайте файл аутентификации userlist.txt. Пароли можно хранить открытым текстом или в MD5-хеше (результат SELECT passwd FROM pg_shadow):

sudo -u postgres psql -c "SELECT '\"' || usename || '\" \"' || passwd || '\"' FROM pg_shadow;" | sudo tee /etc/pgbouncer/userlist.txt

Настройка балансировки нагрузки между primary и репликами

Для разделения записи и чтения создайте два пула в PgBouncer. Первый указывает на primary, второй - на реплики. Patroni предоставляет REST API для определения текущего лидера. Настройте скрипт, обновляющий конфигурацию PgBouncer при смене primary:

[databases]
app_write = host=10.0.0.1 port=5432 dbname=mydb
app_read = host=10.0.0.2 port=5432 dbname=mydb
app_read_replica = host=10.0.0.3 port=5432 dbname=mydb

Приложение подключается к PgBouncer на порт 6432: для записи использует базу app_write, для чтения - app_read или app_read_replica. Более продвинутый вариант - использовать единый эндпоинт с автоматической маршрутизацией через pgBouncer-rr или HAProxy с проверкой read-only статуса через Patroni API.

Для автоматического обновления IP primary при failover добавьте cron-задачу, опрашивающую Patroni REST API:

#!/bin/bash
LEADER=$(curl -s http://10.0.0.1:8008/leader | jq -r '.address')
sed -i "s/host=.*port=5432 dbname=mydb_write/host=$LEADER port=5432 dbname=mydb_write/" /etc/pgbouncer/pgbouncer.ini
sudo systemctl reload pgbouncer

Тестирование отказоустойчивости и мониторинг

Тестирование - обязательный этап перед вводом в production. Проверьте реакцию кластера на три типа сбоев: остановка primary, изоляция сети и высокая нагрузка.

Моделирование отказов и проверка failover

Тест 1 - остановка primary:

sudo systemctl stop patroni

Ожидаемое поведение: в течение ttl (30 секунд) Patroni на репликах обнаруживает пропажу лидера, DCS запускает выборы, одна из реплик повышается до primary. Проверьте через patronictl list и выполните тестовую запись в базу. Остановленный узел после запуска возвращается как реплика.

Тест 2 - сбой сети: заблокируйте трафик на primary через iptables:

sudo iptables -A INPUT -j DROP
sudo iptables -A OUTPUT -j DROP

Кластер должен обнаружить недоступность лидера и выполнить failover. После восстановления сети узел возвращается как реплика. Если использовался pg_rewind, бывший primary синхронизирует расхождения без полного копирования данных.

Тест 3 - высокая нагрузка: используйте pgbench для имитации нагрузки во время failover:

pgbench -i -s 100 mydb
pgbench -c 50 -j 10 -T 300 mydb

Во время теста выполните switchover. Транзакции должны завершаться корректно, часть из них может быть отменена и повторена приложением.

Настройка мониторинга кластера Patroni

Patroni предоставляет REST API с метриками состояния. Эндпоинт /metrics отдает данные в формате Prometheus. Добавьте в prometheus.yml:

- job_name: patroni
  static_configs:
    - targets:
      - 10.0.0.1:8008
      - 10.0.0.2:8008
      - 10.0.0.3:8008

Ключевые метрики для мониторинга: patroni_postgres_running (1 - экземпляр запущен), patroni_postgres_role (0 - реплика, 1 - primary), patroni_replication_lag (отставание в байтах), patroni_postgres_in_archive_recovery (процесс восстановления). Настройте алерты в Grafana: отставание репликации более 100 МБ, смена primary чаще одного раза в час, недоступность любого узла более 60 секунд.

Типичные проблемы и их решение

Проблемы с DCS и восстановление кворума

Симптом: patronictl list показывает узлы в состоянии «unknown» или «stopped», REST API недоступен. Причина - потеря кворума etcd. Кластер etcd из трех узлов требует минимум два работающих для принятия решений. При отказе двух узлов etcd переходит в режим read-only.

Решение: восстановите упавшие узлы etcd. Если данные потеряны безвозвратно, пересоздайте кластер etcd с флагом --initial-cluster-state new на всех узлах. Patroni восстановит состояние кластера PostgreSQL из DCS. Для предотвращения настройте автоматические бэкапы etcd через etcdctl snapshot save и мониторинг через Prometheus etcd exporter.

Ошибки репликации и отставание реплик

Симптом: pg_stat_replication показывает растущий write_lag или flush_lag. Реплика отстает на гигабайты. Причины: высокая нагрузка на запись на primary, медленная дисковая подсистема реплики, конфликты восстановления (запросы на реплике блокируют применение WAL).

Диагностика:

SELECT application_name, state, sync_state, 
       pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag_bytes,
       pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag_bytes
FROM pg_stat_replication;

Решение для конфликтов восстановления: установите max_standby_streaming_delay = 30s и hot_standby_feedback = on в patroni.yml. Первый параметр задает максимальную задержку применения WAL при конфликте, второй - сообщает primary о запросах на реплике, чтобы primary не удалял нужные им строки. При хроническом отставании увеличьте max_wal_senders и wal_keep_size на primary, проверьте IOPS дисковой подсистемы реплики.

Если вы проектируете отказоустойчивую инфраструктуру с нуля, обратите внимание на связь проектирования базы данных и администрирования - многие проблемы репликации закладываются на этапе схемы данных. Для углубленного понимания механизмов восстановления после сбоев изучите руководство по аварийному переключению в облачных и гибридных средах, где разобраны метрики RPO, RTO и стратегии Active-Passive/Active-Active. Если в вашей инфраструктуре также используется MongoDB, ознакомьтесь с настройкой репликасета MongoDB - конфигурация приоритетов узлов и slaveDelay решают схожие задачи отказоустойчивости.

Для развертывания кластера требуются серверы с достаточными ресурсами. Timeweb Cloud предоставляет VDS с гарантированными IOPS и возможностью размещения узлов в разных дата-центрах - это упрощает создание географически распределенного кластера Patroni без капитальных затрат на железо.

Поделиться:
Сохранить гайд? В закладки браузера