Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Схема безопасного failover PostgreSQL в Kubernetes с Patroni, ZooKeeper и синхронными standby

Риски автоматического failover: как построить по-настоящему отказоустойчивый PostgreSQL в Kubernetes

Опыт инженеров Datadog по устранению критической уязвимости в кластерах PostgreSQL на Kubernetes. Рассматриваются тонкости настройки Patroni и ZooKeeper, переход на strict-режим синхронной репликации и результаты бенчмарков производительности при различных уровнях коммита для защиты от потери данных.

Риски автоматического failover: как построить по-настоящему отказоустойчивый PostgreSQL в Kubernetes

В проектировании высокодоступных систем существует парадокс: автоматический failover (переключение на резервный узел) при аварии может отказаться работать, и это — правильное решение для защиты бизнеса. При сбоях приходится балансировать между доступностью записи (write availability), сохранностью транзакций (durability) и задержкой ответа (bounded latency). Стандартные алгоритмы автоматизации без тонкой настройки репликации могут подвести в критический момент. Разберем, почему автоматический failover в кластерах PostgreSQL на Kubernetes не гарантирует надежность по умолчанию, как сетевые задержки блокируют асинхронные реплики и как инженеры Datadog перешли на строгую синхронную репликацию для защиты от потери данных.

Исходная архитектура: разделение пулов и роль координатора

Для обеспечения масштабируемости и надежности кластер СУБД разделяют на два пула:

  • Leader Pool (Пул лидера). Включает один основной пишущий узел (primary/writer) и два резервных (standbys). Резервные узлы служат исключительно кандидатами на failover.
  • Read Replica Pool (Пул реплик для чтения). Состоит из асинхронных реплик для масштабирования чтения. Они исключены из процесса выборов нового лидера, чтобы их нагрузка не мешала аварийному переключению.

Репликацией управляет Patroni, а координатором выступает ZooKeeper (хранилище конфигурации, DCS). Он хранит блокировку лидера (leader lock), состояние узлов и показатели LSN (Log Sequence Number — номер записи в журнале БД). ZooKeeper использует механизм эфемерных узлов (ephemeral znodes): блокировка лидера удерживается основным сервером, только пока он подтверждает свое присутствие в сети. Если связь теряется, блокировка удаляется, а Patroni инициирует выборы в Leader Pool. Однако при сетевых проблемах схема ведет себя консервативно: реплика без связи с лидером или ZooKeeper не может доказать актуальность своих данных, поэтому безопасное переключение блокируется.

Сбой на практике: хроника одного игрового дня

Проблемы асинхронной схемы проявились во время игрового дня (gameday) в staging-окружении Datadog при имитации зонального сбоя (zonal failure) с искусственной задержкой сети. В одной из зон доступности возникли сетевые проблемы. Приложения продолжали писать данные на лидер, но передача журнала предзаписи (WAL — Write-Ahead Log) на резервные узлы начала сильно отставать.

В этот момент сработал предохранитель Patroni — параметр maximum_lag_on_failover. Этот лимит задает максимально допустимое отставание резервного узла от лидера (в байтах WAL) для безопасного переключения. Поскольку лаг на всех standby-узлах превысил лимит, Patroni заблокировал автоматический failover. После окончательного выхода лидера из строя система автоматизации отказалась повышать любую из реплик, так как это привело бы к безвозвратной потере транзакций. Кластер остался без пишущего узла, перейдя в режим недоступности на запись. Инцидент показал: автоматический failover бесполезен, если реплики отстают.

Анатомия надежности: пять уровней подтверждения транзакций

Чтобы гарантировать наличие актуальной копии данных на standby при падении лидера, подтверждение коммита клиенту связывают с доставкой WAL на реплику через настройку synchronous_commit в PostgreSQL. Администраторам доступны пять уровней подтверждения транзакции:

  1. remote_apply (Максимальная надежность). Лидер ожидает, пока реплика получит, запишет на диск и применит изменения в память. Это гарантирует актуальность данных при чтении после failover.
  2. on / remote_flush (Стандартная синхронность). Лидер ждет, пока реплика запишет WAL на диск и выполнит сброс буферов (flush). Изменения на реплике могут быть еще не применены, но физически сохранены.
  3. remote_write (Слабая синхронность). Подтверждение возвращается после приема WAL в кэш ОС реплики. Есть риск потери при одновременном падении лидера и ОС реплики.
  4. local (Локальная гарантия). Транзакция подтверждается после сброса WAL на диск лидера. Сетевые подтверждения от реплик не ожидаются.
  5. off (Асинхронный режим). Подтверждение возвращается еще до физической записи WAL на диск лидера. Максимальная скорость при риске потери данных.

