Привет, Хабр! Меня зовут Дмитрий, я работаю в компании Orion soft. Преимущественно занимаюсь проектированием и разработкой бэкендов различного уровня от низкоуровневых сервисов до масштабируемых API. Сегодня мои основные инструменты — языки Python и Go. Так как ранее плотно работал с системным программированием, очень люблю *nix и всё, что с ними связано.

В статье расскажу, почему классические подходы к сетям перестали работать в условиях масштабирования, как мы выбирали стек, с какими архитектурными и техническими ограничениями столкнулись на практике — и почему выгоднее доработать существующий Open Source, а не переписывать всё с нуля. Покажу, как мы шаг за шагом избавлялись от узких мест, оптимизировали работу с большими объёмами ACL, переносили критичные компоненты на Go, и что в итоге получилось в продуктивных инсталляциях.

Если вам интересно, как реально эволюционирует инфраструктура и почему MVP — это не всегда «быстро и грязно», а зачастую — про осознанный выбор компромиссов — добро пожаловать под кат! Перед вами наш путь разработки программно-определяемой сети (SDN) для платформы виртуализации zVirt.

Традиционный подход

Давайте начнём с простого примера. Наверняка многие из вас работали с системой виртуализации сети. Для этого нужно создать сеть, виртуальную машину, вывести её в сеть и настроить IP-адресацию. Пользователь оперируют понятиями: «виртуальная машина-1» (VM1), «виртуальная машина-2» (VM2) и сеть (Network), но под капотом всё устроено несколько сложнее.

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

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

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

Эти сложности и ограничения стали причиной поиска новых решений для виртуализации сетей. Управление VLAN-сегментами оказалось недостаточно гибким. Если настройку на гипервизорах ещё можно как-то автоматизировать, то для настройки коммутаторов часто приходится обращаться к сетевикам внутри компании. Например, чтобы разрешить прохождение трафика с нужными тегами (конфигурировать транки). Это не только замедляет процессы, но и выходит за рамки компетенций специалистов по виртуализации.

С масштабируемостью тоже есть сложности. Дело в том, что для каждого сегмента сети, VLAN использует уникальный идентификатор (ID), который представлен всего 12 битами. Это значит, что максимально можно создать только 4096 отдельных VLAN. А для крупных современных инфраструктур этого часто недостаточно. Кроме того, разные VLAN могут связываться между собой только на сетевом уровне — через маршрутизатор или L3-коммутатор. А поскольку изоляция реализуется на канальном уровне (политики доступа на этом уровне недоступны), гибко управлять доступом внутри VLAN невозможно. Например, нельзя поставить межсетевой экран между виртуальными машинами в одном сегменте. А значит, контролировать трафик можно только на выходе из VLAN, то есть на более высоком уровне сети.

В результате, если внутри сегмента окажется скомпрометированное устройство, оно сможет свободно рассылать вредоносный трафик всем остальным участникам сегмента — вплоть до организации DoS-атак.

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

А поскольку мы ориентируемся на Enterprise-сегмент, этот этап развития наступил и у нашей платформы. zVirt — это комплексное решение для управления средой виртуализации, которое возникло на базе Open Source проекта oVirt. Мы развиваем его уже более семи лет, поддерживаем комьюнити и контрибьютим в Open Source.

Виртуальная сеть

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

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

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

Нам было важно реализовать:

  • аналоги VLAN, а именно логические и широковещательные домены со всеми необходимыми функциями;

  • систему управления IP-адресацией (аналог IPAM), чтобы удобно работать с адресами внутри виртуальных сетей;

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

Для полноценного управления и контроля были нужны инструменты мониторинга, журналирования операций и удобный интерфейс управления. Кроме того, важно было обеспечить интеграцию с корпоративными протоколами маршрутизации, такими как BGP и OSPF, а также предоставить удобный API для автоматизации и внешнего управления.

Перед нами встал выбор: разрабатывать собственное решение или использовать готовое. Конечно, такие вопросы не решаются по щелчку. Мы проанализировали различные варианты, но в итоге остановились на стеке OVN/OVS, потому что в апстриме oVirt уже была реализована базовая интеграция с ним. И хотя на тот момент она была на уровне концепта и не отличалась стабильностью, это стало фундаментом для проведения первых тестов и дало необходимую возможность убедиться в работоспособности подхода.

