Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Failover PostgreSQL: Patroni и автопереключение

Failover Postgres без потерь: Patroni, репликация и честные компромиссы

Как устроить автопереключение PostgreSQL: Patroni и DCS, синхронная и асинхронная репликация, RPO/RTO, promote и план восстановления после фейловера.

Прямой ответ

Failover БД — переключение на резерв при смерти лидера — и у Postgres он «из коробки» только ручной (pg_ctl promote на реплике — репликация — «ночью умер мастер — дежурный промоутит» — RTO = время подъёма дежурного — часы); автоматизация = Patroni (демон на каждом узле Postgres: следит за здоровьем, выбирает лидера через DCS (etcd/Consul/ZooKeeper — распределённый консенсус — «кто лидер» — единственная правда — лидер держит lock-ключ в DCS — умер — ключ протух — кандидаты выбирают нового — консенсус-механика не нужна — Patroni сам); что даёт: автоматический promote (секунды-минуты против часов — RTO), защита от split-brain (два лидера — смерть данных — DCS-lock гарантирует одного — «самодельный failover по cron-пингу» — классический split-brain-генератор — пинг врёт при сетевом разделе — Patroni+DCS — единственный честный путь), управление конфигурацией кластера (patronictl edit-config — postgresql.conf кластеру централизованно — конфигурация — «поправить параметр на трёх узлах» — одна команда), плановые свитчоверы (patronictl switchover — обслуживание без простоя — rolling-культура); главный честный компромисс — RPO против доступности (сколько транзакций готовы потерять): асинхронная репликация (дефолт — лидер коммитит, не дожидаясь реплик — умер лидер — последние транзакции (миллисекунды-секунды WAL) могут не доехать — RPO > 0 — потеря данных возможна — зато реплика не тормозит запись); синхронная (synchronous_standby_names='ANY 1 (replica1, replica2)' — коммит ждёт подтверждения синхронной реплики — RPO = 0 — цена: запись медленнее (round trip до реплики — закон Литтла — +1–5 мс на коммит — заметно на write-heavy), смерть синхронной реплики блокирует запись лидера (ждать некого — «реплика легла — прод встал» — деградация доступности ради консистентности — synchronous_commit=remote_apply/off — тюнинг-градации); ANY 1 из двух+ — смерть одной не блокирует — минимум 3 узла (лидер+2) для «RPO=0 и доступность» — топология не случайно такая); выбор: финансовые записи — синхронно (потеря транзакции дороже латентности), контент/аналитика — асинхронно (секунда данных переживётся — SLO-контекст — RPO в договор с бизнесом — «сколько данных можем потерять» — вопрос не технический); RTO-реальность Patroni: детект (TTL ключа ~30 с) + выборы + promote (replay оставшегося WAL — секунды-минуты по лагу) + приложение переподключилось (DNS/vip/прокси — ещё секунды) — реалистично 1–3 минуты — «failover за 5 секунд» — маркетинг против физики WAL-лага; после фейловера (часть, которую забывают): старый лидер вернулся — не промоутить руками (Patroni переприсоединит его репликой автоматически — pg_rewind — разошедшийся WAL откатывается — «руками промоутили вернувшийся узел — split-brain» — главная авария самодельных схем); проверка реплик/лага (метрики — pg_stat_replication — алерт на lag — Alertmanager); инцидент-разбор (runbook — почему умер лидер — фейловер лечит симптом).

Практика: топология, DCS, проверка боем

Топология минимума: 3 узла Postgres (лидер + 2 реплики — одна синхронная ANY 1, вторая асинхронная — реплики-механика) + 3 узла etcd (нечётное — кворум 2 — etcd на тех же машинах допустим для старта, но не сеть-разделённая с Postgres — «etcd лёг вместе с лидером» — DCS-кворум переживёт одну машину); приложение: подключение через vip/HAProxy/pgbouncer-слой (не напрямую к лидеру — после фейловера — «приложение знало IP лидера» — пулинг — DNS TTL 30 с или прокси с health-check роли — patronictl list покажет кто есть кто); проверка боем обязательна (фейловер, который не репетировали, не работает): учения ежеквартально — patronictl switchover (плановый — приложение не заметило? — graceful — connections/пул переживают — проверяем), systemctl kill -9 postgresql на лидере (аварийный — RTO по секундомеру — лог-таймлайн: детект/выборы/promote/переподключение — цифры в SLO-отчёт), сетевой раздел (iptables-дроп между узлами — поведение при кворум-потере — лидер без DCS уходит в read-only (Patroni demote — «лучше недоступен, чем split-brain» — осознанное поведение — бизнес знает — chaos-практика); лаг-сценарий (реплика с лагом 10 ГБ стала лидером — RPO-потеря замерена — «асинхронка теряет X при аварии» — цифра, не догадка); бэкап-страховка (фейловер ≠ бэкап — PITR — «удалили таблицу» — реплики скопируют удаление — восстановление только из архива WAL/бэкапа — стратегия 3-2-1 — две разные защиты от двух разных бед — частейшее смешение); мониторинг Patroni (patroni /health эндпоинты + метрики — роль/лаг/DCS-здоровье — Grafana-дашборд — «кластер в порядке» — не по «postgres отвечает», а по patronictl-статусу); миграция существующего прода под Patroni — отдельная операция (логическая репликация на новый кластер + свитч — миграция-паттерн — «обернуть работающий прод» — только с окном/репликой).

Практический сценарий: ночь, когда лидер умер по-настоящему

Контекст: SaaS, Patroni-кластер (3 узла, синхронная ANY 1) — 8 месяцев после настройки, учения проведены дважды (последнее — за 6 недель); 03:47 — лидер-узел: смерть NVMe-контроллера (железо — не софт — «тестировали софтверные сбои» — железо всегда оригинальнее); что произошло автоматически (таймлайн из логов Patroni/метрики): 03:47:12 — лидер теряет DCS-ключ (TTL истёк — детект 28 с), 03:47:41 — выборы — синхронная реплика (лаг 0 — RPO=0 отработал) получает лидерство, 03:47:58 — promote завершён (26 с — WAL-хвост мал — синхронность дала нулевой хвост), 03:48:05 — HAProxy health-check перенаправил трафик (read-only-режим приложения на 8 секунд — write-запросы в очередь Celery — переждали — пользователи: «залип заказ» — идемпотентность повторов не допустила); RTO факт: 53 секунды (детект+выборы+promote — переподключение ещё 7 с) — SLO «фейловер < 3 мин» — выполнен с запасом; RPO факт: 0 транзакций (сверка — последние коммиты присутствуют на новом лидере — транзакции — синхронная репликация оправдала свои +2 мс на коммит); утро — разборы: (1) третий узел (асинхронный) — лаг на момент аварии 1.2 с (если бы он стал лидером — потеря ~40 транзакций — «асинхронка — план C с известной ценой» — цифра из учений совпала); (2) вернувшийся узел (диск заменили через день) — Patroni+pg_rewind присоединил репликой автоматически (руками не трогали — runbook-пункт «никогда не промоутить вернувшийся узел» — сработал); (3) находка учений-на-коленке: алерт на «DCS-кворум деградировал» молчал (etcd-метрику собирали, алерт не настроили — алертинг-практика — починили — второй узел etcd умер бы в ту же ночь незамеченным); (4) постмортем-вывод: фейловер отработал, потому что (а) репетировали, (б) синхронность выбрали осознанно (write-нагрузка позволяла +2 мс — бенчмарк мерили до решения), (в) приложение переживает короткий read-only (graceful-поведение — очередь вместо 500); чего НЕ было: потери данных, ручных промоутов, split-brain — скучная ночь — лучшая оценка фейловер-архитектуры (SRE-культура — «инцидент без приключений» — цель всего этого).

FAQ

Patroni против repmgr/pgpool/самописных скриптов — почему именно он? repmgr — проще (меньше компонент — без DCS), но фейловер-решения локальные (split-brain-риски выше — «наблюдатель» против консенсуса); pgpool-II — прокси-слой (свои фейловер-механики — «прокси решает, кто лидер» — ещё один слой с отказом); самопис (cron+пинг+promote-скрипт) — split-brain-генератор (сетевой раздел — два «лидера» — консенсус через DCS — единственная честная защита — писать свой Raft — нет); Patroni: DCS-консенсус (etcd — проверенный), автоматический rejoin (pg_rewind), switchover/failover через patronictl, конфигурация кластера — стандарт де-факто (Zalando-происхождение — прод-масштабы); цена: +etcd-кластер (операционная сложность — 3 узла — деплой-культура); альтернатива-контекст: managed-БД (RDS/Cloud SQL — фейловер — забота провайдера — «не хотим эксплуатировать» — цена/привязка — выбор-контекст не про это, но аналогия — buy против build); критерий: self-hosted Postgres с требованиями RTO/RPO — Patroni — индустриальный дефолт 2026.

Сколько занимает реальный failover и от чего зависит? Слагаемые: детект (TTL DCS-ключа — 30 с дефолт — тюнинг loop_wait/ttl — «быстрее» — ложные срабатывания на сетевых сбоях — баланс), выборы (секунды — DCS-транзакция), promote (replay недовоспроизведённого WAL — зависит от лага реплики — синхронная — секунды, асинхронная с лагом 10 ГБ — минуты), переподключение приложения (DNS TTL/прокси health-interval — секунды-десятки); итог-реальность: синхронная топология — 30–90 с, асинхронная с лагом — минуты; как ускорять: синхронность/полусинхронность (лаг→0), TTL-тюнинг (осторожно), прокси-слой с быстрым health-check (не DNS TTL 300 — прокси-практика); как не надо: «фейловер за 5 с» ценой TTL 5 с — ложные фейловеры на каждом сетевом чихе — хуже планового простоя — SLO-взвешивание; измерение — учения с секундомером (сценарий выше — 53 с — своя цифра, не из статей).

Что такое split-brain и как Patroni его предотвращает? Split-brain — два узла считают себя лидером (сетевой раздел/глюк детекта — оба принимают запись — данные расходятся — слияние невозможно — худшая авария БД); предотвращение в Patroni: лидерство = владение lock-ключом в DCS с TTL (ключ один — владеет один — TTL истёк — владение потеряно — бывший лидер уходит в read-only сам (demote) — даже если «жив и здоров», но не видит DCS — «лучше недоступен для записи, чем два лидера» — осознанный дизайн); кворум DCS (etcd 3 узла — большинство 2 — разделённая меньшинственная сторона не выберет лидера — majority-сторона продолжит — консенсус-основы — Raft-механика); fencing-дополнения (виртуализация/STONITH — «выключить старого лидера железно» — в облаках — security-group-изоляция — Patroni умеет callbacks — полная защита для параноиков); правило эксплуатации: вернувшийся после раздела узел — только через Patroni (pg_rewind — PITR-понимание WAL-расхождения — «промоутить руками» — прямой путь к split-brain — runbook-запрет).

Нужен ли failover, если есть бэкапы и PITR? Разные беды — разные защиты: failover лечит смерть железа/узла (RTO минуты — данные целы — реплика); PITR лечит порчу/ошибки данных (удаление таблицы/кривая миграция/шифровальщик — реплика скопирует ошибку мгновенно — восстановление из архива — часы — PITR); «у нас реплики = бэкап» — самая дорогая ошибка в эксплуатационных списках (DROP TABLE на лидере — на реплике через 50 мс — «восстанавливать нечего» — стратегия 3-2-1 — отдельная копия обязательна); обратное: «бэкапы есть — failover не нужен» — RTO из бэкапа — часы (restore 500 ГБ — ёмкость/время) — бизнес-простой против 53 секунд; полный набор: Patroni (доступность) + PITR-архивация WAL (откат данных) + регулярные restore-учения (бэкап-тесты — «бэкап, который не восстанавливали, — не бэкап») — три ноги, ни одной лишней.

Источники: документация Patroni, PostgreSQL (warm standby, WAL-конфигурация) — ссылки в шапке.