Для решения проблемы сбоев инженеры Datadog внедрили гибридную схему: standby-узлы из Leader Pool перевели в режим синхронной репликации, а реплики из Read Replica Pool остались асинхронными. В Patroni включены параметры synchronous_mode=true, synchronous_node_count=1 и synchronous_mode_strict=true (строгий режим), а в PostgreSQL выбран уровень synchronous_commit=remote_apply.

Цена безопасности: результаты бенчмарков производительности

Синхронный режим увеличивает накладные расходы на запись из-за ожидания подтверждения от реплики. Тестирование с помощью pgbench (тест TPC-B) на Patroni 3.2.1 показало следующие результаты по сравнению с асинхронным режимом:

  • Рост средней задержки записи (latency): при local +32%, при remote_write +38%, при on / remote_flush +46%, при remote_apply +53%.
  • Падение пропускной способности (tps): при local -23%, при remote_write -27%, при on / remote_flush -31%, при remote_apply -34%.

Хотя на уровне БД строгий режим remote_apply забирает до трети пропускной способности и увеличивает задержку наполовину, реальный опыт эксплуатации показал важный нюанс. После постепенного раската синхронного режима на высоконагруженных кластерах общего падения производительности приложений на прикладном уровне обнаружено не было. Прикладная система сглаживает пиковые задержки БД за счет архитектурных решений: очередей сообщений, батчинга (группировки мелких записей в одну транзакцию) и правильного распределения latency-бюджета.

Поведение в экстремальных сценариях

Строгий синхронный режим меняет поведение кластера при авариях:

  • Потеря одной синхронной реплики. Patroni отслеживает состояние узлов в ZooKeeper и представление pg_stat_replication. Если реплика падает, Patroni автоматически обновляет параметр synchronous_standby_names, назначая другой standby из Leader Pool синхронным.
  • Потеря всех реплик. В строгом (strict) режиме запись на лидере физически заблокируется: PostgreSQL будет локально принимать транзакции, но не вернет подтверждение клиенту, пока хотя бы один подходящий standby-узел не вернется в сеть. Это выбор в пользу сохранности данных ценой временного простоя.
  • Крах лидера во время коммита. Если лидер упал до подтверждения от реплики, при назначении нового лидера старый узел при возвращении обнаружит расхождение timeline (веток истории БД). Patroni задействует утилиту pg_rewind, которая откатит локальные нереплицированные изменения старого лидера, синхронизирует его с новым primary и вернет как реплику.
  • Потеря ZooKeeper. Без связи с DCS лидер переходит в режим read-only после истечения TTL блокировки. При использовании встроенного механизма failsafe лидер через REST API напрямую опрашивает участников кластера и продолжает работу, если они доступны, даже при падении ZooKeeper.

Пошаговое руководство по внедрению безопасного failover

Регламент безопасного перехода на синхронную схему:

  1. Изоляция топологии. Убедитесь, что резервные копии для failover находятся в выделенном Leader Pool, а реплики для чтения (Read Replica Pool) полностью исключены из списка кандидатов на повышение.
  2. Настройка Patroni. Включите в конфигурации кластера параметры: synchronous_mode: true, synchronous_node_count: 1 и synchronous_mode_strict: true (для максимальной защиты данных).
  3. Настройка PostgreSQL. В параметрах БД установите значение: synchronous_commit: remote_apply.
  4. Проверка. Выполните команду patronictl show-config и убедитесь, что параметры применились.
  5. Тестирование. Проведите синтетический тест производительности с помощью pgbench на тестовом контуре, чтобы зафиксировать базовые показатели задержки (write latency) и пропускной способности (tps).
  6. Поэтапный раскат. Начинайте внедрение с наименее критичных кластеров с периодом наблюдения (bake-in period) не менее нескольких дней, отслеживая прикладные показатели.
  7. Мониторинг. Добавьте отслеживание событий ожидания синхронной репликации (postgresql.activity.waits по типу SyncRep), лага репликации в байтах, активного статуса patroni_sync_standby и роста очереди задач на стороне приложения.
  8. Проведение учений. Имитируйте аварии на тестовом стенде: отключение одного standby-узла, изоляцию зоны доступности, падение лидера во время интенсивной записи и отключение ZooKeeper. Убедитесь, что система ведет себя предсказуемо.
  9. Аварийный регламент. Внесите в должностные инструкции (runbooks) пункт об экстренном снижении строгости репликации при авариях. Для быстрого перевода в асинхронный режим используйте команду patronictl edit-config с изменением synchronous_mode на false без перезапуска СУБД.