Мы провели тестирование OVN/OVS отдельно от нашей системы виртуализации: проверили работу dataplane, прогнали нагрузочные тесты, сняли различные метрики производительности — и результат нас почти устроил.

Тестирование подтвердило, что OVN/OVS — это действительно стабильное и производительное решение, способное выдерживать серьёзные нагрузки. Оно соответствовало нашим требованиям по масштабируемости и гибкости. А благодаря поддержке со стороны сообщества и успешному опыту использования в крупных OpenStack инсталляциях мы были уверены, что сможем быстро вывести его в продакшн и при этом сохранить стабильность нашей платформы. Конечно, у него были и недостатки, но чтобы разобраться в них, давайте углубимся в устройство OVN. Начнём с принципиальных отличий от традиционной модели.

Open Virtual Network (OVN)

В традиционной модели построения виртуальных сетей каждая виртуальная машина подключается на гипервизоре к отдельному сетевому мосту (bridge), который связан с физическим сетевым адаптером сервера и далее с корпоративным коммутатором. В программно-определяемых сетях (SDN) виртуальные машины подключаются не к отдельному физическому или программному мосту, а к интеграционному виртуальному программируемому коммутатору Open vSwitch (OVS).

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

Внутри Open vSwitch выделяют три ключевых компонента:

  1. Модуль ядра (терминальный модуль) — это ядро, которое непосредственно занимается обработкой сетевого трафика: коммутацией, маршрутизацией, фильтрацией и применением других сетевых правил. Именно здесь происходит вся «черновая работа» по передаче пакетов.

  2. База данных правил — здесь хранятся все сетевые политики и инструкции в формате OpenFlow. Эти правила определяют, как должен обрабатываться трафик на каждом этапе.

  3. Демон управления — специальный сервис, который отвечает за то, чтобы актуальные правила из базы данных своевременно применялись в Data plane. Он следит за изменениями в политике и оперативно обновляет конфигурацию коммутатора.

Эта система обеспечивает гибкое и централизованное управление сетевым трафиком на уровне каждого гипервизора.

Конфигурация для Data plane поступает с более высокого уровня — из Control plane. В нашем случае это распределённые контроллеры OVN, которые запускаются на каждом гипервизоре, а также централизованная база данных (южная база, Southbound DB), где хранится текущее состояние всей сети. Эта база может быть реплицирована для отказоустойчивости, а контроллеры автоматически отслеживают изменения в инфраструктуре. Например, если какой-то сервер вышел из строя.

Над этим уровнем находится Management plane — слой управления, где оперируют уже более привычными для пользователя сущностями: логическими коммутаторами, группами портов, настройками безопасности и так далее. Данные о желаемом состоянии сети попадают сюда из северной базы данных (Northbound DB) и транслируются через специальные сервисы в южную базу.

Именно Northbound DB является точкой интеграции OVN с внешними системами управления, такими как oVirt или zVirt. Например, в классической интеграции с oVirt используется промежуточный сервис ovirt-provider-ovn, который связывает ядро системы виртуализации с OVN и предоставляет совместимость с OpenStack Neutron API.

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

Интеграция OVN

Мы подробно разобрали внутреннее устройство OVN, и теперь пришло время вернуться к недостаткам, выявленным в ходе тестирования.

На тот момент интеграция OVN/OVS с oVirt поддерживала только базовые сетевые функции: отсутствовали такие важные возможности, как трансляция адресов, вывод трафика наружу, журналирование, зеркалирование и ряд других функций, которые пользователи ожидают от современного SDN-решения.

В классической схеме для интеграции OVN с системой виртуализации используется сервис ovirt-provider-ovn. Это Python-приложение, которое с помощью библиотеки ovs-python подключается к северной базе данных OVN, хранит её актуальное состояние в памяти и обновляет его при изменениях. А также предоставляет интерфейс взаимодействия, совместимый с OpenStack Neutron API, что позволяет использовать стандартные инструменты для управления сетью.

