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

Привет! Меня зовут Валерий Симаков, я разработчик команды Network DPL в MWS Cloud Platform. Сегодня мы подробно поговорим о сетевой составляющей нашего облака, а именно об устройстве Data Plane (DPL) в Overlay-слое — том самом слое, который и отвечает за передачу пользовательского трафика. Мы разберём, как из разрозненных компонентов рождается единый, слаженный механизм передачи данных. Благодарю коллег из команды Network DPL за помощь в подготовке статьи.

Как соединить части и получить целое

На Хабре вышло уже немало материалов, посвящённых архитектуре облачной платформы MWS Cloud Platform: мы рассказывали об общем видении и выборе технологий, детально разбирали устройство физического Underlay-слоя и даже заглядывали внутрь самих виртуальных машин. Отдельного упоминания заслуживает и технология SRv6, которая лежит в основе нашей Overlay-сети. Однако до сих пор оставался без ответа важный вопрос: как все эти компоненты работают вместе, обеспечивая базовую связность виртуальных машин? 

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

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

Именно из этого стремления к простоте и родилась та связка QEMU, VPP и SRv6, о которой и пойдёт речь далее. Мы разберём, как этот механизм устроен и за счёт чего виртуальные машины в MWS Cloud общаются тихо, не спеша, без суеты, но в то же время быстро и с чётким пониманием, куда летит каждый пакет.

Монструм или терминоописание

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

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

DPDK (Data Plane Development Kit) — набор библиотек и драйверов для высокопроизводительной обработки сетевых пакетов в пространстве пользователя (user-space), минуя стандартный сетевой стек операционной системы. В нашем случае это отдельный плагин в VPP, выступающий мостиком между драйверами на хосте и самим VPP, позволяя напрямую взаимодействовать с сетевым оборудованием, забирая пакеты из кольцевых буферов сетевой карты без участия ядра и дорогостоящих системных вызовов.

VPP (Vector Packet Processing) — высокопроизводительный сетевой стек, работающий в пространстве пользователя. Его главное преимущество — векторная обработка сетевого трафика: вместо того чтобы обрабатывать каждый пакет по отдельности, VPP группирует их и применяет одни и те же операции сразу ко всей группе. В MWS Cloud Platform именно VPP является ядром Data Plane на каждом вычислительном узле.

Vhost-user — протокол, который связывает виртуальную машину (QEMU) и наше ядро (VPP). Он позволяет передавать сетевые пакеты напрямую через разделяемую память, минуя накладные расходы операционной системы. Благодаря vhost-user виртуальная машина общается с VPP через общую shared memory, избегая лишних копирований пакетов между процессами.

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

SRv6 (Segment Routing over IPv6) — технология, которая позволяет гибко управлять маршрутами трафика в виртуальной сети Overlay. Идея проста: путь каждого пакета кодируется прямо в его IPv6-заголовке в виде цепочки сегментов. Это избавляет нас от необходимости держать сложные таблицы маршрутизации на промежуточных узлах и делает сеть более масштабируемой, то есть никакого состояния на underlay.

NUMA (Non-Uniform Memory Access) — архитектура памяти в мультипроцессорной системе, при которой время доступа к разным областям оперативной памяти неодинаково. Процессор обращается к своей локальной памяти быстро, а к памяти другого процессора — заметно медленнее. 

Теперь, когда мы вооружились базовыми понятиями, можно переходить к самому интересному — как именно виртуальные машины общаются друг с другом и с внешним миром.

О чём говорят машины

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

Теперь важный момент, касающийся адресации. У виртуальной машины в MWS Cloud Platform может быть два IP-адреса. Чтобы лучше понять их суть, представьте, что наша виртуальная машина — это человек, у которого, подобно традиции некоторых культур, есть два имени:

  1. Внутренний IP-адрес — это как имя для близкого круга. Допустим, нашу виртуальную машину зовут Серёжа. Когда мы находимся в небольшой компании друзей (в рамках одной приватной облачной сети), достаточно просто окликнуть: «Серёжа!» — и нужный Серёжа отзовётся. Но попробуйте крикнуть «Серёжа!» в час пик на станции метро — вероятность, что к вам подойдёт именно тот Серёжа, о котором вы подумали, крайне мала. Внутренний IP-адрес уникален только в пределах вашей виртуальной сети в облаке и не виден из большого мира.

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

