В исходниках PostgreSQL встречаются ироничные комментарии, а самый весёлый, на мой взгляд, находится в строке прямо перед запуском сервера. Судя по логам Git, это комментарий Тома Лейна, который сообщает: "We are ready to rock and roll", — а следующей строкой идёт запуск сервера СУБД.
Действительно, когда пытаешься запустить кластер PostgreSQL, порой не покидает ощущение "rock and roll", а потом вдруг кластер не стартует или внезапно переключается на другой узел из-за отступов в YAML-конфиге :)

В статье разберём ключевые элементы отказоустойчивого кластера и типовые места, где чаще всего допускают ошибки.
Почему слоника надо холить и лелеять
Всем известно, что база данных — это сердце любой информационной системы: не работает СУБД — не работает вообще ничего. Если вам кажется, что СУБД ничего не делает, вам только кажется. При поломке Postgres система, конечно, может какое-то время продержаться без него, но недолго, и это будет не самое приятное время для пользователей БД.
Поэтому нашего слоника надо холить и лелеять — выстроить вокруг него инфраструктуру, которая позволит иметь несколько баз данных, настроить репликацию между ними, своевременно переключаться между узлами при авариях.
За годы моей работы с PostgreSQL СУБД обросла разными элементами: репликами, хранилищами секретов, пулами соединений, серверами резервного копирования, прокси — всем, что требуется для нормальной работы кластера.

А штука в том, что все эти элементы должны дублировать друг друга. Сложность системы возрастает, администрирование усложняется, и вот уже DBA должен обладать знаниями системного администратора или DevOps-инженера.
Все эти элементы необязательно нужно строить с нуля, но знать про них нужно.
Хранилище конфигурации: etcd или Consul?
Для построения отказоустойчивого кластера на ванильном PostgreSQL прежде всего нужно хранилище конфигурации. В распределённых системах оно, как правило, должно быть распределённым. Обычно используют два хорошо известных решения: etcd или Consul.
Etcd выглядит проще. Его можно освоить за пару-тройку дней: документация вменяемая, функций немного, а из «плюшек» модель доступа. Если хотите защитить кластер от потери конфигурации, есть полный RBAC. Правда, нет полноценного multi-DC: если у вас разнесённый геокластер, придётся исхитриться, чтобы получить общее хранилище конфигурации на два дата-центра.
Consul посложнее: в нём целые политики и токены, зато это полноценный Service Discovery, а ещё его можно использовать в качестве DNS. Контроль доступа строится не на ролях, а на ключах и ветвях ключей на основе списков доступа. Поначалу «въехать» в это достаточно трудно, но освоить вполне по силам.
Оба этих решения, если развернуть их в кластерной конфигурации и настроить правильно, могут поддерживать множество кластеров PostgreSQL. Если в вашей инфраструктуре несколько десятков отказоустойчивых кластеров, то можно использовать один кластер еtcd или Consul как хранилище конфигурации для всех остальных. Звучит рискованно? Так и есть. Но для этого существуют отказоустойчивые конфигурации самих DCS-кластеров.
Важный момент: их лучше размещать отдельно от PostgreSQL. Все кластеры типа «ключ–значение» достаточно требовательны к I/O, и если вы разместите еtcd на том же узле, что и СУБД с высокой нагрузкой, то при просадке ввода-вывода мастер будет пропускать ответы на хартбиты (heartbeats). А это чревато внезапными перевыборами лидера, что никому не нужно.
Почти вечный вопрос: Patroni или Stolon?
С вопросом из подзаголовка в нашу техподдержку обращаются чаще всего.
В своё время я дорабатывал Stolon на Go для нашего решения по шардированию. Stolon удобный, дистрибутив всего из трёх бинарных файлов, в отличие от Patroni, который написан на Python. Разбросал файлы по узлам, добавил нехитрые конфиги — и погнали. Но я познакомился со всеми ограничениями Stolon изнутри, из Go-кода.
Могу сказать точно: Stolon поддерживает только ванильный PostgreSQL и ничего больше. Если вдруг вы используете коммерческий форк или сами дорабатывали Postgres, то при появлении новых параметров конфигурации Stolon «упадёт», потому что ему задали непонятные параметры.
У Stolon три компонента:
Sentinel (надмозг кластера, у него тоже должен быть лидер);
Keeper (следит за PostgreSQL);
Proxy (отправляет трафик в лидера).
В целом решение надёжное за исключением нескольких параметров, которые нельзя изменить, потому что они контролируются внутри его кода.
Patroni — это набор Python-скриптов, объединённых одним замыслом. Patroni поддерживает коммерческие форки, а не так давно ему добавили поддержку CitusDB. В последних версиях Patroni использует кворумную репликацию, переходя от модели «синхронная – асинхронная реплика» к модели «до кого коммит доехал быстрее, тот и молодец». Это порождает некоторые проблемы, но иногда избавляет от ожидания подтверждения коммита.
В Patroni есть система колбэков на все случаи жизни, удобное управление, теги и приоритеты узлов. Единственное неудобство — сам Python: далеко не все готовы держать его на сервере базы данных. У нас был случай, когда партнёры отдали код Patroni на проверку в службу безопасности, которая обнаружила в коде десять “critical high issues". На моё возражение: «Ну это же не веб-приложение, как можно применить SQL-инъекции, что у вас тут перечислены?» они ответили: «Это не предмет дискуссии, дайте другое решение». То есть Patroni применим далеко не везде из-за требований безопасности.
Но почему же в конечном итоге я выбираю Patroni? У него огромное распространение в продакшене, живое сообщество, сам продукт постоянно развивается. Даже без коммерческой поддержки можно написать issue в репозитории GitHub, и, скорее всего, вам ответит сам создатель Patroni Александр Кукушкин.
Как «промахнуться» с кластером
В переводе с греческого слово «грех» означает «промахнуться» или «попасть мимо», «не достичь нужного». В кластерных делах грешить получается удивительно часто.
В кластер попадают разными способами через TCP-прокси:
с помощью скрипта, который обходит API Patroni и выясняет, кто лидер;
с помощью HTTP-проверки, встроенной в сам TCP-прокси;
с помощью PSQL-проверки, если нет Patroni;
написать свой TCP-чек, смотреть в пакеты, выбирать сигнатуры и балансировать трафик, но обычно хватает встроенной HTTP-проверки.
Представьте ситуацию: сервис работал, пока на уровне кластера не произошёл failover. Приложение стало работать медленно, но только на тех запросах, которые возвращают много данных. Пользователи нажимают «Сохранить», ждут неимоверно долго и вызывают DBA. DBA открывает консоль, подключается с локального хоста к СУБД, запускает EXPLAIN ANALYZE, получает 10 миллисекунд — всё нормально. Затем смотрит pg_stat_statements: время запроса с 10 миллисекунд увеличилось после фейловера до 4 000 мс.