Однако в архитектуре oVirt точкой интеграции для клиента выступает oVirt Engine — большой монолит, который объединяет API, UI и логику управления. Мы же стремились к более модульной архитектуре, где каждый сервис отвечает за свою часть функциональности. А интеграция с монолитом на 500 тысяч строк добавляла ненужную связанность и риски. Из-за неё усложнялась синхронизация с Upstream-версией и поддержка обратной совместимости при кастомизации, а гибкость системы снижалась.

Кроме того, нас не устраивал сам подход к управлению сетями на уровне OpenStack Neutron API. Он изначально проектировался для облачных платформ с поддержкой мультитенантности и сложных моделей изоляции, тогда как oVirt (и наш zVirt) реализуют изоляцию на уровне инфраструктуры. Для большинства наших клиентов этого достаточно, а более сложные схемы можно реализовать на уровне внешних систем, не усложняя интеграцию с OVN.

В процессе работы мы столкнулись и с другими неудобствами. Например, группы безопасности и правила доступа приходилось настраивать не через привычный UI или API платформы, а напрямую через сервис провайдера, зачастую вручную или с помощью плейбуков из репозиториев OpenStack. Такой подход ещё приемлем для тестирования, но не подходит для повседневной эксплуатации в корпоративной среде.

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

В итоге мы исправили выявленные критические баги и даже контрибьютили большинство из них в апстрим и всё вроде бы было хорошо…

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

Его архитектура не была рассчитана на масштабные инсталляции: сервис работал в однопоточном режиме, и при большом количестве запросов остальные операции зависали в очереди. Мы оптимизировали часть логики на уровне приложения, выделили отдельный слой управления SDN в сервисе zvirt-engine-backend, который стал бэкендом для UI и API клиентов. Это позволило значительно ускорить работу, но при большом количестве объектов задержки всё равно оставались заметными. На большом количестве сущностей по-прежнему приходилось ждать 15 секунд.

Дальнейший анализ показал, что основная нагрузка падает на библиотеку, через которую сервис обращается к данным и грузит CPU.

При этом большая часть нагрузки приходилась на метод __getattr()__. В Python это обработчик операции доступа к атрибуту объекта, если атрибут не найден через обычный механизм (т.е. его нет в __dict__ объекта или в цепочке наследования). Данный метод в оригинальном коде библиотеки был переопределен таким образом, что внутри выполнялся большой пласт логики с конвертацией из одних типов в другие.

# получение динамического атрибута объекта 
uuid = rule.uuid
# приводит к вызову
uuid = rule.__getattr__(“uuid”)

def getattr(self, column_name):
    ...
    if column.type.is_map():
        dmap = datum.to_python(self._uuid_to_row)
        ...
        datum = data.Datum.from_python(column.type, dmap)
        ...
    return datum.to_python(self._uuid_to_row)

Данные хранились не в нативных Python-типах, а в прокси-объектах, и каждое обращение требовало дополнительных преобразований типов, которые прокатывались через данный метод. Логика приблизительно такая:

def row2rest(rule_wrapper: RuleWrapper):
    return {
        Mapper.SEC_GROUP_RULE_ID: rule.uuid,
        Mapper.SEC_GROUP_RULE_DIRECTION: rule.direction,
        ... # и еще 10 свойств
    }

def serialize_rules(raw_rules: List[RuleWrapper]):
    rules = []
    for rule in raw_rules:
        rules.append(row2rest(rule))
    return ujson.dumps(rules)

на 1 правило — 12 обращений к полям

Именно дополнительное преобразования типов данных при работе с правилами безопасности стали для нас узким горлышком. Например, для одного правила безопасности требовалось до 12 обращений к полям, а при тысячах правил это приводило к миллионам операций и высокой нагрузке на CPU. В результате эти конвертации блокировали выполнение практически всех остальных операций в системе.

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

Однако быстро стало ясно, что переработка библиотеки ovs-python — не самый подходящий путь. Она не только работала с OVN, но и реализовывала работу с протоколом OVSDB, а кэширующий слой является её ядром. То есть пришлось бы переписывать само ядро библиотеки. А это повлекло бы за собой проблемы с дальнейшей поддержкой и стабильностью низкоуровневого слоя.