Когда мы говорим о пользовательском трафике, мы всегда имеем в виду IPv4

Несмотря на то что в статье часто упоминается IPv6, речь о нём идёт исключительно в контексте облачного Underlay-слоя. Причина проста: глобальное распространение IPv6 всё ещё оставляет желать лучшего — многие провайдеры до сих пор не предоставляют такую возможность конечному пользователю. Технически ничто не мешает упаковать пользовательский IPv6-пакет в IPv6-обёртку (так называемый IPv6-over‑IPv6) с SRv6-заголовком и точно так же успешно передать его по облаку, но пока реальность диктует нам работу с четвёртой версией протокола. Кто знает, может сразу придётся добавлять поддержку IPv8?

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

На основе фактора сетевого общения можно выделить 2 принципиально разных сценария: 

Когда VM общается с другой VM в рамках одной облачной сети

Для полноты картины сразу отмечу, что, если бы обе VM располагались на одном хосте, весь маршрут упростился бы буквально на пару шагов, но в остальном принцип остался бы неизменным.

Итак, два виртуальных «Серёжи» — VM 1 с адресом 192.168.5.4 и VM 2 с адресом 192.168.5.5 — принадлежат одной приватной сети. Организационно это означает, что на каждом Compute-хосте этой сети выделен отдельный VRF. VRF — это, по сути, виртуальный маршрутизатор со своей собственной таблицей маршрутизации. 

Благодаря VRF мы жёстко изолируем приватные сети друг от друга: даже если в соседней сети волею случая окажется точно такой же адрес 192.168.5.5, пакеты никогда не перепутаются, потому что живут в разных VRF. Такая изоляция работает не только в рамках одного сервера, но и во всём облаке, при этом совершенно не мешая гибко разносить виртуальные машины одной сети по разным хостам.

А теперь ключевой трюк, который делает всю эту магию возможной. Для наглядности представим в виде диаграммы:

Благодаря тому что SDN-агент (в нашей терминологии CPL) непрерывно следит за состоянием облака, VPP на Хосте 1 точно знает: VM 2 с адресом 192.168.5.5 находится на Хосте 2, и даже знает сам адрес хоста 2 в формате IPv6. Когда VM 1 хочет отправить сообщение VM 2, она формирует обычный IPv4-пакет и передаёт его локальному VPP. VPP, заглянув в свою таблицу (заботливо заполненную Network-agent по конфигурации из CPL), видит целевой хост, на котором расположена VM 2.

VPP Хоста 1 инкапсулирует исходный IPv4-пакет в новый IPv6-заголовок с SRv6-секцией, в которой прописан нужный путь через Underlay до Хоста 2. Фактически этот заголовок говорит: «доставь меня на Хост 2, а как доставишь — действуй по зашитому на нём локальному правилу». Готовый пакет уходит в оптоволоконные недра физической Underlay-сети.

Когда посылка достигает Хоста 2, его встречает местный VPP. Первым делом он смотрит: «Так, пункт назначения — я. Значит, снимаю внешний заголовок». После декапсуляции остаётся исходный IPv4-пакет, адресованный 192.168.5.5. VPP сверяется с внутренней таблицей маршрутизации того VRF, которому принадлежит приватная сеть наших машин, и видит, что этот адрес привязан к виртуальному интерфейсу VM 2. Последний рывок — пакет попадает внутрь VM 2. Цель достигнута.

Обратный путь абсолютно симметричен: VM 2 отправляет ответный пакет, VPP Хоста 2 упаковывает его в SRv6 с маршрутом до Хоста 1 и весь процесс повторяется в зеркальном отражении. Так, шаг за шагом, две виртуальные машины в разных концах дата-центра общаются так, будто сидят в одном коммутаторе.

Когда VM общается напрямую с интернетом

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

