MAATRIX / Блог / 0,2 мс внутри дата-центра против 30 мс между странами: как это меняет архитектуру

0,2 мс внутри дата-центра против 30 мс между странами: как это меняет архитектуру

MAATRIX

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

Порядок величины: доли миллисекунды против десятков — это не просто цифры

Задержка между двумя серверами в одной серверной стойке или соседних стойках одного дата-центра — это доли миллисекунды на round trip: коммутатор, метры меди или оптики, минимальная обработка на сетевых картах. Точное число зависит от топологии сети конкретного ДЦ и загрузки коммутаторов, но порядок величины именно такой — доли миллисекунды, не единицы.

Задержка между серверами в разных странах или тем более на разных континентах — это уже другой порядок: физика распространения сигнала в оптоволокне и число промежуточных переходов маршрута дают десятки миллисекунд round trip, а для действительно дальних пар (разные континенты) — иногда больше. Механика этого предела — почему скорость света в кабеле работает против вас и как её прикинуть для конкретной пары точек — подробно разобрана в статье про трафик между своими серверами в разных странах: там же физическая формула, из которой понятно, что это не характеристика провайдера, а жёсткий пол, который не пробить более дорогим тарифом.

Важная оговорка сразу: конкретные цифры RTT для конкретной пары направлений нужно измерять ping или mtr на своём маршруте, а не брать из статьи. Здесь и дальше любые цифры — иллюстрация порядка величины ("доли миллисекунды" против "десятки миллисекунд"), а не гарантированный факт для вашей пары локаций. Но сама разница на два-три порядка — это не погрешность измерения, а предмет этой статьи: не абсолютные числа, а то, что происходит с архитектурой, когда эту разницу игнорируют.

Почему цепочка из тридцати вызовов внутри одного ДЦ "не болит"

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

Если все эти компоненты живут в одном дата-центре, суммарная задержка от такой цепочки остаётся приемлемой: десятки обращений по доли миллисекунды каждое складываются в единицы миллисекунд, редко больше. Это не значит, что архитектура хорошая — избыточные обращения к базе и лишние прыжки между сервисами всё равно тратят CPU, занимают воркеров и добавляют точки отказа, — но именно по времени отклика цепочка не создаёт проблемы, которую заметит пользователь. Разбор того, как TTFB (время до первого байта ответа) складывается именно из таких последовательных этапов обработки запроса — вычисления, база, внешние вызовы, — подробно разобран в статье про TTFB 800 мс при пинге 20 мс: там показано, что при низкой сетевой задержке узкое место обычно совсем не в сети, а именно в числе и характере таких внутренних обращений.

Здесь и кроется ловушка. Архитектура с большим числом "chatty"-обращений (от английского chatty — "болтливая", часто и много общающаяся) внутри одного ДЦ месяцами работает без нареканий к скорости. Инженеры видят, что производительность нормальная, и не видят повода пересматривать структуру взаимодействия — она же работает. Проблема проявится не сейчас, а в момент, когда эту архитектуру частично разнесут географически.

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

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

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

Та же цепочка между странами: как десятки вызовов превращаются в секунды

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

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

Ключевой момент — задержка не складывается один раз "за переезд в другой регион", она умножается на количество раундов в цепочке. Если сервис A вызывает сервис B, тот вызывает сервис C, а C делает ещё три запроса к базе — и всё это пересекает границу региона, RTT платится за каждый переход отдельно, а не один раз за факт "система теперь распределена". Именно поэтому "просто перенести один сервис в другой регион" в chatty-архитектуре — операция с ценой, растущей нелинейно относительно того, сколько компонентов физически переехало.

Практический принцип: "chatty" — вместе, "редкое и батчируемое" — можно разносить

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

Что обычно попадает в первую категорию (держать вместе): фронтенд/API-слой и сервис авторизации, дёргаемый на каждый запрос; приложение и его основная транзакционная база — синхронный путь записи и чтения на критичном пути ответа; сервис и кеш, который он читает синхронно на каждый запрос (Redis, Memcached в режиме read-through); цепочка внутренних микросервисов, последовательно вызываемая для сборки одного ответа (оркестрация в стиле "сервис A ждёт ответ B, чтобы вызвать C").

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

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

Как понять, что у вас chatty-взаимодействие, до того как разносить компоненты

Оценка "на глаз" здесь ненадёжна — распределённая система обрастает связями постепенно, и часто никто в команде не держит в голове полную картину того, сколько раз запрос пользователя пересекает границы сервисов. Три практических способа получить объективную картину до переезда части системы в другой регион:

Посчитать число сетевых обращений на один входящий запрос. Распределённая трассировка (Jaeger, Zipkin, любой APM-инструмент, строящий дерево вызовов запроса) прямо показывает, сколько внутренних вызовов порождает один запрос пользователя и какие из них последовательные, а какие параллельные. Дерево с 20-30 последовательными прыжками между двумя сервисами — явный кандидат на "держать вместе", независимо от того, что подсказывает интуиция.

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

Смоделировать перенос заранее. Перед физическим переносом компонента полезно добавить искусственную задержку между сервисами в тестовом окружении (tc netem на Linux умеет эмулировать задержку канала) и посмотреть, как ведёт себя суммарное время ответа и не начинают ли срабатывать таймауты, рассчитанные на локальную задержку. Это дешевле, чем находить проблему на проде после реального переезда.

Частая ошибка: перенос части системы в другой регион без пересмотра архитектуры

Самая распространённая причина, по которой команды наступают на эту грабли, — решение о географическом разнесении принимается отдельно от решения об архитектуре взаимодействия. Типичный сценарий: часть инфраструктуры переносят в другой регион "ради отказоустойчивости", "чтобы данные были ближе к части аудитории" или просто "по историческим причинам" — второй дата-центр когда-то появился в другом регионе провайдера, и туда постепенно перетащили часть сервисов. При этом сама цепочка вызовов между компонентами остаётся ровно такой же, какой была спроектирована для работы внутри одного ДЦ — с тем же числом синхронных обращений, тем же N+1 к базе, тем же оркестратором, который последовательно дёргает пять сервисов подряд.

Проблема в том, что "перенести сервер в другой регион" — операция инфраструктурная, а "пересмотреть, какие вызовы между компонентами синхронные, а какие можно сделать асинхронными или батчевыми" — операция архитектурная, требующая понимания кода и бизнес-логики, а не только DevOps-доступа к серверам. Первую можно сделать за один спринт силами инфраструктурной команды. Вторая требует ревизии межсервисных контрактов и, возможно, переписывания части логики под асинхронную модель — совсем другая по времени задача. Соблазн ограничиться только первой понятен, но именно это и создаёт описанную выше арифметику: та же chatty-цепочка, только с ценой на порядки выше. Похожий по сути антипаттерн — перенос системы в облако методом lift-and-shift, "как есть", без пересмотра архитектуры под новую среду — разобран в статье про переезд в облако без смены архитектуры: перенос сам по себе не решает и не создаёт архитектурных проблем — он их либо консервирует, либо, как в случае с геораспределением, делает заметно дороже по конкретному ресурсу — задержке.

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

Практические паттерны для распределённой архитектуры

Если географическое разнесение части системы неизбежно или уже сделано, есть набор приёмов, снижающих цену пересечения региональных границ без полного переписывания системы:

  • Агрегирующий слой (BFF, API Gateway). Вместо пяти последовательных запросов клиента к пяти бэкенд-сервисам в другом регионе — один агрегирующий сервис в регионе клиента собирает данные параллельными запросами. RTT между регионами это не убирает, но убирает его умножение на число прыжков.
  • Асинхронная модель для редких межрегиональных обращений — постановка в очередь вместо синхронного ожидания, там, где это позволяет бизнес-логика.
  • Кеш для данных, которые редко меняются, но часто читаются через границу региона — убирает RTT из горячего пути для большинства запросов, оставляя обращение к источнику только на промах кеша.
  • Таймауты, пересчитанные под новую задержку. Значения, унаследованные от "локального вызова внутри ДЦ", после переноса начинают срабатывать на легитимных, просто более медленных ответах.
  • Явная карта синхронных зависимостей каждого компонента — недорогой способ не потерять эту информацию к моменту, когда придётся выбирать географию.

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

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

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

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

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

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

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

Можно ли просто увеличить таймауты и оставить архитектуру как есть после переноса части системы в другой регион?

Это уберёт немедленные ошибки по таймауту, но не уберёт саму накопленную задержку — пользователь будет ждать секунды там, где раньше ждал миллисекунды. Увеличение таймаутов маскирует симптом, а не решает причину.

Насколько вырастет суммарная задержка, если перенести один сервис из цепочки в другой регион?

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

Если данные редко меняются, можно ли их не считать "chatty"-зависимостью?

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

Асинхронная репликация или очередь между регионами полностью снимает проблему RTT?

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

С чего начать, если непонятно, какие компоненты у нас chatty, а какие нет?

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

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

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

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