Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

HA: Отказоустойчивость PostgreSQL. Transaction Guard

Дата публикации: 14-08-2026 17:38:34

При создании отказоустойчивых систем стремятся гарантировать отсутствие потерь транзакций при восстановлении из бэкапов или при переключении на реплику. В статье рассматривается, как в коде приложения можно выяснить, была ли потеряна транзакция при переключении на реплику, а также если команда на фиксацию была отправлена, но подтверждение о том, что она зафиксирована, получено не было. Вы познакомитесь с техникой (шаблоном написания запросов) Transaction Guard. Читать далее

Основное содержимое страницы с новостью.

При создании отказоустойчивых систем стремятся гарантировать отсутствие потерь транзакций при восстановлении из бэкапов или при переключении на реплику. В статье рассматривается, как в коде приложения можно выяснить, была ли потеряна транзакция при переключении на реплику, а также если команда на фиксацию была отправлена, но подтверждение о том, что она зафиксирована, получено не было. Вы познакомитесь с техникой (шаблоном написания запросов) Transaction Guard.

Patroni — стандарт де‑факто для создания HA конфигураций PostgreSQL. Преимуществом Patroni является то, что в нём учтено множество граничных условий и выявлены особенности работы PostgreSQL, которые в других системах даже не упоминаются. При этом, Patroni использует внешнюю базу DCS (Distributed Configuration Store). Почему же разработчики Patroni не используют встроенный DCS, ведь это бы упростило внедрение Patroni?

В Patroni был встроен DCS и он до сих пор есть, хотя и не рекомендуется к использованию и объявлен deprecated. То есть можно сконфигурировать Patroni без внешнего DCS (etcd). Однако, Patroni прошел долгий путь развития, и его разработчики пришли к тому, что лучше использовать внешний DCS. Так почему же? Допустим, на каждом узле Patroni, был бы встроенный (built‑in) узел кластера DCS. Всё работало бы прекрасно, пока узлов немного — 3 или 5. При увеличении членов кластера (реплик PostgreSQL), увеличивалось бы и число узлов DCS, время прихода к консенсусу существенно бы возрастало. В документации к etcd написано «Вероятно, кластер etcd не должен состоять более чем из семи узлов. Опыт эксплуатации сервиса блокировок Google Chubby, аналогичного etcd и используемого Google на протяжении многих лет рекомендует использование пяти узлов».

Число же реплик может исчисляться десятками, как у OpenAI (50 реплик). Вряд ли работоспособность DSC, встроенного и замоноличенного в экземпляр PostgreSQL, при большом (больше 7) числе экземпляров, останется приемлемой.

Второй пример. Patroni способен пережить полный отказ внешнего DCS, это режим failsafe: Primary работает, пока доступны все Standby. По умолчанию режим отключён, но его стоит включить.

Третий пример. В документации Patroni написано об особенности PostgreSQL:

«Из‑за особенностей реализации синхронной репликации в PostgreSQL возможна потеря транзакций даже при использовании synchronous_mode_strict. Если работа серверного процесса PostgreSQL прерывается во время ожидания подтверждения от реплики (по любой причине: прерывание запроса, сессии, тайм‑аута клиента, сбоя процесса), результат транзакции становится видимым для других сессий мастера. Запись же о фиксации транзакции может быть ещё не реплицирована, и если реплика станет мастером, результат транзакции на новом мастере будет потерян.»

На вопрос Jepsen, есть ли способы настроить Patroni так, чтобы не терять транзакции, Александр Кукушкин (Микрософт, ранее Zalando), автор Patroni, ответил, что это поведение PostgreSQL и приемлемого способа устранить это средствами Patroni нет.

При использовании логической репликации происходит ровно то же самое.

Фиксация транзакции физически многошаговая, а не атомарная, а с синхронными репликами, ещё и протяжённая во времени:

  1. Журнальная запись, содержащая COMMIT, сохраняется в WAL‑файл (writeback, fdatasync)

  2. Устанавливается бит о фиксации транзакции в журнале транзакций pg_xact (CLOG)

  3. WALSenders отправляют содержимое WAL файлов репликам и ждут подтверждения о приёме (synchronous_commit=remote_write) или сохранении (on)

  4. Транзакция помечается как видимая другим сессиям

  5. Клиенту возвращается подтверждение о фиксации транзакции