Итак, VM 1 формирует обычный IPv4-пакет, адресованный какому-нибудь внешнему серверу. VM 1 передаёт его в локальный VPP на Compute-хосте. VPP смотрит на адрес назначения и видит: это не сосед по приватной сети, не другая VM в облаке, а «внешний мир». Иными словами, l3-посылке пора отправляться в большое плавание за пределы нашего Underlay. Тогда сначала пакет проходит NAT’ирование, чтобы подменить приватный адрес на публичный. После чего, поскольку все дороги наружу лежат через пограничный узел, локальный VPP немедленно инкапсулирует IPv4-пакет в IPv6-обёртку с SRv6-заголовком, где в явном виде прописан маршрут до нужного Border-хоста, и передаёт его в физическую Underlay-сеть.

На Border-хосте работает такой же VPP. Получив датаграмму из Underlay, он снимает внешний IPv6-заголовок вместе с SRv6-секцией, извлекая исходный IPv4. Теперь ему нужно просто отправить пакет дальше — и он уходит в сторону интернет-провайдера уже в качестве классической IPv4-посылки.

Теперь обратный путь. Допустим, внешний сервер отвечает, и ответный пакет приходит на тот самый публичный IP-адрес, под которым скрывается VM 1. Он попадает на Border-хост. Здесь VPP по адресу назначения (публичному IP) мгновенно понимает, какой именно виртуальной машине он предназначался — эту информацию, как вы уже догадываетесь, заранее подготовил и раздал SDN-агент CPL. Дальше знакомый трюк: Border-хост упаковывает полученный IPv4-пакет в IPv6-заголовок с SRv6-маршрутом, ведущим точно на тот Compute-хост, где живёт VM 1. После он ныряет в Underlay, доезжает до нужного хоста, где локальный VPP снимает обёртку, проходит NAT (трансляцию сетевых адресов)  в приватный адрес и передаёт его прямиком внутрь виртуальной машины. Вуаля — круг замкнулся.

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

Теперь пора поговорить про устройство самих хостов, разобрав чёрный ящик отвёрткой. 

Откуда растут хосты

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

Border-хост 

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

На Border-хосте нет виртуальных машин — он целиком занят обработкой и маршрутизацией трафика. Программно его основу составляет VPP, работающий в связке с DPDK. При этом Border-хост видит одновременно две реальности: с одной стороны — интернет с его классическими IPv4-маршрутами, с другой — внутренний Underlay на базе IPv6.

Схематично это можно представить так:

Где RX-hardware — это аппаратная часть сетевого интерфейса (сетевой карты), отвечающая за приём пакетов, а TX-hardware — за передачу.

Основная задача Border-хоста — быть «переводчиком» на стыке двух миров: внешнего (интернет) и внутреннего (облачный Underlay MWS Cloud). Для каждого направления эта трансляция выглядит по-своему. Рассмотрим два ключевых сценария. 

Сценарий 1: входящий трафик из интернета

Когда пакет поступает на Border-хост из внешней сети, никакого SRv6-заголовка у него, разумеется, нет. Это обычный IPv4-пакет, адресованный публичному IP-адресу, закреплённому за одной из виртуальных машин в нашем облаке.

Первым делом VPP на Border-хосте анализирует адрес назначения пакета. Здесь в игру вступает CPL — наш централизованный SDN-агент, который в режиме реального времени отслеживает состояние всей облачной инфраструктуры. Именно CPL заранее «рассказал» Border-хосту, за каким публичным IP-адресом скрывается конкретная виртуальная машина и какой путь к ней нужно построить. Фактически Border-хост располагает полной картой соответствия для дальнейшего построения SRv6-цепочки. 

Дополнительно на Border-хосте расположен внешний Load Balancer, поэтому если пришедший пакет пришел на Virtual IP внешнего балансировщика, то далее сетевой пакет отправляется туда.

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

Сценарий 2: исходящий трафик в интернет

Во всех случаях, когда пакет приходит к Border-хосту из облачной Underlay-сети, это означает, что виртуальная машина отправляет данные во «внешний мир». Такой пакет приходит в SRv6-обёртке. Задача Border-хоста — выполнить декапсуляцию: снять внешний IPv6-заголовок и SRv6-секцию, освободив исходный IPv4-пакет. Затем чистый IPv4-пакет отправляется в сторону провайдера по стандартным правилам IP-маршрутизации.

Border-хост не подключён напрямую к интернет-провайдеру

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

Compute-хост

