ADG Оставить заявку
Блог Данные и аналитика 6 мин чтения

Patroni 4.0 и quorum-репликация: переводим трёхнодовый кластер на синхронный режим

Patroni 4.0 принёс нативную поддержку quorum-репликации. Разбираем, что это меняет в трёхнодовом кластере с нодами в разных ЦОД и как ведёт себя при split-brain.

Контекст момента

Patroni 4.0: поддержка синхронной репликации quorum и переработанный REST API управления кластером

Patroni 4.0 вышел в начале года, и главное изменение там - это не косметика и не рефакторинг API (хотя REST API тоже переработали), а доведённая до ума поддержка quorum-репликации. Для нас это был повод наконец-то перевести один кластер, который давно хотелось сделать по-человечески, с асинхронного режима на синхронный - но без потери возможности пережить отказ одной ноды без вмешательства руками.

Почему до 4.0 это было неудобно

В Patroni до версии 4.0 синхронная репликация работала по принципу «один синхронный стендбай»: лидер ждёт подтверждения от одной конкретной реплики. Если эта реплика падает - Patroni переключается в асинхронный режим, чтобы не положить запись. Это предсказуемо, но означает следующее: в момент, когда вы потеряли синхронную реплику, у вас временно нет гарантий durability до тех пор, пока другая реплика не займёт её место.

При трёх нодах в разных ЦОД этот сценарий неприятен: ЦОД-2 упал, Patroni переключился в асинхрон, в этот момент записи идут только на ЦОД-1, и если сразу за этим падает ЦОД-1 - данные за окно асинхронного режима под вопросом.

Quorum-репликация в PostgreSQL (параметр synchronous_commit = remote_apply или remote_write с synchronous_standby_names = ANY 1 (...)) решает это иначе: лидер ждёт подтверждения от любых N реплик из пула. При трёх нодах и кворуме 1-из-2 реплик - транзакция подтверждена когда хотя бы одна из двух стендбаев ответила. Отказ одной реплики не меняет поведение записи. Но в Patroni до 4.0 управлять этим параметром было неудобно: synchronous_standby_names нужно было прописывать вручную и синхронизировать с тем, что Patroni знает о топологии кластера. Рассинхронизация - отдельный класс инцидентов.

Что даёт Patroni 4.0

В 4.0 параметр synchronous_mode: quorum в конфигурации Patroni - появился он ещё в 3.0, но 4.0 довёл его поведение до стабильного состояния. Когда он включён, Patroni сам управляет synchronous_standby_names в PostgreSQL: добавляет ноды в список по мере их готовности, убирает при отказе, следит за тем, чтобы кворум оставался достижимым. Это то, что раньше приходилось делать руками или через внешние скрипты.

Параметр synchronous_node_count задаёт сколько реплик должны подтвердить транзакцию. При трёх нодах кластера (1 лидер + 2 реплики) и synchronous_node_count: 1 - это классический кворум: любая одна из двух реплик достаточна. При synchronous_node_count: 2 - обе, что избыточно для большинства сценариев.

Практически это выглядит так в конфиге:

bootstrap:
  dcs:
    synchronous_mode: quorum
    synchronous_node_count: 1

После перезапуска Patroni сам выставил в PostgreSQL:

synchronous_standby_names = 'ANY 1 (node2,node3)'

И начал обновлять этот параметр при изменениях топологии - что при тесте отказа мы и проверили.

Три ноды в трёх ЦОД: поведение при split-brain

Наш стенд - три ноды, каждая в отдельном ЦОД, DCS (etcd) - тоже трёхнодовый, по одной ноде на ЦОД. Это классическая схема для geographically distributed кластера.

Сценарий, который нас интересовал: потеря связи между ЦОД-1 (лидер) и ЦОД-2, при сохранении связи ЦОД-1 с ЦОД-3 и связи ЦОД-2 с ЦОД-3. Потенциальный split-brain: ЦОД-1 и ЦОД-2 не видят друг друга, у каждого может быть соблазн считать себя лидером.

Что произошло:

  • ЦОД-1 (лидер) видит etcd: два из трёх etcd-нодов (ЦОД-1 и ЦОД-3) доступны, кворум etcd есть. Лидер продолжает работать. Синхронная реплика ЦОД-3 подтверждает транзакции - запись продолжается без деградации.
  • ЦОД-2 (реплика) потерял связь с лидером, но видит etcd через ЦОД-3. Patroni на ЦОД-2 видит в etcd что лидер жив и держит lease. Промоута не происходит. ЦОД-2 просто ждёт восстановления репликации.

Фактического split-brain не возникло - и это корректное поведение, etcd как DCS предотвращает его независимо от режима репликации. Quorum-репликация здесь решает другую проблему: что происходит с durability пока ЦОД-2 недоступен.

При асинхронной репликации Patroni переключился бы в полностью асинхронный режим (только ЦОД-3 в пуле, и то без гарантии). При quorum-режиме с synchronous_node_count: 1 - достаточно одной подтверждающей реплики, и ЦОД-3 её обеспечивает. Гарантия durability не пропадает.

Переработанный REST API

Второе крупное изменение в 4.0 - REST API. В предыдущих версиях набор эндпоинтов был неполным: часть операций требовала patronictl, часть можно было сделать напрямую через API, но документация была скудной. В 4.0 API приведён в порядок: добавлены эндпоинты для управления конфигурацией, явный /cluster с состоянием всех нод, улучшены ответы с кодами ошибок.

Для нас это имеет прикладной смысл: мониторинг кластера через Prometheus/Grafana у нас строится на опросе Patroni REST API. Раньше /patroni возвращал состояние локальной ноды, а для агрегированной картины приходилось опрашивать все три ноды отдельно. Теперь /cluster с любой ноды даёт состояние всего кластера - одним запросом.

Небольшое неудобство при апгрейде: несколько эндпоинтов поменяли поведение ответов. У нас был самодельный скрипт для проверки роли ноды перед деплоем - он полагался на конкретный формат JSON из /patroni. Пришлось обновить.

Как обновлялись

Rolling upgrade без остановки кластера: сначала реплики, потом лидер. Patroni поддерживает смешанные версии в кластере на период обновления.

Перед включением synchronous_mode: quorum убедились что все ноды на 4.0 - смешивать старый и новый режим не стоит, поведение непредсказуемо. Включали через patronictl edit-config, не через файл конфига - чтобы изменение применилось сразу через DCS, без рестарта Patroni.

После включения проверили что PostgreSQL получил корректный synchronous_standby_names - это видно через patronictl show-config и через SHOW synchronous_standby_names в psql на лидере. Совпало с ожидаемым.

Итог на сейчас

Кластер работает на quorum-репликации около двух недель. Write-latency немного выросла - синхронный режим это и предполагает, платишь за durability сетевым RTT до ближайшей реплики. На наших нодах внутри одного города это несколько миллисекунд - для DWH-нагрузки приемлемо, не OLTP с тысячами транзакций в секунду.

Главный выигрыш - управляемость. Patroni сам следит за тем чтобы synchronous_standby_names в PostgreSQL соответствовал реальному состоянию кластера. Один конфигурационный параметр вместо ручного сопровождения. Для трёхнодового геораспределённого кластера это перестаёт быть экспериментальной настройкой и становится нормальным эксплуатационным выбором.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.