Добавление дополнительных кэшей или отдельной базы данных мы рассматривали как план «Б». Потому что нам пришлось бы выделять отдельное хранилище для состояния сети, хотя OVN уже содержит собственную базу данных. Что, в свою очередь, привело бы к необходимости синхронизировать два источника данных, усложнять логику работы, добавлять триггеры и обработчики изменений. А это неизбежно привело бы к увеличению сроков реализации.

Нашим планом «А» стало переписывание важной части кода на другом языке. Мы сразу отказались от Java, несмотря на то, что на нём написан наш основной бэкенд: поддержка библиотеки ovsdb-client-library прекратилась около шести лет назад, и использовать её для наших задач было бы ненадёжно. В итоге остановились на Go, потому что он выглядел максимально таргетным решением для быстрого создания прототипа и оперативного тестирования производительности.

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

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

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

Соответственно, цепочка вызовов «до»:

Превратилась вот в такую «после»:

Весь уровень сложности по работе с данными мы инкапсулировали непосредственно в приложении. Для преобразования данных использовали мапперы, которые конвертировали нативные структуры OVN в подходящие форматы для сериализации через JSON-библиотеку. Это позволило минимизировать накладные расходы на преобразование типов и ускорить обработку данных.

Чтобы наглядно показать, как всё это выглядело на практике, напомню, каким был наш Proof of Concept на этапе, когда мы только начинали реализовывать собственный API.

У нас появился тот самый сервис zvirt-ovn, построенный на Go и libovsdb, в котором мы теперь реализовываем все новые фичи.

Мы перенесли в новый сервис всю критически важную функциональность, которая мешала эффективно работать с ovirt-provider-ovn. А в ближайшем будущем планируем полностью отказаться от использования ovirt-provider-ovn, но при этом сохранить обратную совместимость для работы с oVirt Engine.

Создание собственного SDN-слоя позволило гибко спроектировать внешние API управления — так, как этого требуют наши задачи и архитектурные принципы. При этом одной из целей было сохранить соответствие оригинальному дизайну API oVirt Engine, чтобы избежать несогласованности и проблем при интеграции. Новый API получился отзывчивым, с поддержкой параллельных вычислений и возможностью выполнять bulk-операции при управлении OVN. Это особенно важно, поскольку обновление правил, групп и других сетевых сущностей — ресурсоёмкая операция, которая может занимать значительное время. Благодаря возможности группировать изменения и отправлять их в OVN транзакциями, мы существенно ускорили работу и избавились от ситуации, когда работа с API превращается в «спорт на выживание» при больших нагрузках.

Несмотря на минимальную связанность компонентов нашей системы, иногда возникает необходимость получать данные или события непосредственно от ядра системы виртуализации. Например, при запуске виртуальной машины или создании новой сети. Для решения подобных задач мы реализовали подсистему хуков, которая лаконично вписалась в оригинальное ядро системы виртуализации. Теперь наш SDN-слой может полностью мониторить все важные события управления сетями. Когда из админки добавляется новая сущность, мы автоматически перехватываем событие, обновляем информацию в базах OVN и поддерживаем актуальное состояние сети. Это заметно упростило интеграцию с Upstream и повысило надёжность работы всей системы.

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

Заключение

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

Мы сделали акцент на создании отзывчивого API, который скрывает внутреннюю сложность. Это упростило интеграцию, сделало управление сетью удобнее и позволило масштабировать платформу под разные сценарии эксплуатации. Например, одна из наших крупнейших внедрённых систем, полностью работающих на SDN, насчитывает порядка 100 хостов с десятками тысяч ACL. Этот опыт помог выявить и устранить основные узкие места, особенно при работе с большим количеством правил безопасности. В то же время мы старались сохранить универсальность решения, чтобы оно подходило не только для крупных, но и для небольших инсталляций, позволяя строить гибкие сетевые топологии под различные задачи.

Практика показала, что для MVP выгоднее доработать критичные элементы существующего Open Source-решения, чем разрабатывать всё с нуля. Это экономит ресурсы и снижает риски, связанные с масштабированием. Для нас важно, чтобы система оставалась универсальной и подходила для широкого круга задач, чтобы zVirt был не просто «коробочным» продуктом с жёстко заданной архитектурой, а настоящим конструктором, позволяющим пользователям строить гибкие и адаптивные топологии под свои потребности.

Спасибо за уделенное время. Голосуйте за статью!

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