Физический сервер, на котором работают виртуальные машины. Если продолжить аналогию с многоквартирным домом, то compute-хост — это само здание с общей инфраструктурой: фундаментом (процессорные ядра), водопроводом и электричеством (оперативная память, сетевые интерфейсы). Каждая квартира в этом доме — отдельная виртуальная машина, изолированная от соседей, но пользующаяся общими ресурсами здания. Роль управляющей компании, которая следит за порядком и распределяет ресурсы, выполняет гипервизор QEMU/KVM. А чтобы жильцы могли быстро отправлять и получать «посылки» (сетевые пакеты), в доме работает высокоскоростной почтовый сервис — VPP, связанный с каждой квартирой через отдельный протокол vhost-user. 

Важно, что дом разбит на несколько подъездов (NUMA-узлов) и для максимальной эффективности каждый жилец привязан к определённому подъезду, чтобы не бегать через весь дом за своими посылками, но об этом мы поговорим позднее.

Схематично это можно представить так:

На первый взгляд схема может показаться запутанной, но давайте разбираться по порядку. Как уже рассказывалось в предыдущих материалах, Underlay-сеть MWS Cloud Platform целиком построена на IPv6 и активно использует технологию SRv6 (Segment Routing over IPv6). Соответственно, каждый физический хост в такой сети имеет уникальный IPv6-адрес.

Когда закодированный в SRv6 пакет поступает на входной сетевой интерфейс хоста, VPP первым делом анализирует его SRv6-заголовок. Он проверяет, не является ли данный хост конечной точкой (EndPoint) для этого конкретного сегмента маршрута. Если нет — значит, посылка предназначена другому хосту, и VPP, строго следуя правилам SRv6, обновляет заголовок и отправляет пакет дальше по цепочке сегментов.

Если же хост действительно конечная точка, начинается самое интересное: внешний IPv6-заголовок (вместе с SRv6-обёрткой) снимается и на свет появляется исходный внутренний IPv4-пакет. Но просто «освободить» его недостаточно — нужно понять, что с ним делать внутри хоста. Для этого в SRv6 используется Local SID — специальный сегмент, который описывает поведение непосредственно на этом узле. 

В наш Local SID заранее зашита вся необходимая информация: к какой виртуальной машине или группе машин направляется пакет, требуется ли ему предварительно пройти через NAT, попасть на внутренний Load Balancer, в каком изолированном домене маршрутизации VRF он должен перемещаться и даже на каком именно NUMA-узле его следует обрабатывать, чтобы соблюсти NUMA locality.

Итак, пакет попадает в назначенный ему VRF и дальше его судьба зависит от конкретного сценария:

  • NAT: если виртуальная машина общается с внешним миром, пакет сначала проходит трансляцию адресов, подменяя приватный адрес на публичный (или наоборот).

  • Load Balancer: если запрос адресован не конкретной VM, а сервису, балансировщик выбирает один из здоровых бэкендов и отправляет пакет к нему.

  • Прямая связность между VM: когда две виртуальные машины находятся в одной приватной сети, но на разных хостах, пакет сразу отправляется на целевой виртуальный интерфейс.

В любом случае после всех этих шагов мы оказываемся на финишной прямой: IPv4-адрес назначения нашего пакета совпадает с адресом, который носит виртуальный Ethernet-интерфейс внутри целевой VM (этот адрес, разумеется, равен внутреннему IP-адресу виртуальной машины).

Здесь нас и поджидает тот самый «мост» — протокол vhost-user, который связывает процесс VPP с конкретной виртуальной машиной. Дополнительно лишь укажу, что с точки зрения vhost-user терминологии, QEMU выступает в качестве сервера, а VPP подключается к нему как клиент (справедливости ради, в терминологии vhost-user протокола существует не только клиент-сервер, но и frontend-backend, правда этот аспект уже сильно выходит за рамки данной статьи). После согласования параметров соединения они начинают общаться через общую область памяти (shared memory).

VPP, обработав пакет, помещает его в разделяемую память и уведомляет «соседа». Гостевой агент (virtio-драйвер внутри QEMU/KVM) забирает его из общей памяти и передаёт его операционной системе виртуальной машины. Цель достигнута: сообщение доставлено из одного облачного уголка в другой, преодолев цепочку из физических и виртуальных устройств. На этом мы оставим наш пакет — ему ведь ещё лететь обратно, но это уже совсем другая история.

Итак, NUMA

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

  • NUMA (Non-Uniform Memory Access) — архитектура памяти в мультипроцессорной системе, при которой время доступа к разным областям оперативной памяти неодинаково. Процессор обращается к своей локальной памяти быстро, а к памяти, висящей на другом процессоре, — заметно медленнее. В облачном контексте этим «тормозом» пренебрегать нельзя, потому что он напрямую сказывается на скорости передачи данных.

  • NUMA Node — аппаратная группировка процессорных ядер, памяти и устройств ввода-вывода. Ядра, работающие в пределах одной ноды, имеют быстрый доступ к своей локальной памяти. Память соседних нод для них — удалённая, и каждый поход за данными туда обходится дороже.

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

  • NUMA Penalty — тот самый «штраф» за cross-NUMA взаимодействие, выражающийся в дополнительных тактах процессорного времени и возросших задержках. Для Data Plane, где счёт идёт на микросекунды, такие штрафы быстро складываются в неприемлемые потери производительности.

  • NUMA Locality — принцип организации работы, при котором каждый процесс или поток использует исключительно ту память, которая находится в его локальной NUMA-ноде. Цель — свести cross-NUMA взаимодействие к минимуму, а в идеале исключить его вовсе. Именно этот принцип мы строго соблюдаем при размещении VPP и QEMU на наших Compute-хостах.

К счастью, когда мы говорим о проекте VPP, стоит признать, что текущая реализация, находящаяся в open source, уже «с коробки» следует принципам NUMA Locality, насколько это возможно, и не нуждается в дополнительных доработках, например: VPP умеет в NUMA-осведомлённую агрегацию интерфейсов (bonding) с приоритетом локальных, NUMA-осведомлённое векторное выделение памяти и в целом выделение памяти с помощью готовых внутренних библиотек на необходимой NUMA-ноде — так называемый pmalloc. А это значит, что с нашей стороны была только необходимость использовать готовую базу, чтобы минимизировать penalty настолько, насколько это возможно, ведь полностью исключить невозможно, и далее мы разберём почему.

По степени внедрения NUMA locality можно выстроить следующую градацию:

«Полное» NUMA locality — это идеальный случай, при котором penalty равен нулю: процессор работает исключительно со своей локальной памятью и никаких перекрёстных обращений не происходит. «Частичное» — ситуация, когда penalty положительный, но мы сознательно идём на компромисс, стараясь удержать штраф в разумных пределах. «Отсутствует» — наихудший сценарий, когда cross-NUMA доступы происходят хаотично и бесконтрольно, порождая максимальный штраф; таких ситуаций в высоконагруженной сетевой подсистеме нужно избегать любой ценой.

О том, что NUMA penalty может быть весьма ощутимым, дополнительных исследований можно не проводить — их существует достаточно, причём на любой вкус, вот лишь один пример — статья на Academia.edu.

Казалось бы, вывод очевиден: всегда стремись к полному NUMA locality. Но есть нюанс, и его лучше представить графически:

Варианты прохождения сетевого трафика с точки зрения NUMA locality
Варианты прохождения сетевого трафика с точки зрения NUMA locality

Эта иллюстрация подтверждает предыдущую таблицу, и всё хорошо, но теперь несколько поменяем сценарий:

Пример частного случая обработки трафика
Пример частного случая обработки трафика

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

Выше, в главе про путь пакета, мы упоминали, что в Local SID SRv6 зашита информация о том, на каком именно NUMA-узле следует продолжать обработку трафика. Именно этот механизм позволяет принимать пакет сразу на DPDK-интерфейсе, локальном для нужной ноды, а не гонять его через межпроцессорные шины к соседнему узлу. Вдобавок наш SDN-агент CPL при создании виртуальной машины заранее знает, к какой NUMA-ноде её ресурсы будут привязаны, и инструктирует систему размещать ресурсы строго локально по отношению к этой VM.

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

Крякнем, плюнем и надёжно склеим скотчем

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

