MAATRIX / Блог / Реплика базы в другой стране: сколько миллисекунд съедает синхронная запись

Реплика базы в другой стране: сколько миллисекунд съедает синхронная запись

MAATRIX

Вы решили вынести реплику базы данных в другую страну — ради географической отказоустойчивости, чтения ближе к части аудитории или просто потому, что второй дата-центр оказался в другом регионе провайдера. Включили синхронную репликацию, чтобы не потерять ни одной транзакции при падении мастера, и тут же уперлись в стену: запись, которая раньше занимала доли миллисекунды, теперь ощутимо тормозит. Дело не в железе и не в настройках базы — дело в скорости света и расстоянии между странами, и это ограничение не обходится ни более мощным сервером, ни более быстрым диском.

Что физически ждёт мастер при записи в другую страну

Механизм синхронной репликации сам по себе несложный: мастер принимает COMMIT, пишет изменение в собственный журнал (WAL в PostgreSQL, binlog в MySQL), отправляет эту запись реплике и не отвечает клиенту, пока реплика не подтвердит получение. Только после подтверждения транзакция считается завершённой. Если хотите разобрать сам механизм пошагово — уровни synchronous_commit в PostgreSQL, semi-sync в MySQL, конкретные конфиги и SQL-запросы для проверки статуса — это подробно разобрано в статье про синхронную репликацию и цену надёжности. Здесь сосредоточимся на конкретном случае, который добавляет самую тяжёлую цену: реплика не в соседней стойке и не в соседнем дата-центре того же города, а в другой стране.

Ключевая физика простая и неустранимая: подтверждение от реплики должно долететь по сети туда и обратно, и это время — round-trip time, RTT — целиком добавляется к времени каждого COMMIT. Внутри одного дата-центра RTT измеряется в десятых долях миллисекунды, между дата-центрами одного города или региона — уже заметно больше, но обычно в пределах единиц миллисекунд. Между разными странами, а тем более континентами, RTT определяется в первую очередь расстоянием по кабелю и числом промежуточных переходов маршрута — величина может стать доминирующей частью времени отклика транзакции, кратно превышая время самой записи на диск. Точные цифры зависят от конкретной пары точек, поэтому не берите на веру абстрактные оценки: ориентировочные диапазоны по разным направлениям собраны в таблице ориентиров по задержке между локациями, а для своей пары серверов надёжнее просто измерить ping напрямую, до того как закладывать архитектуру в прод.

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

Чем это принципиально отличается от асинхронной репликации

При асинхронной репликации мастер отвечает клиенту сразу после того, как записал изменение локально, — реплика получает копию журнала параллельно, в фоне, не блокируя ответ. Задержка клиента при этом не зависит от расстояния до реплики вообще: реплика в соседней стойке или в другой стране — для скорости записи на мастере разницы нет. Реплика просто отстаёт на какую-то величину, которая обычно небольшая и в спокойном режиме работы её не замечают.

Цена этой скорости — риск. Если мастер физически умирает (сгорел диск, пропало питание, зависла виртуальная машина) в узком окне между «записали локально» и «переслали на реплику», эта транзакция теряется безвозвратно. Клиент уже получил COMMIT OK, возможно, приложение уже показало пользователю «заказ оформлен» — а реального второго экземпляра данных не существует. Для аналитики, логов, некритичных счётчиков и кеша представлений это допустимая цена. Для платежей, списаний и бронирований — обычно нет.

Синхронная репликация устраняет этот риск по построению: клиент физически не может получить подтверждение, пока данные не существуют минимум в двух независимых местах. Но именно поэтому она не может быть «бесплатной» — гарантия существования данных в двух точках требует физического подтверждения из второй точки, а это подтверждение не может прийти быстрее, чем позволяет сеть между ними. Разница между синхронной и асинхронной репликацией — это не разница в «качестве» реализации, а прямой обмен задержки на гарантию, и чем дальше физически вторая точка, тем дороже этот обмен.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как RTT одной записи превращается в падение всей пропускной способности

Интуитивно кажется: если каждый COMMIT стал медленнее на фиксированную величину RTT, то и пропускная способность записи упала на ту же долю. На практике эффект часто сильнее, и вот почему.

Транзакции внутри одного соединения к базе обычно выполняются последовательно — соединение не отправит второй COMMIT, не дождавшись ответа на первый. Значит, добавленный RTT умножается на количество последовательных записей в этом соединении, а не тратится один раз. Приложение, которое делает тысячу мелких транзакций в секунду через одно соединение, физически не может уложиться в секунду, если каждая транзакция ждёт RTT до другой страны — она просто не успеет отправить столько записей последовательно.

