При создании отказоустойчивых систем, стремятся гарантировать отсутствие потерь транзакций при восстановлении из бэкапов или при переключении на реплику. В статье рассматривается, как в коде приложения можно выяснить, была ли потеряна транзакция при переключении на реплику, а также если команда на фиксацию была отправлена, но подтверждение о том, что она зафиксирована получено не было. Вы познакомитесь с техникой (шаблоном написания запросов) 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.
При использовании логической репликации происходит то же самое.
Фиксация транзакции физически многошаговая, а не атомарная, а с синхронными репликами, ещё и протяжённая во времени:
Журнальная запись, содержащая COMMIT, сохраняется в WAL‑файл (writeback, fdatasync)
Устанавливается бит о фиксации транзакции в журнале транзакций pg_xact (CLOG)
WALSenders отправляют содержимое WAL файлов репликам и ждут подтверждения о приёме (synchronous_commit=remote_write) или сохранении (on)
Транзакция помечается как видимая другим сессиям
Клиенту возвращается подтверждение о фиксации транзакции.
Эти действия неатомарны и могут быть прерваны в любой момент.
Более того, при задержке в передаче журнальных записей синхронной реплике может пройти много времени и вероятность прерывания на шагах 3–5 увеличивается.
В результате есть вероятность, что:
изменения записаны на диск, но не видны клиентам
транзакция реплицирована и видна сессиям реплики, но не видна сессиям мастера
результат транзакции виден сессиям мастера до того, как сессия, выполнившая транзакцию, получит подтверждение о фиксации своей транзакции
Bruce Momjian рекомендует использовать тот же самый метод, который используется Oracle в опции Oracle Transaction Guard — функции txid_current() и txid_status().
Как появляется транзакция, не переданная на реплики
Клиенты могут:
прервать собственные запросы, послав сетевой пакет
CancelRequest(pid, secret). psql посылает такой пакет при нажатии комбинации клавиш ctrl+c.В других сессиях клиенты могут вызвать функцию
pg_cancel_backend()Суперпользователи и члены роли
pg_signal_backendмогут вызвать функциюpg_terminate_backend()Сессия прервётся по таймауту transaction_timeout или таймауту на клиенте (серверный процесс получит уведомление о закрытии сетевого сокета).
экземпляр может перезапуститься после сбоя или пропадания питания.
В этих случаях, в локальном журнале транзакция, ожидающая подтверждения (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, но не получил подтверждение о фиксации по любой причине (сеть перестала пропускать пакеты), то клиенту не известно зафиксировалась ли транзакция или нет:
Команда COMMIT могла не дойти до серверного процесса, тогда транзакция не зафиксируется
Команда COMMIT была получена серверным процессом и он зафиксировал транзакцию, но подтверждение до клиента не дошло.
Ив этом члучае, клиент может проверить статус транзакции, используя функции txid_current() + txid_status().
Проблему потери транзакций сложно воспроизвести, вероятность её возникновения низка, если только не прерывать запросы или сессии вручную или по таймауту. Вероятность увеличивает замедление работы сети, большой поток коротких транзакций, перед тем, как реплика станет мастером. Если пакеты проходят по сети без замедления или в транзакции несколько команд, вероятность проблемы невелика. Вероятность также возрастает при использовании огромного числа кластеров PostgreSQL, которые (или контейнеры, в которых они работают) часто перезапускаются, что приводит к частым продвижениям реплик до мастера.
Тантор Лабс приглашает читателей Хабра на конференцию Tantor JAM, которая пройдёт в Москве 10 сентября 2026г. Участие в конференции, как очное, так и дистанционное - бесплатно.
Комментарии (10)

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

Siemargl
14.08.2026 20:53АБС и процессинг банков, про особенности и боли эксплуатации с Postgres
Поддерживаю!
Чтобы с этими банками больше дела не иметь.

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

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

OlegEgoism
14.08.2026 20:53Установка таймаутов на стороне приложения меньше, чем Patroni TTL (обычно 30с) — это классический выстрел в ногу, который гарантирует, что при сетевом шторме вы сами себе отмените транзакции (и получите те самые локальные коммиты без репликации) до того, как Patroni вообще успеет распознать сбой и провести выборы.
hard_sign
Хороший способ, он значительно снижает вероятность потери данных, но не устраняет её полностью. Чтобы способ сработал, надо, чтобы сервер бизнес-логики не умер одновременно с ведущим сервером БД.
OlegIct Автор
Для защиты от одновременного умирания придётся разнести команду и COMMIT:
Cохранить полученный номер транзакции на серевере приложений (бизнес-логики) в файл или куда-нибудь.
Дальше послали COMMIT, но подтверждения не получили. Сервер приложений и/или базы данных упали. После перезапуска сервера приложений прочли номер транзакции из файла. Дальше достаточно проверить статус транзакции:
Если сервер приложений обслуживает человека, то при падении сервера приложений веб-сессия разорвётся сразу после нажатия кнопки человеком "выполнить", человек в новой сессии догадается проверить выполнился ли "выполнить" (баланс счета поменялся или по истории проводок).