Первое — Upstream VPP. VPP — это огромный проект с живым и весьма дружелюбным сообществом, в котором рады доработкам и исправлениям независимо от региона и корпоративной принадлежности (Linux, привет!). Поэтому запереться в комнате с собственным форком — путь заранее провальный. Мы по нему и не пошли. Разумеется, у нас есть форк на корпоративном GitLab — так работает любой инженерный процесс. Если в процессе работы обнаруживается неисправность в оригинальном коде, её исправление подразумевает работу с upstream, то есть, например, создание pull request в оригинальный проект (в качестве домашнего задания можно проверить этот факт на https://gerrit.fd.io/r/, поискав почты в домене mts.ru). К сожалению, мы пока только формируем процесс работы с upstream, поэтому пушим не очень часто, но мы прикладываем усилия, чтобы это изменилось в ближайшее время.

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

Второе — персональные плагины. У VPP есть одна особенность, известная каждому, кто погружался в этот чудесный мир: часть плагинов реализована, скажем так, «для галочки». Да-да, load balancer и nat, речь про вас. Несложно догадаться, что в том состоянии, в котором оба этих плагина пребывают в upstream, использовать их в реальном облаке решительно невозможно. Load balancer нам пришлось серьёзно допиливать, хотя нет-нет да и проскальзывают мысли переписать всё с нуля. NAT же был реализован полностью с нуля и с оригинальным плагином имеет общее разве что название. 

Внимательный читатель тут же задастся вопросом: в предыдущем пункте вы говорили про развитие upstream, а теперь внезапно переписали и не поделились. Всё так. Эти плагины слишком сильно разошлись с оригинальным проектом, и в open source им, откровенно говоря, делать нечего — да и незачем: их архитектура завязана на наши внутренние механизмы, а для внешнего мира они просто неактуальны.

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

Сколько жмёшь

К сожалению, эта глава наступила. Почему, «к сожалению»? Потому что замер производительности — самая неблагодарная задача. Результаты зависят от методики, от ресурсов тестовых стендов, от версий инструментов, от положения Венеры в Меркурии, от фазы луны и, конечно, от того, с какой ноги сегодня встал тестировщик (ещё немного — и я проведу исследование на тему корреляции между результатами бенчмарков и временем в пути на работу). А ещё есть неприятный нюанс: продукт постоянно развивается, а значит, цифры, полученные вчера, сегодня уже могут потерять актуальность — из-за новой оптимизации, изменения количества worker-потоков VPP или состояния железа. Поэтому все числа, приведённые ниже, носят скорее ознакомительный характер и с высокой долей вероятности могут не совпадать с тем, что вы увидите на момент чтения этой главы. Но общее представление они дают, и это главное.

Compute-хост

Замеры проводились на реальной боевой ноде с процессором Intel Xeon Gold 6448H: два NUMA-узла, по 32 физических ядра на каждый плюс Hyper-Threading. Мы ничего не приукрашивали — использовали стандартные инструменты: iperf3 и netperf в режиме TCP_STREAM, а для тонких экспериментов привлекали сам VPP и dpdk-testpmd. В качестве внешнего референса взяли открытый отчёт DPDK.

Первый эксперимент — самый честный. Один worker VPP, привязанный к конкретному ядру, и от одной до четырёх пар виртуальных машин. Каждая VM сидит на своём ядре, все интерфейсы VirtualEthernet направлены к единственному worker-у. Никакой магии, просто «в лоб».

Пик — 36,4 Гбит/с на трёх парах VM. Четвёртая пара уже даёт насыщение: скорость чуть падает, потери растут. Вывод: один worker-поток VPP спокойно «кормит» три-четыре мощные виртуальные машины суммарным трафиком под 40 гигабит. Этого с запасом хватает для подавляющего большинства клиентских сценариев.

Чтобы было на что опираться, мы сравнили результаты с чистым L2-форвардингом на dpdk-testpmd. На мелких посылках, 64 байта, наблюдается полный паритет — 15,2 миллиона пакетов в секунду в обоих случаях. На крупных, 1500 байт, VPP ожидаемо отстаёт: 32,6 Гбит/с против 52,8 Гбит/с у testpmd. Ничего удивительного — testpmd просто перекладывает пакеты из порта в порт, а VPP вдобавок маршрутизирует, заглядывает в VRF и обслуживает vhost-user. Он не является узким местом, он просто делает гораздо больше полезной работы.

BBR вместо Cubic: приятный бонус

Гоняя тесты в пределах одного хоста, где физических потерь нет по определению, мы заметили странные всплески ретрансмитов. Оказалось, дело в алгоритме управления перегрузкой. По умолчанию на виртуальных машинах стоял Cubic. Мы переключились на BBR — и картина резко изменилась.

VPP + BBR даёт те же 36,4 Гбит/с, но ретрансмитов становится в восемь раз меньше, чем у Cubic. Testpmd с BBR и вовсе обнуляет потери. Мораль простая: включайте BBR на всех виртуальных машинах, которые активно работают с сетью. Это бесплатно и очень эффективно.

NUMA и изоляция ядер: когда физика важнее софта

Наши compute-хосты — это два NUMA-узла. Если клиентская и серверная виртуальные машины оказываются на разных узлах, по нашим замерам мы платим штраф около 10% пропускной способности. Именно поэтому мы и стараемся сводить NUMA penalty к нулю всеми доступными средствами — о чём, собственно, и говорили в предыдущей главе.

Но самое драматичное влияние на производительность оказывает изоляция процессорных ядер. Мы сравнили работу четырёх worker-ов VPP на одном хосте в двух конфигурациях. Без isolcpus, когда ядра остаются доступны операционной системе для любых фоновых задач, результат составил около 62 Гбит/с. С isolcpus, когда все ядра, кроме нулевого, полностью вычищены от посторонней нагрузки, — 88 Гбит/с. При этом дисперсия результатов падает в разы. Вывод однозначный: isolcpus — не администраторская прихоть, а суровая необходимость для максимальной производительности Data Plane.

Border-хост: встреча с большим интернетом

Теперь переходим к самому, пожалуй, интересному — border-хостам. На них нет виртуальных машин, только VPP с DPDK, который гоняет пакеты между внешними физическими интерфейсами и облачным Underlay. Мы прогнали через генератор TRex два типа нагрузки: мелкие 64-байтовые пакеты, чтобы проверить пиковую производительность в пакетах в секунду, и крупные 1472-байтовые, чтобы выжать максимум битрейта. Тестировали в двух режимах: single port, где трафик идёт с одного физического порта на другой, и both ports в полном дуплексе.

Для 64-байтовых пакетов в режиме single port картина такая:

До 12 миллионов пакетов в секунду потери держатся в пределах сотых долей процента. На 14 Mpps начинается заметная просадка.

Режим both ports, полный дуплекс:

На 7 Mpps на порт, что даёт суммарно 14 миллионов пакетов в секунду и 12,32 Гбит/с, потери пока ещё микроскопические — тысячные доли процента. На 8 Mpps происходит резкий рост до 4,4%. Вывод: Border-хост на мелких посылках уверенно держит 12–14 миллионов пакетов в секунду на порт без значимых потерь.

Для крупных пакетов в 1472 байта в режиме single port:

91 гигабит в секунду с потерями в одну десятитысячную процента. Дальше упираемся в физические ограничения порта и шины, но и без того видно, что Border-хост способен переварить под сотню гигабит на одном порту.

Наконец, полный дуплекс для крупных пакетов:

При 6 Mpps на порт, или суммарно около 146 Гбит/с полнодуплексного трафика, потери остаются ниже тысячной доли процента. На 7 Mpps наступает обвал. Но даже 146 Гбит/с с микроскопическими потерями — это очень солидный запас для любых реальных нагрузок, с которыми сталкивается пограничный узел облака.

Что-то кончается, что-то начинается

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

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

Эта статья носит прежде всего ознакомительный характер: многие темы остались без должного внимания, поскольку каждая заслуживает отдельного разговора. Тем не менее первая часть работы подошла к концу. Да, всё верно: это именно первая, теоретическая часть. Если она вызовет у читателей положительную реакцию, мы обязательно подготовим вторую, где на практике развернём упрощённую модель Overlay MWS Cloud Platform и покажем всё своими руками.

Приходите в сообщество MWS Cloud Platform, чтобы поделиться обратной связью об облаке или задать вопрос — в чате на вопросы пользователей отвечает команда разработки платформы.

Комментарии (0)