Как работает синхронная репликация и почему она честно делит вашу скорость записи пополам
Вы включили синхронную репликацию, потому что асинхронная один раз потеряла у вас несколько последних транзакций при падении мастера, и это было неприятно объяснять клиентам. Теперь данные в безопасности, но приложение стало заметно медленнее писать, и разработчики спрашивают, почему. Короткий ответ: вы обменяли скорость на гарантию, и это честная сделка — если понимать, что именно вы купили.
Содержание
- Что происходит с одной записью при синхронной репликации
- Почему это реально защищает данные, а не просто успокаивает
- Асинхронная репликация: та же схема без ожидания
- Откуда берётся цена: задержка сети добавляется к каждой записи
- Как задержка одного commit превращается в падение пропускной способности записи
- Полусинхронная репликация — компромисс между двумя крайностями
- Как настроить и на что смотреть в PostgreSQL и MySQL
Что происходит с одной записью при синхронной репликации
Возьмём обычный INSERT или UPDATE внутри транзакции. При обычной (асинхронной) репликации последовательность такая: клиент отправляет COMMIT, мастер записывает изменение в свой журнал упреждающей записи (WAL в PostgreSQL, binlog в MySQL), физически сбрасывает его на диск и сразу же отвечает клиенту «готово». Копия этой записи параллельно уходит на реплику, но мастер её не ждёт — он уже занят следующим запросом.
При синхронной репликации между «мастер записал в свой журнал» и «клиент получил ответ» вставляется ещё один шаг: мастер отправляет журнальную запись реплике и ждёт от неё подтверждения получения (в зависимости от настройки — подтверждения записи на диск реплики или даже подтверждения применения к данным). Только после этого подтверждения COMMIT считается завершённым, и клиент получает ответ.
Формально это выглядит так:
Асинхронная:
клиент → COMMIT → мастер пишет WAL локально → ответ клиенту
└─ WAL уходит на реплику (не блокирует)
Синхронная:
клиент → COMMIT → мастер пишет WAL локально
→ отправляет WAL на реплику
→ ЖДЁТ подтверждения от реплики
→ только теперь ответ клиенту
Разница в одной строчке кода на стороне мастера — «дождаться ack» — но именно она определяет всё дальнейшее поведение системы, и в надёжности, и в скорости. Подробнее про сам механизм передачи журнала и то, что происходит на реплике, читайте в статье о том, как устроена репликация и почему реплика отстаёт.
Почему это реально защищает данные, а не просто успокаивает
Смысл ожидания подтверждения не в ритуале, а в конкретном сценарии отказа: что если мастер упадёт ровно в момент между «данные записаны локально» и «данные ушли на реплику»?
При асинхронной репликации ответ клиенту уже отправлен раньше, чем WAL долетел до реплики. Если в этот момент мастер физически умирает (сгорел диск, отключилось питание, зависла виртуалка), то реплика никогда не получит эту транзакцию. С точки зрения приложения транзакция подтверждена — клиент получил COMMIT OK, возможно, уже показал пользователю «заказ оформлен». А физически данные существуют только на разрушенном диске мёртвого мастера. Это классический разрыв между «клиент думает, что данные сохранены» и «данные реально есть хотя бы в двух местах».
При синхронной репликации это невозможно по построению: клиент физически не может получить COMMIT OK, пока реплика не подтвердила получение записи. Значит, к моменту, когда приложение узнало об успехе, данные гарантированно существуют минимум в двух независимых местах — на мастере и на реплике. Если мастер умирает через долю секунды после ответа клиенту, данные не потеряны: реплику можно повысить до нового мастера, и она уже содержит эту транзакцию.
Это именно гарантия нулевой потери подтверждённых транзакций (в терминологии PostgreSQL — RPO, равный нулю для успешных коммитов), а не абстрактное «надёжнее». И это разумно требовать там, где потеря последних секунд транзакций стоит дороже, чем задержка записи: платежи, списания, бронирования, финансовый учёт.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверАсинхронная репликация: та же схема без ожидания
Чтобы контраст был честным: асинхронная репликация не теряет надёжность вообще — она теряет только последние несколько транзакций в узком окне отказа. Все данные, реплицированные раньше, остаются целыми, и в обычном режиме работы реплика отстаёт от мастера на величину, которую вы даже не замечаете.
Асинхронная репликация — это осознанный выбор в пользу скорости там, где полная потеря нескольких последних записей допустима: аналитика, логи, кеш представлений, некритичные счётчики, поисковые индексы. Если реплика вашего блога отстанет от мастера на секунду и при падении потеряет последний комментарий — это неприятно, но не катастрофа. Если то же самое происходит со списанием средств с карты — это уже инцидент с расследованием.
Ключевая мысль: синхронность — это не свойство «хорошей» или «плохой» репликации, а параметр, который вы выбираете под конкретные данные. Многие системы вообще позволяют держать оба режима одновременно для разных таблиц или баз.
Откуда берётся цена: задержка сети добавляется к каждой записи
Вернёмся к схеме выше. При синхронной репликации мастер ждёт подтверждения от реплики, а подтверждение должно физически долететь по сети туда и обратно. Это время — round-trip time (RTT) до реплики — целиком добавляется к времени выполнения каждого COMMIT, без исключений.
Если мастер и реплика находятся в одном дата-центре, RTT обычно небольшой и предсказуемый — задержка внутри одной локальной сети. Если реплика вынесена в другой дата-центр того же региона, задержка растёт до величины, сравнимой с сетевым переходом между площадками. Если реплика в другой стране или на другом континенте — задержка может стать доминирующей частью времени отклика транзакции, кратно превышая время самой записи на диск. Ориентировочные диапазоны задержек между разными типами связок серверов — от одной стойки до разных континентов — собраны в таблице ориентиров по latency между локациями; используйте её, чтобы прикинуть порядок величины для своей конкретной пары площадок, а не абстрактно.
Важно понимать: это не разовая плата за подключение, а плата за каждый отдельный COMMIT. Если приложение делает тысячу мелких транзакций в секунду, и каждая ждёт полный RTT до реплики, вы платите этот RTT тысячу раз, последовательно для каждой транзакции того же клиентского соединения (хотя разные соединения, конечно, могут ждать параллельно). Именно поэтому синхронная репликация особенно болезненна для нагрузок с большим числом мелких транзакций — и относительно терпима для нагрузок с редкими, но крупными.
Что именно происходит физически внутри одного COMMIT до появления записи на диске, включая работу журнала, разобрано в статье о том, что происходит при commit транзакции — синхронная репликация добавляет к этому процессу ровно один дополнительный сетевой скачок.
Как задержка одного commit превращается в падение пропускной способности записи
Здесь легко ошибиться в интуиции. Кажется, что если каждый commit стал медленнее на фиксированную величину, то и общая пропускная способность упала на ту же долю — но на практике эффект часто сильнее, и вот почему.
Во-первых, если транзакции внутри одного соединения выполняются последовательно (а это обычный случай — одно соединение не может отправить второй COMMIT, не дождавшись ответа на первый), то время ожидания реплики умножается на количество последовательных транзакций в этом соединении. Пропускная способность одного соединения на запись падает пропорционально добавленной задержке.
Во-вторых, растёт число одновременно открытых транзакций и удерживаемых блокировок. Пока мастер ждёт ack от реплики, транзакция ещё не отпустила ресурсы, которые держит до фиксации — блокировки строк, место в пуле соединений, буферы. Больше параллельно висящих транзакций — выше конкуренция за эти ресурсы, что может создавать вторичные задержки, не связанные напрямую с сетью до реплики.
В-третьих, вы теряете возможность просто «накинуть больше параллелизма» без ограничений: пул соединений к базе не бесконечен, и если каждое соединение стало держать транзакцию дольше, для той же общей нагрузки нужно больше одновременных соединений, что упирается в лимиты max_connections и в память сервера.
Практический вывод: реальное падение пропускной способности записи стоит не предсказывать по формуле, а измерять на своей нагрузке и своей паре площадок — цифры сильно зависят от расстояния до реплики и параллелизма приложения. Универсального процента здесь нет.
Полусинхронная репликация — компромисс между двумя крайностями
Между «мастер вообще не ждёт» и «мастер ждёт полного применения на реплике» есть промежуточные уровни, и именно они чаще всего используются в проде, а не крайние варианты.
PostgreSQL с версии 9.1 позволяет выбирать, чего именно ждать через параметр synchronous_commit:
synchronous_commit = local # не ждать реплику вообще (по сути асинхронно)
synchronous_commit = remote_write # ждать, пока реплика ЗАПИШЕТ WAL в ОС (не обязательно на диск)
synchronous_commit = on # ждать, пока реплика сбросит WAL на СВОЙ диск (fsync)
synchronous_commit = remote_apply # ждать, пока реплика ПРИМЕНИТ изменения к данным
Каждый следующий уровень даёт более сильную гарантию и добавляет больше задержки. remote_write — самый лёгкий синхронный режим: данные пережили падение процесса реплики, но не обязательно падение её ОС. remote_apply — самый строгий: после ack от реплики на ней уже можно читать эти данные, что полезно, если вы балансируете чтения между мастером и репликами и не хотите отдавать клиенту устаревший ответ сразу после записи.
MySQL решает ту же задачу через плагин полусинхронной репликации (semi-sync). В отличие от строгого варианта PostgreSQL, классический semi-sync в MySQL ждёт подтверждения получения от хотя бы одной реплики, но с таймаутом: если реплика не ответила за rpl_semi_sync_master_timeout миллисекунд, мастер по умолчанию откатывается к асинхронному поведению для последующих транзакций, чтобы не встать намертво из-за одной недоступной реплики. Это осознанный компромисс между надёжностью и доступностью — строгая синхронность без такого предохранителя означала бы, что падение единственной синхронной реплики останавливает запись на мастере целиком.
Как настроить и на что смотреть в PostgreSQL и MySQL
В PostgreSQL синхронность настраивается на стороне мастера через postgresql.conf:
# postgresql.conf на мастере
synchronous_standby_names = 'FIRST 1 (replica1, replica2)'
synchronous_commit = on
Запись FIRST 1 (replica1, replica2) означает: ждать подтверждения хотя бы от одной из перечисленных реплик, той, что ответит первой. Можно требовать кворум из нескольких реплик синтаксисом ANY 2 (replica1, replica2, replica3) — это уже ближе к моделям с кворумной записью, о которых стоит думать вместе с общей отказоустойчивостью кластера, включая то, зачем в кластере нужен третий узел для кворума.
Проверить, в каком режиме реально работает конкретная реплика прямо сейчас, можно запросом к мастеру:
SELECT application_name, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
Поле sync_state покажет sync (эта реплика синхронная и мастер её ждёт), potential (кандидат на замену синхронной реплики, если основная отвалится) или async. Если sync_state показывает async там, где вы ожидали sync, значит имя реплики в application_name не совпадает со значением в synchronous_standby_names — частая опечатка при первой настройке.
В MySQL включение semi-sync выглядит так:
-- на мастере
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = 1;
SET GLOBAL rpl_semi_sync_source_timeout = 10000; -- мс
-- на каждой реплике
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = 1;
(В версиях MySQL до переименования терминологии те же плагины назывались rpl_semi_sync_master и rpl_semi_sync_slave — если у вас старая версия, используйте старые имена.) После включения статус подтверждает состояние:
SHOW STATUS LIKE 'Rpl_semi_sync%';
Значение Rpl_semi_sync_source_status = ON означает, что мастер прямо сейчас работает в синхронном режиме; если оно упало в OFF при включённом плагине — значит сработал таймаут, и мастер временно откатился на асинхронное поведение, потому что синхронная реплика не отвечала вовремя. Это стоит мониторить отдельно: сам по себе факт «плагин включён» не гарантирует, что репликация в данный момент действительно синхронна.
Сетевую задержку до кандидатов на роль синхронной реплики стоит проверить ещё на этапе выбора площадки, а не постфактум — прогнать ping и замер RTT между конкретными узлами, а не полагаться на общие представления о географии.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Синхронная репликация защищает от потери данных при любом сбое?
Нет, только от потери уже подтверждённых транзакций при падении мастера. Она не защищает от логических ошибок (удалили не то, что нужно — удаление тоже успешно реплицируется), от повреждения данных на уровне файловой системы, которое реплицировалось до обнаружения, и не заменяет резервные копии.
Можно ли использовать синхронную репликацию только для части таблиц?
В PostgreSQL синхронность настраивается на уровне сессии через SET synchronous_commit = local перед конкретными транзакциями, для которых задержка неприемлема, при этом критичные операции в той же базе можно оставить в режиме on. Это гибче, чем настройка на уровне всего кластера.
Что будет, если единственная синхронная реплика упадёт?
В PostgreSQL с synchronous_standby_names без резервных кандидатов мастер зависнет на COMMIT, ожидая ack, которого никогда не будет, — новые записи не смогут завершиться. Поэтому в проде почти всегда держат минимум двух кандидатов в синхронный список или используют встроенный таймаут-предохранитель, как в semi-sync MySQL.
Насколько сильно упадёт скорость записи, если я включу синхронную репликацию?
Однозначного числа нет — зависит от задержки сети до реплики, характера нагрузки и параллелизма приложения. Единственный надёжный способ узнать — измерить на своей конкретной паре серверов и своей реальной нагрузке, а не полагаться на чужие цифры из других условий.
Стоит ли ставить синхронную реплику в другом регионе ради географической отказоустойчивости?
Это законный сценарий (защита от отказа всего дата-центра), но цена в задержке записи будет заметно выше, чем при реплике в том же регионе. Стоит явно решить, какие именно транзакции обязаны ждать межрегиональное подтверждение, а какие могут остаться асинхронными.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →