MAATRIX / Блог / Почему трафик между двумя вашими серверами в одной стойке тоже стоит времени

Почему трафик между двумя вашими серверами в одной стойке тоже стоит времени

MAATRIX

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

Почему «тот же зал» — не то же самое, что localhost

Разница между вызовом функции внутри одного процесса и сетевым запросом к соседнему серверу — разница на несколько порядков, даже если оба сервера физически рядом. Вызов внутри процесса — переход по адресу в памяти, наносекунды. Сетевой запрос — это данные, которые нужно упаковать в пакет, провести через сетевую карту, коммутатор, кабель, принять другой картой и провести через весь сетевой стек ОС на другой стороне. И так же в обратную сторону для ответа.

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

  1. Приложение отдаёт данные ядру (системный вызов send/write).
  2. Ядро формирует пакет, проходит сетевой стек (TCP/IP, возможно netfilter), кладёт его в очередь передачи сетевой карты.
  3. Сетевая карта сериализует пакет в электрический или оптический сигнал и передаёт его в кабель.
  4. Сигнал идёт по кабелю до коммутатора — конечное время, потому что сигнал распространяется не мгновенно.
  5. Коммутатор принимает сигнал, разбирает заголовки, решает, на какой порт слать дальше, и передаёт его.
  6. Сигнал идёт по второму отрезку кабеля до сетевой карты сервера-получателя.
  7. Сетевая карта принимает сигнал, собирает пакет, сообщает об этом ядру (прерывание или опрос).
  8. Ядро сервера-получателя проводит пакет через сетевой стек в обратном порядке и будит процесс, который ждёт данные.

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

Сетевая карта, коммутатор и кабель: путь по проводам

Начнём с чисто физической части пути — она самая наглядная и часто недооценённая.

Сериализация на сетевой карте. Прежде чем пакет попадёт в кабель, его нужно перевести из байтов в памяти в последовательность электрических или оптических импульсов — это называется сериализацией. Чем выше скорость линка (1G, 10G, 25G, 100G), тем быстрее идёт сериализация: время передачи пакета определённого размера падает пропорционально пропускной способности. Переход с 1G на 10G даёт не только больше пропускной способности, но и заметно меньшую задержку сериализации на каждый отдельный пакет.

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

Коммутатор — не бесплатная труба. Коммутатор реально обрабатывает каждый пакет: читает заголовок Ethernet, смотрит MAC-адрес назначения в таблице коммутации, решает, на какой порт переслать кадр. Это конечная работа с конечным временем. Есть два основных режима пересылки: store-and-forward — коммутатор принимает кадр целиком, проверяет контрольную сумму и только потом пересылает (надёжнее, но задержка растёт с размером кадра), и cut-through — пересылка начинается сразу после чтения заголовка назначения, не дожидаясь конца кадра (быстрее, но проверка целостности ложится на получателя). Большинство дата-центровых коммутаторов умеют оба режима, но в любом случае у ASIC-чипа коммутатора есть собственное время обработки — прогон пакета через внутреннюю коммутационную матрицу и очереди на портах. Это время у разных моделей разное, производитель обычно указывает его в спецификации в микросекундах, но точное значение зависит от модели, загрузки и конфигурации конкретного оборудования.

Если путь между серверами проходит не через один коммутатор, а через два-три (top-of-rack → агрегация → top-of-rack соседней стойки), время обработки на каждом складывается. Поэтому топология сети внутри дата-центра — вопрос не только отказоустойчивости, но и задержки: чем меньше «прыжков», тем меньше накопленное время на коммутаторах.

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

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

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

Стек операционной системы съедает не меньше, чем провод

Физическая часть пути — это меньшая часть истории. На практике на обеих сторонах, отправителя и получателя, ощутимую долю времени съедает работа операционной системы, а не провода и коммутаторы.

Когда сетевая карта получателя приняла сигнал и собрала кадр, она должна сообщить об этом ядру. Раньше это делалось только через аппаратные прерывания — на каждый входящий пакет CPU останавливал текущую работу и переключался на обработку прерывания, что при высокой частоте пакетов превращалось в заметную нагрузку на конкретное ядро (в top это видно как высокий %si — softirq). Мы разбирали этот механизм подробнее в статье про то, почему сетевая карта отбирает у сервера целое ядро — там же про то, как NAPI и опрос вместо прерывания на каждый пакет снижают эту нагрузку, но не убирают её полностью, а просто группируют обработку в пачки.

После этого пакет проходит через сетевой стек ядра: разбор Ethernet-, IP- и TCP-заголовков (проверка контрольной суммы, номера последовательности, состояния соединения), возможные правила netfilter/iptables/nftables, попадание данных в буфер сокета. Дальше нужно разбудить процесс, который ждёт эти данные, — это планировщик ядра и переключение контекста с того, что CPU делал до этого, на процесс-получатель. Переключение контекста не бесплатно: нужно сохранить и загрузить состояние исполнения, что означает потерю части кэшей CPU и промахи кэша уже в новом контексте. Подробнее — в статье про что такое context switch и сколько он стоит серверу, там же способы увидеть эти переключения через vmstat и pidstat.

То же самое в обратном порядке происходит на стороне отправителя при вызове send(): переход из пользовательского пространства в ядро, копирование данных в буфер сокета, прохождение стека вниз, очередь передачи на сетевой карте.

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

TCP и протоколы приложения добавляют свои раунды

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

