Этот пост основан на моем докладе на конференции PGConf.EU 2026, "The Primary Is Down. Are You Sure?".
Управляемый сервис Postgres должен предотвращать любые возможные подключения с записью в primary перед повышением нового standby. Это называется изоляцией (fencing) и имеет решающее значение для предотвращения потери данных при записи или ситуаций split-brain.
Чтобы понять это, нужно сначала разобраться, как происходит повышение и какие шаги необходимы, чтобы сделать standby новым primary. Имейте в виду, что старый primary может быть на самом деле не выключен. При сетевом разделении (partitioning), когда клиент может подключиться к primary, а мы — нет, клиент может продолжать запись в старый primary.
Вот эти шаги:
- Мы повышаем standby, выполняя SELECT pg_promote(true, 300). В этот момент standby перестает подтверждать фиксации транзакций (commits) от primary.
Мы повышаем standby, выполняя SELECT pg_promote(true, 300). В этот момент standby перестает подтверждать фиксации транзакций (commits) от primary.
- Мы обновляем DNS-записи, чтобы новые подключения направлялись на новый primary.
Мы обновляем DNS-записи, чтобы новые подключения направлялись на новый primary.
Ни один из этих шагов не останавливает старый primary от приема записей, что может привести к сценарию split-brain. С DNS все понятно: пока не истечет TTL, новые клиенты все еще могут обращаться к старому primary, а уже подключенные клиенты просто остаются там. А что насчет момента, когда мы прекращаем подтверждение фиксаций со стороны standby? Есть три сценария, в которых это не помогает:
- Если у нас один standby, он настраивается с асинхронной репликацией, поэтому primary его не ждет. Даже при наличии двух standby любая сессия может отключить ожидание для себя с помощью SET synchronous_commit = local или off.
Если у нас один standby, он настраивается с асинхронной репликацией, поэтому primary его не ждет. Даже при наличии двух standby любая сессия может отключить ожидание для себя с помощью SET synchronous_commit = local или off.
- Если у нас два standby, мы настраиваем их с кворумной фиксацией (ANY 1). После повышения одного из них, пока второй standby подтверждает фиксацию (ACK), старый primary продолжит фиксацию транзакций.
Если у нас два standby, мы настраиваем их с кворумной фиксацией (ANY 1). После повышения одного из них, пока второй standby подтверждает фиксацию (ACK), старый primary продолжит фиксацию транзакций.
- Даже если оба standby полностью отключены, клиент, все еще подключенный к старому primary, может отправить COMMIT. Обычно Postgres ждет подтверждения, но если клиент отменяет запрос (Ctrl+C в psql) или старый primary аварийно завершает работу и перезапускается, каждая сессия увидит фиксацию на старом primary, так как она уже записана в WAL (журнал упреждающей записи) старого primary. Это известное поведение синхронной репликации Postgres.
Даже если оба standby полностью отключены, клиент, все еще подключенный к старому primary, может отправить COMMIT. Обычно Postgres ждет подтверждения, но если клиент отменяет запрос (Ctrl+C в psql) или старый primary аварийно завершает работу и перезапускается, каждая сессия увидит фиксацию на старом primary, так как она уже записана в WAL (журнал упреждающей записи) старого primary. Это известное поведение синхронной репликации Postgres.
Поэтому мы стараемся помешать старому primary принимать любые записи до повышения standby.
Изоляция критична по времени, так как мы не можем повысить standby до ее завершения. Любое время, потраченное на изоляцию, напрямую увеличивает время простоя. Чтобы минимизировать его, мы запускаем несколько подходов к изоляции параллельно и переходим к failover сразу после успешного завершения первого из них.
Панель управления Ubicloud поддерживает управляемый PostgreSQL в трех средах: «железо» (bare metal), AWS и GCP. Каждая среда поддерживает общие методы изоляции, дополненные специфическими для каждой платформы подходами. Мы комбинируем эти методы, чтобы изолировать старый primary как можно быстрее.
ClickHouse Managed Postgres работает на базе панели управления Ubicloud с открытым исходным кодом, которую Ubicloud и ClickHouse разрабатывают совместно. Некоторые из нас в обеих командах создают управляемый Postgres вместе еще со времен Citus Data и Microsoft, и я один из них.
ClickHouse Managed Postgres работает на базе панели управления Ubicloud с открытым исходным кодом, которую и ClickHouse разрабатывают совместно. Некоторые из нас в обеих командах создают управляемый Postgres вместе еще со времен Citus Data и Microsoft, и я один из них.
Предположим, у панели управления есть SSH-доступ к базовой ВМ, но сам Postgres работает некорректно. У нас есть два удобных инструмента: pg_stop и hba.
pg_stop просто останавливает кластер с помощью pg_ctlcluster, но с ним возникают проблемы при перезагрузке. По умолчанию в start.conf из postgresql-common заложено автозапуск, и мы его сохраняем, так как в большинстве случаев нам нужно, чтобы экземпляр Postgres автоматически перезапускался при перезагрузке ВМ. Однако это означает, что если при остановке кластера ВМ по какой-либо причине перезагрузится, primary снова запустится.
Механизм hba, с другой стороны, записывает новый файл pg_hba.conf и перезагружает Postgres, поэтому он сохраняется и после перезагрузки. Тем не менее, правила применяются только к попыткам новых подключений. Документация Postgres documentation указывает для каждого типа записей, что запись «соответствует попыткам подключения». Таким образом, каждая уже открытая сессия не проходит проверку pg_hba повторно. К таким подключениям также относятся долгоживущие серверные подключения PgBouncer. Следовательно, для существующих подключений мы от этого ничего не получаем.
Более того, оба этих механизма зависят от доступности SSH-доступа, поэтому панель управления должна иметь возможность обращаться к базовой ВМ. Если сама ВМ недоступна, панели управления нужен механизм вне гостевой ОС. Именно здесь пути начинают расходиться в зависимости от провайдера виртуальных машин.
На «железе» панель управления также управляет хостом, на котором запущен Postgres ВМ. Таким образом, мы можем изолировать ее со стороны хоста. Каждая ВМ получает собственное сетевое пространство имен (namespace) на своем хосте, и они соединяются парой veth. Конец vethi<vm> находится внутри пространства имен, а конец vetho<vm> остается на хосте. Хост маршрутизирует публичные IPv4- и IPv6-адреса ВМ на vetho<vm>, а зашифрованная частная сеть, по которой идет репликация, также проходит через эту пару.
Таким образом, изоляция сводится к выполнению следующей команды на хосте:
В veth(4) man page указано: «Когда любое из устройств не работает, состояние связи пары также не работает». Клиенты, репликация, выгрузка WAL в объектное хранилище и собственный SSH-доступ панели управления в ВМ — все это проходит через данную пару, поэтому все они обрываются. Postgres продолжает работать внутри, но ничто за пределами пространства имен не может связаться с ним, а панель управления все еще может уничтожить ВМ через хост.
Для изоляции требуется ответ от хоста. Если хост недоступен, все три механизма терпят неудачу, поскольку pg_stop и hba также обращаются к гостевой ОС через этот хост.
В AWS у панели управления нет доступа к хосту, поэтому единственное, что она может сделать снаружи гостевой ОС, — это вызвать API EC2. Мы оценили три способа изоляции через этот API, но ни один из них не удовлетворил наши потребности.
- Команда stop-instances --force работает медленно, так как ожидает завершения работы в штатном режиме. Начиная с июля 2025 года, StopInstances имеет параметр skipOsShutdown, который пропускает завершение работы ОС. Мы не стали его использовать, потому что найденный нами метод работает значительно быстрее, а во время отработки отказа (failover) дорог буквально каждый момент.
- Начиная с июля 2025 года, StopInstances имеет параметр , который пропускает шаг завершения работы ОС. Мы не стали его использовать, потому что найденный нами метод работает значительно быстрее, а во время отработки отказа дорог буквально каждый момент.
- Группы безопасности (security groups) работают быстро, но имеют состояния, поэтому отслеживаемые соединения остаются активными.
- Сетевые списки контроля доступа (Network ACL) устроены сложнее и создают дополнительные ресурсы, которые впоследствии потребуется очищать.
В итоге мы пришли к решению отсоединять сетевой интерфейс (NIC). Это прерывает трафик как для существующих, так и для новых соединений. Проблема в том, что каждый экземпляр EC2 имеет основной сетевой интерфейс с индексом устройства 0, и отсоединить основной сетевой интерфейс во время работы экземпляра невозможно. Однако вторичные сетевые интерфейсы можно отсоединять «на лету», даже когда экземпляр запущен.
Поэтому мы спроектировали панель управления (control plane) так, чтобы каждый сервер Postgres на AWS получал второй сетевой интерфейс. Интерфейс управления находится под индексом устройства 0 и используется для SSH-доступа панели управления. Пользовательский интерфейс находится под индексом устройства 1. На нем размещаются публичные адреса, на него также указывает DNS-запись базы данных. Через него идут клиентские подключения и репликация. Это противоположно примеру сети управления из документации AWS, где основной интерфейс принимает публичный трафик.
Для изоляции (fencing) панель управления принудительно отсоединяет пользовательский NIC:
В справочнике по API указано: «Используйте параметр Force только в крайнем случае для отсоединения сетевого интерфейса от сбойного экземпляра». Мы не используем сбойные первичные узлы повторно. Ни один компонент панели управления не подключает пользовательский NIC обратно, и он остается отключенным до тех пор, пока старая виртуальная машина не будет уничтожена, поэтому в нашем случае использовать опцию принудительного отключения допустимо.
Вот пример того, как этот механизм изоляции работает в реальных условиях. Ниже приведены логи панели управления при отработке отказа на AWS:
Механизм pg_stop не обнаружил запущенного кластера, и hba не смог перезагрузиться, что ожидаемо после SIGKILL. Таким образом, отсоединение оказалось единственным сработавшим механизмом, и резервный узел перешел к повышению (promotion). Тринадцать секунд спустя панель управления сохранила лог ядра старого первичного узла через интерфейс управления, и вот его последняя строка:
В Google Cloud (GCP) панель управления также не имеет доступа к хосту, поэтому вне гостевой ОС она может вызывать только API Compute Engine. Мы оценили несколько способов, и ни один из очевидных вариантов Compute Engine не подходит для изоляции:
К счастью, 30 октября 2025 года динамические NIC стали общедоступными (generally available) в GCP. Динамический NIC представляет собой VLAN-субинтерфейс обычного NIC. В документации GCP указано: «Добавление или удаление динамических NIC не требует перезапуска или воссоздания экземпляра». Там же отмечается, что динамический NIC разделяет полосу пропускания и очереди своего родительского NIC.
В GCP nic0 является интерфейсом управления и находится в отдельной сети VPC (виртуальной частной облачной сети), которая пропускает SSH-трафик панели управления и блокирует весь исходящий трафик. Пользовательский NIC представляет собой динамический NIC в VLAN 2 в подсети базы данных; на нем размещаются публичные адреса, репликация и маршруты по умолчанию. Изоляция достигается путем удаления пользовательского NIC с помощью одного вызова deleteNetworkInterface.
Документация приводит стандартную задержку для добавления динамического NIC и ничего не говорит о том, когда именно прекращается трафик после удаления, поэтому мы измерили это самостоятельно. В ходе наших тестов последний пакет по любому пользовательскому потоку приходил через 0,7–2,5 секунды после запроса, а Google отмечал операцию как завершенную через 5,0–7,0 секунд.
Машина состояний старого первичного узла запускает по одной дочерней задаче на каждый механизм одновременно:
В этом коде bud запускает задачу, а hop переводит машину состояний на следующий шаг. Каждая дочерняя задача пробует свой механизм и сообщает lockout_succeeded, когда команда или вызов API завершаются. Ошибка SSH, сбой или тайм-аут команды, либо ошибка, возвращенная API EC2, приводят к сообщению lockout_failed. Когда дочерних задач не остается, старый первичный узел считается изолированным, независимо от того, что сообщили задачи. Резервный узел проверяет это раз в секунду, после чего выполняет повышение.
Таким образом, даже если все механизмы завершаются с ошибкой, панель управления все равно выполняет повышение. Мы сделали это намеренно: архитектура спроектирована по принципу отказа в открытом состоянии (fail-open), и соответствующий тест в репозитории фиксирует это поведение. Более строгий дизайн отправлял бы уведомление дежурному инженеру, если ни один метод изоляции не сработал, и база данных оставалась бы без первичного узла до вмешательства человека. Выбирая между этими крайностями, мы отдали предпочтение доступности.
Код отработки отказа одинаков для AWS, «голого железа» (bare metal) и GCP, а изоляция — это шаг, для которого в каждом окружении требуется свой механизм. На AWS каждый сервер Postgres, создаваемый панелью управления, имеет второй NIC, а изоляция представляет собой принудительное отсоединение пользовательского NIC. На «голом железе» изоляция выполняется одной командой ip link на хосте. В GCP изоляция удаляет динамический NIC. Если все механизмы терпят неудачу, панель управления все равно выполняет повышение, поскольку архитектура спроектирована по принципу fail-open.
Два вывода из этой работы применимы и за пределами панели управления. Во-первых, на серверах с пакетом postgresql-common проверяйте настройки в start.conf и поведение остановленного первичного узла при следующей загрузке. во-вторых, механизм изоляции, удаляющий сетевой интерфейс, зависит от того, какие именно NIC облачная платформа позволяет отсоединять от работающей виртуальной машины. На AWS это любой NIC, кроме интерфейса с индексом устройства 0, а в GCP — только динамические NIC, поскольку обычные сетевые интерфейсы фиксируются при создании виртуальной машины.