Эти действия неатомарны и могут быть прерваны в любой момент. Более того, при задержке в передаче журнальных записей синхронной реплике может пройти много времени и вероятность прерывания на шагах 3–5 увеличивается.

В результате есть вероятность, что:

  1. Изменения записаны на диск, но не видны клиентам

  2. Транзакция реплицирована и видна сессиям реплики, но не видна сессиям мастера

  3. Результат транзакции виден сессиям мастера до того, как сессия, выполнившая транзакцию, получит подтверждение о фиксации своей транзакции

Bruce Momjian рекомендует использовать тот же самый метод, который используется Oracle в опции Oracle Transaction Guard — функции txid_current() и txid_status().

Как появляется транзакция, не переданная на реплики

Клиенты могут:

  1. Прервать собственные запросы, послав сетевой пакет CancelRequest(pid, secret). psql посылает такой пакет при нажатии комбинации клавиш Ctrl+C.

  2. В других сессиях клиенты могут вызвать функцию pg_cancel_backend()

  3. Суперпользователи и члены роли pg_signal_backend могут вызвать функцию pg_terminate_backend()

  4. Сессия прервётся по таймауту transaction_timeout или таймауту на клиенте (серверный процесс получит уведомление о закрытии сетевого сокета).

  5. Экземпляр может перезапуститься после сбоя или пропадания питания.

В этих случаях в локальном журнале транзакция, ожидающая подтверждения (SyncRepWaitForLSN) от синхронной реплики (реплик), будет зафиксирована, а синхронные реплики могут не получить LSN с фиксацией транзакции.

Рассмотрим пример. При прерывании запроса в режиме автофиксации серверный процесс, находящийся в ожидании подтверждения от синхронной реплики (SyncRepWaitForLSN), выдаст сообщение о том, что транзакция зафиксирована (INSERT 0 1), единственно только добавит предупреждение (предупреждение доступно, как минимум, клиентам, использующим драйвера libpq и jdbc):

insert into t1 values (1); 
WARNING:  canceling wait for synchronous replication due to user request 
DETAIL:  The transaction has already committed locally, but might not have been replicated to the standby.
INSERT 0 1

Изменения тут же станут видны в других сессиях мастера, при том, что реплика из‑за медленной работы сети могла не получить изменения. Если мастер сбойнёт и реплика станет мастером, изменений прерванной транзакции на реплике не будет. Что хорошо - увидев изменения, транзакции не смогут поменять данные так, чтобы изменения (сделанные на основе увиденных, но не полученных репликой данных) были переданы на реплику и появились на ней, когда она станет новым мастером. Запись в WAL последовательная — если запись о COMMIT не принята репликой, то не приняты и все последующие записи. То есть не будет ситуации, когда транзакция считывает баланс счёта и вносит изменения на основе неверного баланса. Единственное нарушение в том, что сессии к бывшему мастеру могут на короткое время (до остановки бывшего мастера) увидеть данные, которые на новом мастере не были зафиксированы.

В PostgreSQL предлагалось добавить параметр конфигурации, которым можно было бы запретить прерывать транзакции, ожидающие подтверждения от реплики в течение какого‑то времени. Этого не сделали, так как это не устраняет проблему (например, можно убить серверный процесс).

Что можно сделать?

Не прерывать подвисшие запросы и сессии. В случае, если мастер не продлит лизинг, он будет остановлен и реплика станет новым мастером. При этом параллельные сессии не увидят данных неподтверждённых синхронной репликой транзакций. То есть не прерывать запросы и не устанавливать transaction_timeout в значения, меньшие 30 секунд (Patroni TTL, время на распознавание кто сбойнул — мастер или реплика). В случае, если сбойнула реплика или сеть, реплика станет отставшей, и Patroni её не сделает её мастером. После восстановления связи, реплика получит недостающие журнальные записи.