В чём причина? После failover приложение могло переключиться на HAProxy в соседний дата-центр. Кластер растянут на два дата-центра (это, конечно, антипаттерн, но знали бы вы, сколько таких сетапов в энтерпрайзе; ресурсы не резиновые, приходится экономить), и теперь трафик идёт окольным маршрутом. Приложение должно было попасть в лидера через HAProxy в первом дата-центре, а оно пошло через соседний.
По pg_stat_statements мы этого не поймём, он по-прежнему возвращает 4 000 миллисекунд на этот запрос. Повторим: на уровне СУБД ничего не изменилось, но время выполнения запроса выросло на порядок, что и отразилось в статистике.
А как работает расширение? Для каждого запроса выбирается план, рассчитывается время выполнения, узел отдаёт данные клиенту, и только после отдачи всех строк информация заносится в статистику. Время на передачу данных между дата-центрами из статистики, конечно, никак не вычислить. При этом на маленьких запросах картина не меняется, а вот на больших, где относительно много данных извлекается из таблиц и передаётся клиенту, получается такой эффект, потому что мы гоняем трафик между дата-центрами дважды.

Справиться с таким поведением можно с помощью DNS (изменить адрес прокси на правильный, не забыв про TTL) или специальных скриптов, которые «простукивают» endpoints-кластеры. Идея простая: если приложение «живёт» в ДЦ-1, оно не должно «ходить» на HAProxy в ДЦ-2. Ещё можно поставить прокси прямо рядом с приложением, но не все к этому готовы.
Пулеры соединений: PgBouncer vs Odyssey
PostgreSQL не любит много соединений, поэтому нужен пулер. Из тех, что всегда на слуху, — PgBouncer и Odyssey.
PgBouncer, однопоточная программа с машиной состояний внутри, долгое время страдал от проблем с производительностью. Odyssey же сделан иначе: это изначально многопоточная модель, где каждое соединение использует Goкорутину. Если загрузить все ядра, соединения будут чувствовать себя комфортно.
Но Odyssey, к сожалению, не очень стабилен. Последний релиз на момент написания статьи был в июне 2022 года. Мы измеряли производительность всех пулеров (потому что писали свой) и обнаружили проблемы (это предмет отдельного разговора), в итоге поняли, что Odyssey довольно капризный в эксплуатации.
А вот PgBouncer развивается линейно и регулярно. С 20-й версии добавили SO_REUSEPORT, возможность переиспользования порта несколькими процессами. В конфигурации это параметр peers, который позволяет распределить нагрузку на несколько ядер. А ещё на PgBouncer можно организовать TLS-offload, завершить все TLS-соединения и разгрузить PostgreSQL.
Наши коллеги из Ozon в своё время испытывали проблемы с PgBouncer, в итоге написали свой пулер соединений pg_doorman. Советуем обратить на него внимание: в pg_doorman многое переписано на Rust, что-то ускорено, исправлена куча «родовых травм» PgBouncer.