Если соединение TCP новое, сначала нужен three-way handshake — три пакета (SYN, SYN-ACK, ACK) прежде чем можно отправить хоть байт полезных данных. Если поверх TCP работает TLS (а mTLS между внутренними сервисами всё чаще норма, а не исключение), добавляется ещё один раунд для установления защищённого канала. Прежде чем первый байт запроса долетит до сервиса, может пройти два-три полных round-trip'а, каждый из которых включает в себя весь путь выше: сериализация, коммутатор, кабель, стек ОС, и обратно.

Именно поэтому переиспользование соединений — keep-alive и connection pooling — так сильно влияет на задержку внутри дата-центра, даже когда серверы стоят рядом: разница не в скорости передачи данных (её с запасом хватает), а в количестве round-trip'ов на установление соединения заново для каждого запроса. Мы разбирали похожий эффект для внешнего трафика в статье про разницу между задержкой и полосой канала — хороший ping и медленный сайт объясняются именно количеством последовательных round-trip'ов, а не нехваткой пропускной способности. Внутри дата-центра логика та же, только масштаб задержки одного перехода меньше, а количество переходов в цепочке микросервисов — больше.

Отдельно стоит сказать про TCP delayed ACK и алгоритм Нейгла — механизмы, включённые по умолчанию в большинстве стеков, чтобы не заваливать сеть мелкими пакетами. Для обычного интернет-трафика это разумная экономия. Но для внутрисервисного RPC с мелкими запрос-ответами их взаимодействие иногда добавляет заметную для внутреннего трафика задержку, если один механизм ждёт подтверждения, а другой — накопления данных для отправки. Для латентно-чувствительных внутренних соединений имеет смысл явно отключать Нейгла (TCP_NODELAY) на уровне приложения — многие современные gRPC- и HTTP-клиенты делают это по умолчанию, но не все, и стоит проверить конфигурацию, а не полагаться на предположение.

Как микросекунды превращаются в миллисекунды в цепочке микросервисов

Отдельный round-trip между двумя серверами в одной стойке — величина, которую в изоляции легко списать со счетов. Проблема начинается, когда таких round-trip'ов в обработке одного пользовательского запроса не один, а десять, двадцать, пятьдесят — что для архитектуры из множества мелких микросервисов совершенно обычная ситуация.

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

Хуже, если в цепочке есть избыточные вызовы «на всякий случай» — сервис A спрашивает у сервиса B то, что B мог бы отдать сразу вместе с основным ответом, но архитектурно этого не сделал. Каждый такой лишний round-trip — это ещё один полный проход через весь путь из этой статьи: сериализация, коммутатор, кабель, стек ОС с обеих сторон, возможно TCP-хендшейк, если соединение не переиспользуется.

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

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

Что реально снижает внутреннюю задержку в дата-центре

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

  • Переиспользуйте соединения. Keep-alive и connection pooling между внутренними сервисами убирают повторные TCP- и TLS-хендшейки на каждый запрос — это обычно даёт больший эффект, чем любая оптимизация физического уровня, потому что убирает целые round-trip'ы, а не сокращает время одного.
  • Сокращайте число последовательных прыжков. Там, где вызовы можно выполнить параллельно, а не строго последовательно, распараллеливание убирает накопление задержки по цепочке. Там, где данные можно отдать сразу вместе с основным ответом вместо отдельного запроса, это тоже убирает round-trip целиком.
  • Следите за размещением сервисов. Чем меньше сетевых прыжков между двумя часто общающимися сервисами, тем меньше накопленное время обработки на коммутаторах. Если платформа оркестрации это позволяет, разумно учитывать топологию при размещении тесно связанных сервисов.
  • Отключайте Нейгла для латентно-чувствительного RPC. Для внутренних протоколов с мелкими запрос-ответами TCP_NODELAY часто стоит включить явно, если библиотека не делает этого сама.
  • Не путайте это с проблемой пропускной способности. Если внутренний канал перегружен, задержка растёт по другой причине — из-за очередей на портах, а не из-за физики распространения сигнала, и мониторить нужно очереди и утилизацию портов, а не протокол.
  • Используйте unix-сокеты, если сервисы реально на одном хосте. Если два «сервиса» работают в разных контейнерах одной машины, полноценный сетевой стек через loopback избыточен — unix-сокет между процессами убирает и физическую часть пути, и значительную часть накладных расходов стека.

Ни одна из этих мер не убирает задержку до нуля — физику и работу ОС отменить нельзя. Цель — убрать звенья цепочки, добавленные архитектурой без необходимости: лишние round-trip'ы, пересоздание соединений, синхронные цепочки там, где можно распараллелить.

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

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

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

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

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

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

Насколько велика задержка внутри одной стойки на практике?

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

Если серверы в одной стойке, зачем вообще думать про эту задержку?

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

Поможет ли переход на 10G или 25G вместо 1G?

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

Есть ли смысл использовать RDMA для внутреннего трафика?

Для задач с действительно критичной задержкой (распределённые базы, HPC) технологии вроде RDMA, обходящие часть стека ядра, дают измеримый выигрыш, но это отдельный уровень сложности инфраструктуры. Для типичного HTTP/gRPC-взаимодействия между сервисами эффективнее сначала убрать архитектурные накладные расходы — лишние вызовы, отсутствие keep-alive.

Как понять, где именно теряется время в цепочке вызовов?

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

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

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

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