В сообществе разработчиков PostgreSQL обсуждали, можно ли использовать распределённые транзакции (2PC) для устранения потери транзакций, и пришли к консенсусу, что: «COMMIT PREPARED также необходимо реплицировать, что приводит к той же проблеме, что и обычный COMMIT: если он выполняется во время переключения на реплику, его можно отменить, и зафиксированные данные могут быть неправильно отображены и записаны. 2PC не является решением проблемы, связанной с тем, что PostgreSQL молча отменяет ожидание синхронной репликации. Проблема возникает при наличии любого „commit“. А „commit“ присутствует, если существуют транзакции.»

PostgreSQL Transaction Guard

Он есть в PostgteSQL и работает точно так же, как в Oracle. Для критичных транзакций можно использовать txid_current() для получения номера транзакции, результат которой важен. В случае разрыва сессии, который произойдёт при переключении на нового мастера, проверять статус транзакции функцией txid_status():

insert into t1 values (1) returning txid_current();
 txid_current 
--------------
        6767
WARNING:  canceling wait for synchronous replication due to user request 
DETAIL:  The transaction has already committed locally, but might not have been replicated to the standby.
INSERT 0 1

После разрыва сессии из-за остановки мастера в новой сессии с новым мастером достаточно запросить статус транзакции:

select txid_status(6767); 
 txid_status 
-------------
committed

Такой способ используется в опции Oracle Transaction Guard, появившийся в 12 версии Oracle Database — приложение может получить логический номер транзакции и проверить её статус, в случае, если оно не получило подтверждения о фиксации транзакции по любой причине, например, падения primary и переподключении сессий на резервную базу.

Transaction Guard полезен и без переключения на реплику. Если клиент послал команду COMMIT, но не получил подтверждение о фиксации по любой причине (сеть перестала пропускать пакеты), то клиенту не известно, зафиксировалась ли транзакция или нет:

  1. Команда COMMIT могла не дойти до серверного процесса, тогда транзакция не зафиксируется

  2. Команда COMMIT была получена серверным процессом и он зафиксировал транзакцию, но подтверждение до клиента не дошло.

Ив этом случае, клиент может проверить статус транзакции, используя функции txid_current() + txid_status().

Проблему потери транзакций сложно воспроизвести, вероятность её возникновения низка, если только не прерывать запросы или сессии вручную или по таймауту. Вероятность увеличивает замедление работы сети, большой поток коротких транзакций, перед тем, как реплика станет мастером. Если пакеты проходят по сети без замедления или в транзакции несколько команд, вероятность проблемы невелика. Вероятность также возрастает при использовании огромного числа кластеров PostgreSQL, которые (или контейнеры, в которых они работают) часто перезапускаются, что приводит к частым продвижениям реплик до мастера.

Тантор Лабс приглашает читателей Хабра на очную конференцию Tantor JAM, которая пройдёт в Москве 10 сентября 2026 г. Участие бесплатное.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1BiHA: встроенная отказоустойчивость Postgres Pro Enterprise и Standard07.9414-08-2026
2[Перевод] Статистика PostgreSQL: почему запросы выполняются медленно07.7419-08-2026
3[Перевод] Ограничения целостности с отложенной проверкой в PostgreSQL0706-07-2026
4[Перевод] Проектирование системы хранения POSTGRES0515-07-2026
5Кто выгрузил платежи, или Пример расследования инцидента на аудите в Postgres Pro Enterprise0710-07-2026
6Четыре антипаттерна CTE в PostgreSQL: разбираем на EXPLAIN ANALYZE09.1620-08-2026
7The dark side of компрессия в PostgreSQL07.4819-08-2026
8Оптимизация агрегатов PostgreSQL — что может расширение?07.6718-08-2026
9Асинхронный I/O в PostgreSQL или история выходного дня09.7817-08-2026
10https://youtu.be/KaGqmF6m0rk?is=YPRyWv6NzKkLxozh Почему PostgreSQL захватил мир баз данных? / 🇱🇷СУБД / ...06.3713-08-2026

Классификация: . Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 8.24. Источник: habr.com.