Но и с PgBouncer можно «промахнуться». При использовании пулеров соединений есть тонкости с отменой запросов в каскадированной схеме с несколькими экземплярами PgBouncer. Если запрос ушёл на один экземпляр, а cancel через балансировщик попал на другой, запрос так и будет висеть, пока не истечёт какой-нибудь таймаут.
Конфигурация с peers решает эту проблему: ядро распределяет входящие соединения между слушающими сокетами, при получении cancel проверяет идентификатор сессии и отправляет «отмену» правильному бэкенду.
Особенно хороша конфигурация, когда пулер общается с PostgreSQL через Unix-сокет, — можно сэкономить на обходе TCP-стека. Быстро и надёжно.
Vault, бэкапы и мониторинг
У безопасников всегда была претензия: «Вы храните на узле файлики .pgpass с паролем». С Vault можно построить систему, где не нужно перезапускать сервисы при ротации сертификатов. Vault-агент в нужный момент сходит в Vault-сервер, заберёт сертификаты, разложит по папкам, а сервисы (pgbouncer, HAProxy, Patroni) сами среагируют и перезагрузятся.
Более того, можно создать правила, по которым пользователь с токеном получит временный пароль и временного пользователя по определённому правилу. Красота для банков и других организаций с жёсткими требованиями к безопасности.

По бэкапам самое лучшее и устойчивое из всего, что есть, на мой взгляд, pgBackRest. Умеет всё, включая S3. Бэкап-агент запускает резервное копирование по крону на мастере или реплике и в мультипоточном режиме складывает всё в объектное хранилище. Туда же стримятся WAL-файлы.

Мониторинг — отдельная большая тема. Сообщество сейчас говорит так: «Экспортёр для оперативного мониторинга в PostgreSQL должен забирать только минимальный набор метрик, которые можно быстро получить». Для кастомных метрик есть SQL-экспортёры, которые независимы от БД и позволяют писать запросы к любым базам данных и делать любые кастомные метрики.
Что нужно знать, чтобы «жить» с кластером
Кажется, всё просто: накатим софт на железо — получим кластер. Но чтобы PostgreSQL в отказоустойчивой конфигурации нормально работал, нужно окружить его целым зоопарком систем.
Ещё одна важная часть — цена вопроса. Для управления всем этим нужны DevOps-инженеры и DBA. В моей практике бывало, что классную работу DevOps приходилось переделывать.
В идеале нужны специалисты «два в одном», но таких людей мало. На каждую позицию в кластерную команду консалтинга мы набирали людей по полгода: искали тех, кто и в том, и в другом что-то понимает.

И несколько советов напоследок:
выбирайте самые популярные решения для реализации;
не руководствуйтесь принципом «вчера на GitHub увидел» — потом не найдёте специалистов, кто будет всё поддерживать. Область СУБД довольно консервативна. Самый задумчивый среди всех ИТ-профи, как известно, DBA. Обычно это довольно колоритный персонаж, про которого больше всего анекдотов;
старайтесь избегать самописных решений. Кто будет поддерживать тонну бинарников на Go, когда их талантливый разработчик уволится?
если всё-таки надо написать что-то специфическое под вашу инфраструктуру, пишите shell-скрипт. Он хотя бы поддерживаемый, в нём проще разобраться. Но лучше всего выйти из положения без написания кода.
Статья подготовлена по материалам выступления Павла Конотопова на технической конференции – PGMeetup.PERM 2025. Павел – эксперт по построению отказоустойчивых систем на базе PostgreSQL, лид кластерной команды с пятилетним опытом в консалтинге и множеством проектов по техподдержке высоконагруженных систем.
