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

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

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

В Patroni был встроен DCS и он до сих пор есть, хотя и не рекомендуется к использованию и объявлен deprecated. То есть можно сконфигурировать Patroni без внешнего DCS (etcd). Однако, Patroni прошел долгий путь развития и разработчики Patroni пришли к тому, что лучше использовать внешний DCS.

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

Число же реплик может исчисляться десятками, как у OpenAI (50 реплик). Вряд ли работоспособность встроенного DCS, при большом (больше 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 — приложение получает логический номер транзакции и проверяет статус, в случае разрыва сессии и переподсоединения к резервной базе данных.

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

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

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

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

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

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

@OlegIct
14.08.2026 22:38 UTC
Первоисточник

Комментарии

@hard_sign
14.08.2026 20:53 UTC
+2

Хороший способ, он значительно снижает вероятность потери данных, но не устраняет её полностью. Чтобы способ сработал, надо, чтобы сервер бизнес-логики не умер одновременно с ведущим сервером БД.

@OlegIct
15.08.2026 21:43 UTC
+2

Для защиты от одновременного умирания придётся разнести команду и COMMIT:

postgres=# begin;
BEGIN
postgres=*# insert into t1 values (1) returning txid_current();
 txid_current 
--------------
          805
(1 row)

INSERT 0 1

Cохранить полученный номер транзакции на серевере приложений (бизнес-логики) в файл или куда-нибудь.

Дальше послали COMMIT, но подтверждения не получили. Сервер приложений и/или базы данных упали. После перезапуска сервера приложений прочли номер транзакции из файла. Дальше достаточно проверить статус транзакции:

postgres=# select txid_status(805);
 txid_status 
-------------
 aborted
(1 row)

Если сервер приложений обслуживает человека, то при падении сервера приложений веб-сессия разорвётся сразу после нажатия кнопки человеком "выполнить", человек в новой сессии догадается проверить выполнился ли "выполнить" (баланс счета поменялся или по истории проводок).

@Sleuthhound
15.08.2026 06:23 UTC
+2

Был бы интересен опыт и рассказы людей, которые пишут АБС и процессинг банков, про особенности и боли эксплуатации с Postgres. Если таковые люди и компании конечно есть в РФ. За границей я слышал что в Revolut используется облачный Postgres, но они хотят переехать на селф-хостед.

@Siemargl
15.08.2026 09:08 UTC
0

АБС и процессинг банков, про особенности и боли эксплуатации с Postgres

Поддерживаю!

Чтобы с этими банками больше дела не иметь.

17.08.2026 07:09 UTC
+1

Газпромбанк перевел свою АБС ЦФТ-Банк на PostgresPro. И сделал какой-то "расчетный центр" на YDB, которая ,судя по документации, не умеет в PITR ;) Являюсь клиентом оного банка. Полет нормальный (пока) ;)

17.08.2026 11:33 UTC
0

Подождите, все таки эти страшики про потерю - тут речь про синхроные реплики и автофайловер. И это довольно редкий сценарий, потому как синхронные реплкики - это ооогромный минус доступности системы - синхронная реплика не ответила, АБС стоит.
На практике, в большинстве случаев, это старые добрые асинхронные репики , без автофайловера, с мультипликацией транзакшн логов на мастере. Лег мастер - доставай его транзакшн логи, накатывай на реплику. Да это простой, но вероятность его низка, зато в целом производительно и клиенты денежки не потеряют. И на этом кейсе оракл и постгрес не особо отличаются.

18.08.2026 06:43 UTC
0

если не додумывать, страшилок нет. Жирным текстом выделил фразу "Что хорошо". Это как COPY WITH FREEZE - в документации пугают нарушениями видимости, но всё просто, только надо об особенности знать.

Минус по производительности будет, если на Oracle установить Maximum Protection. Он фиксирует транзакцию, если журнальная зпись передана резервной базе (SYNC) + резервная база записала журнальную запись на диск (AFFIRM). Maximum Availability допускат "fastsync" - (SYNC NOAFFIRM). Фаст значит быстрый, его и используют. Этот режим преедачи журналов и выставляют и для Maximum Availability и для Maximum Performance. Режим передачи не зависит от режима защиты, можно стаить как угодно.

В PostgreSQL аналогично: synchronous_commit=on, это абсолютно то же самое, что в Oracle SYNC AFFIRM - работает медленно. Если установить synchronous_commit=remote_write , то это то же самое, что в Oralce "LogXptMode=fasysync" = SYNC NOAFFIRM. Если сетевая задержка низкая (реплика рядом с мастером), то сеть работает быстрее, чем fdatasync диска, то есть реплика получает журнальную запись быстрее, чем процесс мастера получит подтверждение на fdatasync в свой WAL и синхронная реплика не будет замедлять транзакции. Режим, аналогичный Maximum Availability.

Мультипликации транзакшн логов на мастере посгреса нет, это в оракле. Команд наката транзакшн логов в посгресе нет. В Oracle при ручной активации резервной базы, если не было режима Maximum Protection, я бы тоже рекомендовал проверить сохранились ли оперативные журналы primary и, по возможности, скопировать их и наложить перед активаций резервной базы, если позволяет время, так как в режиме Maximum Availability если сеть между primary и standby разорвется непосредственно перед падением primary, Availability не гарантирует отсутствие потерь транзакций - через NET_TIMEOUT=30 секунд primary подтверждает транзакции без standby и их может быть много. В PostgreSQL такого нет - транзации не подтверждаются и висят, если не прервать сессии или процессы как я описал в статье. То есть разница между Oralce и PostgreSQL есть. Общее то, что ручной накат журналов с primary/мастера на standby/реплику ни в PostgreSQL, ни в Oracle не описывается и не рекомендовался. Можно ли было бы Patroni переносить WAL с матера и проверять есть ли расхождение с репликой, думаю, это было бы сложно и породило бы более вероятные сбои. Даже вручную это чревато ошибками.

19.08.2026 10:56 UTC
-2

Согласен , кроме

Мультипликации транзакшн логов на мастере посгреса нет,
Не шла речь что именно силами БД, для оракла да можно БД, для pg еще 100 спосбов у вас есть.

что ручной накат журналов с primary/мастера на standby/реплику ни в PostgreSQL, ни в Oracle не описывается и не рекомендовался
Или что вы понимаете под ручным накатом ? Если скопировать вручную логи с праймари, то это есть в доке.

Загуглил в pg из спортивного интерса:
26.2.2. Standby Server Operation 

...In standby mode, the server continuously applies WAL received from the primary server. The standby server can read WAL from a WAL archive (see restore_command) or directly from the primary over a TCP connection (streaming replication). The standby server will also attempt to restore any WAL found in the standby cluster's pg_wal directory. That typically happens after a server restart, when the standby replays again WAL that was streamed from the primary before the restart, but you can also manually copy files to pg_wal at any time to have them replayed.

В оракле думаю тоже не проблема найти.

@grigoryvp
15.08.2026 10:09 UTC
+2

@dph, ты писал про карточный процессинг для банков. Расскажешь как вы с этим живете?

@Maxim999
15.08.2026 06:47 UTC
+2

Ошибочку исправьте пожалуйста. Не "в нём учено множество", а "в нём учтено множество" правильно.

@QtRoS
15.08.2026 13:28 UTC
+1

Раз уж начали в комментах обсуждать недочёты, то:

вероятность прерывания на шагах 4–6 увеличивается

Выше этой фразы пунктов всего 5.

@OlegEgoism
16.08.2026 07:28 UTC
+2

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