Пока мастер ждёт подтверждения от удалённой реплики, транзакция не отпускает ресурсы, которые держит до фиксации: блокировки строк, слот в пуле соединений, буферы. Больше висящих транзакций одновременно — выше конкуренция за эти ресурсы на самом мастере, и это уже вторичная деградация, вызванная сетью, но проявляющаяся не в ней. Чтобы удержать ту же нагрузку, приложению приходится открывать больше параллельных соединений — и здесь легко упереться в max_connections или в память сервера, о которую раньше даже не задумывались, потому что при локальной синхронной реплике транзакции освобождали ресурсы почти мгновенно.

Отсюда практический вывод: цена межстрановой синхронной репликации ощущается сильнее всего не на редких крупных операциях, а на нагрузках с большим числом мелких частых записей — типичные веб-приложения с активной записью в основную таблицу, счётчики, очереди статусов. Приложения, где запись редкая и крупная (например, ночная пакетная загрузка), переносят ту же задержку RTT почти безболезненно — она добавляется один раз к операции, которая и так занимает секунды.

Отдельно стоит отметить: чтение при синхронной репликации обычно не платит эту цену вообще. Клиент, читающий с мастера, не ждёт никакой реплики — RTT ложится только на путь записи. Если приложение преимущественно читает и редко пишет, синхронная межстрановая реплика может оказаться совсем не такой болезненной, как кажется по обсуждениям в интернете, где обычно жалуются именно на нагрузки с активной записью.

Паттерн первый: синхронность внутри дата-центра, асинхронность наружу

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

Внутри одного дата-центра или региона держат синхронную реплику (или несколько) — RTT там низкий и предсказуемый, цена задержки на запись приемлема, а гарантия «данные подтверждённой транзакции существуют минимум в двух местах» реально нужна для защиты от локального отказа железа или виртуальной машины. Именно эта пара — мастер и синхронная реплика в одном ЦОДе — переживает большинство реальных отказов: сгорел диск, зависла нода гипервизора, произошёл сбой конкретного сервера.

В другие страны и регионы уходит асинхронная репликация — она не добавляет ни миллисекунды к скорости записи на мастере, но даёт то, чего синхронная реплика в соседней стойке дать не может: защиту от отказа всего дата-центра целиком (пожар, отключение электричества, потеря связности у провайдера в регионе) и возможность поднять сервис в другой стране, если основная площадка недоступна полностью. Такую схему часто называют комбинацией RPO≈0 внутри региона и RPO, равного отставанию реплики (обычно секунды), между регионами — осознанный компромисс, а не недоработка.

Мастер (Лондон, ЦОД A)
  ├─ синхронная реплика (Лондон, ЦОД B, тот же город) — низкий RTT, ждём каждый COMMIT
  └─ асинхронная реплика (Нью-Йорк) — не ждём, догоняет в фоне, страховка от отказа всего региона

При такой схеме трафик между своими серверами в разных странах всё равно идёт постоянным потоком — журнал изменений льётся на асинхронную реплику непрерывно, пока есть запись в базу, и стоит заранее прикинуть его объём и тарификацию у провайдера, а не удивляться счёту через пару месяцев. Это отдельно разобрано в статье про трафик между своими серверами в разных странах — там же про то, как некоторые провайдеры тарифицируют приватную связность между собственными дата-центрами иначе, чем обычный публичный egress, что может ощутимо снизить именно эту статью расходов.

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

Второй практический рычаг — не требовать подтверждения от одной конкретной реплики (тем более удалённой), а требовать подтверждения от большинства реплик из пула, где хотя бы часть находится близко. Общая идея кворумной записи проста: если у вас несколько реплик, транзакция считается зафиксированной не тогда, когда ответили все до единой, а когда ответило достаточное для кворума число — обычно больше половины.

Практическая польза именно для межстрановой схемы в том, что кворум можно набрать преимущественно из близких узлов, не дожидаясь непременно самой дальней реплики каждый раз. Например, при трёх репликах — две в своём регионе, одна в другой стране — кворум «любые 2 из 3» в среднем ждёт ответа от близких узлов и лишь изредка упирается в дальнюю, если один из близких временно недоступен. Это не устраняет физику RTT полностью, но снижает вероятность, что каждая конкретная запись платит полную межстрановую цену.

В PostgreSQL похожий эффект можно получить прямо в параметре synchronous_standby_names, без полноценного консенсус-протокола:

# ждать ЛЮБЫЕ 2 подтверждения из 3 перечисленных реплик,
# а не обязательно от конкретной удалённой
synchronous_standby_names = 'ANY 2 (replica_dc1, replica_dc2, replica_far_country)'

Более строгие кворумные схемы с формальным консенсусом (Raft, Paxos и их производные) устроены глубже и обычно живут не на уровне репликации одной СУБД, а на уровне распределённых систем хранения и кластерных СУБД, спроектированных изначально под многоузловой консенсус. Углубляться в конкретные алгоритмы здесь не будем — для практической архитектуры на PostgreSQL или MySQL достаточно понимать идею: кворум большинства почти всегда дешевле по задержке, чем ожидание ответа от каждой без исключения реплики, особенно если хотя бы одна из них далеко. Тот же принцип большинства — причина, по которой кластерным системам вообще нужно нечётное число узлов, что разобрано в статье про кворум в кластере и зачем нужен третий узел: split-brain и требование к кворуму устроены той же логикой, что и здесь, просто в другом контексте.

Архитектурное решение: где не тянуть синхронность

Главный практический вывод из всего вышесказанного не технический, а управленческий: синхронность — это решение, которое стоит принимать отдельно для каждого класса данных, а не для базы целиком. Одна и та же схема хранения может иметь и синхронно реплицируемые таблицы, и асинхронные — большинство современных СУБД это позволяет на уровне сессии или конкретной транзакции.

Прежде чем включать синхронную репликацию в другую страну, стоит ответить на три вопроса:

  1. Какие именно данные обязаны пережить отказ дата-центра без потери последней записанной транзакции? Обычно это узкий список: платежи, финансовые проводки, критичные состояния заказов. Основная масса записей в типичном приложении — профили, логи действий, счётчики просмотров — переживёт потерю последних секунд при катастрофе без реального ущерба бизнесу.
  2. Достаточно ли синхронной реплики в том же регионе, или нужна защита именно от отказа всей площадки? Если цель — пережить отказ конкретного сервера или диска, синхронная реплика в соседнем ЦОДе того же города закрывает эту цель почти без цены. Межстрановая синхронность нужна только тогда, когда вы осознанно защищаетесь от катастрофы уровня всего дата-центра или региона — и готовы платить за это на каждой записи.
  3. Какую деградацию пропускной способности записи бизнес готов принять ради этой гарантии? Ответ на этот вопрос не считается по формуле — его нужно измерить на своей реальной нагрузке и своей конкретной паре площадок, потому что RTT между конкретными странами и параллелизм конкретного приложения сильно влияют на итоговую цифру.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли ускорить RTT между странами более быстрым сервером или более дорогим тарифом?

Нет. RTT определяется расстоянием, маршрутом и числом промежуточных переходов сети между двумя точками — это физическое ограничение, а не характеристика конкретного сервера. Более дорогой тариф может дать более прямой или менее загруженный маршрут и тем самым немного снизить RTT, но не отменить его порядок величины, заданный расстоянием.

Стоит ли вообще ставить синхронную реплику в другой стране, если это так дорого?

Иногда да — если бизнес осознанно защищается от отказа всего региона или дата-центра и готов платить задержкой на каждую запись ради этой конкретной гарантии. Но чаще правильнее решение — синхронная реплика внутри региона плюс асинхронная в другую страну, а не наоборот.

Как измерить реальную цену до того, как включать синхронную репликацию в проде?

Поднять тестовую реплику в целевой стране, включить синхронный режим на копии нагрузки (или на части трафика через canary) и замерить задержку commit и пропускную способность записи на реальных запросах приложения, а не на синтетическом бенчмарке. Универсальной цифры для конкретной пары стран не существует — только измерение на своём маршруте.

Кворумная запись полностью решает проблему задержки?

Нет, она её смягчает, а не устраняет: если для кворума нужен ответ хотя бы одной дальней реплики (например, при отказе близких узлов), запись всё равно ждёт полный RTT до неё. Кворум снижает частоту, с которой запись платит полную межстрановую цену, но не гарантирует, что она никогда её не заплатит.

Что произойдёт с записью, если единственная межстрановая синхронная реплика станет недоступна?

Зависит от настройки. Без резервных кандидатов в списке синхронных реплик мастер зависнет на COMMIT, ожидая подтверждения, которого не будет. Поэтому в проде почти всегда держат больше одного кандидата в синхронном списке или используют встроенный таймаут-предохранитель, который временно откатывает поведение к асинхронному, если синхронная реплика не отвечает вовремя.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →