Skip to content

Instantly share code, notes, and snippets.

@vorlon001
Last active October 11, 2026 18:00
Show Gist options
  • Select an option

  • Save vorlon001/933d2807c27484f792928c195f38b564 to your computer and use it in GitHub Desktop.

Select an option

Save vorlon001/933d2807c27484f792928c195f38b564 to your computer and use it in GitHub Desktop.
Диалоги о рыбалке

Чтобы реализовать эту схему на 10 000 хостов, мы полностью отказываемся от модели «один VRF = один интерфейс» на стыке OVS и Linux. Вместо этого мы используем VLAN-ориентированный Trunk-интерфейс, заставляя ядро Linux разбирать локальные VLAN-теги прямо на лету, маппить их в сетевые пространства (VRF/Маршруты) и передавать демону FRR (Free Range Routing) для BGP EVPN SRv6 сигнализации.

Ниже представлена полная техническая реализация одного такого гипервизора.


1. Архитектурная схема датаплана на хосте

[ VM 1 (Тенант А) ]     [ VM 2 (Тенант Б) ]
 (Локальный tag=10)      (Локальный tag=20)
         \                       /
       [ Единственный OVS Мост: br-int ]
                       |
               ( veth-ovs [Trunk] )
                       |  <-- Передаются тегированные кадры (VLAN 10, 20)
               ( veth-linux )
                       |
        +--------------+--------------+  <-- Ядро Linux (VLAN Subinterfaces)

        |                             |
 [ veth-linux.10 ]             [ veth-linux.20 ]  
 (Управляется vrf-10)          (Управляется vrf-20)

        |                             |
 [ VRF Роутер: vrf-10 ]        [ VRF Роутер: vrf-20 ] 
        \                             /
         +------------+--------------+
                      |
            [ Демон FRR (BGP EVPN) ]  <-- Анонсирует RT/RD и маппит VLAN в SRv6 uSID
                      |
              ( Физический eth0 )  <-- Улетает чистый IPv6 (uSID подложка)

2. Пошаговая настройка Open vSwitch

OVS на хосте выполняет только роль «глупого», но очень быстрого патч-корда. Он принимает пакеты от ВМ, вешает на них локальный тег и выплевывает в магистральный кабель.

1. Создаем единственный интеграционный мост

sudo ovs-vsctl add-br br-int

2. Создаем Trunk-пару (магистральный кабель в ядро)

sudo ip link add veth-ovs type veth peer name veth-linux
sudo ip link set veth-ovs up
sudo ip link set veth-linux up

3. Подключаем конец кабеля в OVS в режиме Trunk (пропускает теги 10 и 20)

sudo ovs-vsctl add-port br-int veth-ovs trunks=10,20

4. Подключаем виртуальные машины с локальными тегами

sudo ovs-vsctl add-port br-int tap-vm1 tag=10
sudo ovs-vsctl add-port br-int tap-vm2 tag=20

3. Настройка ядра Linux (Парсинг VLAN, VRF и Политики)

Теперь ядро Linux должно принять тегированный трафик из veth-linux, разобрать теги, изолировать их по виртуальным роутерам (VRF) и подготовить для маршрутизации.

1. Включаем глобальный IPv6 форвардинг и поддержку SRv6

sudo sysctl -w net.ipv6.conf.all.forwarding=1
sudo sysctl -w net.ipv6.conf.all.seg6_enabled=1
sudo sysctl -w net.ipv6.conf.eth0.seg6_enabled=1

2. Создаем виртуальные роутеры (VRF) для каждого тенанта

sudo ip link add vrf-10 type vrf table 10
sudo ip link add vrf-20 type vrf table 20
sudo ip link set vrf-10 up
sudo ip link set vrf-20 up

3. Создаем VLAN-сабинтерфейсы на базе veth-linux (Парсинг тегов ядра)

sudo ip link add link veth-linux name veth-linux.10 type vlan id 10
sudo ip link add link veth-linux name veth-linux.20 type vlan id 20

4. Помещаем сабинтерфейсы в соответствующие VRF

sudo ip link set veth-linux.10 master vrf-10
sudo ip link set veth-linux.20 master vrf-20
sudo ip link set veth-linux.10 up
sudo ip link set veth-linux.20 up

5. Назначаем IP-адреса (Они станут Default Gateway для ВМ внутри своих VRF)

sudo ip addr add 192.168.1.1/24 dev veth-linux.10
sudo ip addr add 192.168.1.1/24 dev veth-linux.20  # Конфликта нет, так как они в разных VRF

4. Конфигурация FRR (Связующее звено: от VLAN до SRv6 uSID)

На таком масштабе никто не пишет маршруты руками. Мы запускаем FRR, который забирает маршруты из Linux VRF, упаковывает их в BGP EVPN, автоматически генерирует SRv6 uSID (Micro-SID) локэйторы и отправляет их на Route Reflector. Современный стек FRR поддерживает автоматическую оркестрацию SRv6 через конфигурационный файл /etc/frr/frr.conf:

! Глобальные настройки демона
frr version 10.1
frr defaults traditional
hostname hypervisor-5432
log syslog informational
no ipv6 forwarding
!
! Настройка SRv6 локатора для данного гипервизора (выделенный блок uSID)
segment-routing srv6
 locator MY-USID-LOCATOR
  prefix fc00:5432::/48
  format usid-f3216
!
! Настройка BGP и связывание с Route Reflector
router bgp 65000
 bgp router-id 10.0.0.5432
 no bgp default ipv4-unicast
 neighbor 2001:db8::RR1 remote-as 65000
 neighbor 2001:db8::RR1 update-source eth0
 !
 ! Включаем EVPN семейство для обмена маршрутами ВМ
 address-family l2vpn evpn
  neighbor 2001:db8::RR1 activate
  advertise-all-vni
 exit-address-family
!
! Виртуальный роутер Тенанта А (VLAN 10) -> Маппинг в SRv6
router bgp 65000 vrf vrf-10
 bgp router-id 10.0.0.5432
 !
 address-family ipv4 unicast
  redistribute connected
  sid vpn export MY-USID-LOCATOR
 exit-address-family
 !
 address-family l2vpn evpn
  rd 65000:10
  route-target export 65000:10
  route-target import 65000:10
 exit-address-family
!
! Виртуальный роутер Тенанта Б (VLAN 20) -> Маппинг в SRv6
router bgp 65000 vrf vrf-20
 bgp router-id 10.0.0.5432
 !
 address-family ipv4 unicast
  redistribute connected
  sid vpn export MY-USID-LOCATOR
 exit-address-family
 !
 address-family l2vpn evpn
  rd 65000:20
  route-target export 65000:20
  route-target import 65000:20
 exit-address-family

5. Как это работает от начала до конца (Жизненный цикл пакета)

  1. Отправка: VM 1 отправляет пакет на удаленный IP 192.168.2.50.
  2. Маркировка в OVS: Пакет залетает в OVS, где на него вешается локальный VLAN 10. Пакет уходит в транк veth-linux.
  3. Разбор в Ядре: Ядро Linux видит тег 10, сдирает его и перенаправляет пакет в виртуальный интерфейс veth-linux.10, который жестко привязан к роутеру vrf-10.
  4. Поиск uSID: Ядро заглядывает в таблицу маршрутизации vrf-10. Эту таблицу заполнил FRR, получив по BGP EVPN апдейт от удаленного хоста. Там написано: «До подсети 192.168.2.0/24 лети через SRv6 uSID fc00:1111:2222:0010::».
  5. Инкапсуляция: Ядро Linux оборачивает IPv4 пакет в IPv6-заголовок, где Destination IP равен полученному uSID (в нем зашиты ID удаленного хоста и ID удаленного VRF) и выплевывает его в eth0.
  6. Физическая фабрика (Underlay) коммутирует пакет как самый обычный IPv6.

За счет того, что OVS на каждом из 10 000 хостов управляет только парой строк конфигурации для своих локальных портов, а FRR изолированно обменивается BGP-маршрутами только тех тенантов, которые реально присутствуют на хосте, архитектура линейно масштабируется до миллионов ВМ. Для обеспечения высокой доступности (HA) на уровне хоста вы планируете использовать LACP bonding из двух физических интерфейсов (eth0 + eth1) под управлением ядра Linux (bonding/team), или хотите отдать резервирование аплинков на откуп OVS?


Чтобы связать глобальные параметры оркестратора (VNI, ID проекта, Имя ВМ) с локальным VLAN на конкретном гипервизоре, локальный агент хоста должен выполнять детерминированный алгоритм. Поскольку на одном гипервизоре в среднем находится не более 100–200 активных ВМ, главная задача алгоритма — динамически выделить уникальный для этого хоста тег (1–4094), жестко привязать его к метаданным проекта и обеспечить корректную очистку памяти при удалении ВМ.

Ниже представлен пошаговый алгоритм, структура базы данных (State) хоста и пример реализации на Python.


Схема работы алгоритма

[ Запрос от Оркестратора ] -> ( VNI, Project ID, VM Name )
                                     |
                                     v
                       +---------------------------+

                       |   Проверка локальной БД   |
                       |       (State хоста)       |
                       +---------------------------+
                               /           \
                 (Проект уже есть)       (Проекта еще нет)
                             /               \
                            v                 v
           [ Вернуть существующий VLAN ]   [ Выделить свободный ID из пула ]

                            |                 |
                            |                 v
                            |      [ Создать Linux VRF, sub-interface ]

                            |                 |
                            v                 v
                 +-----------------------------------+

                 |  Добавить порт ВМ в OVS c тегом   |
                 +-----------------------------------+

1. Структура локального состояния (State хоста)

Локальный агент хранит в оперативной памяти (или легковесной БД вроде SQLite/BoltDB) структуру данных для отслеживания утилизации ресурсов.

  • Пул свободных VLAN (vlan_pool): Массив целых чисел от 10 до 4090 (первые и последние теги обычно резервируются под системные нужды).
  • Карта проектов (project_to_vlan): Ключ: VNI или Project_ID $\rightarrow$ Значение: Локальный VLAN.
  • Счетчик ссылок (vlan_ref_counters): Ключ: Локальный VLAN $\rightarrow$ Значение: Количество активных ВМ этого проекта на хосте.

2. Шаги алгоритма при деплое ВМ (Выделение VLAN)## Шаг 1: Идентификация проекта

Агент получает параметры новой ВМ. Он проверяет карту project_to_vlan по уникальному VNI проекта.

  • Сценарий А (Проект уже живет на хосте): Если для этого VNI уже назначен локальный VLAN (например, VLAN 42), алгоритм берет его.
  • Сценарий Б (Новый проект для этого хоста):
  1. Из пула vlan_pool извлекается первое свободное число (например, VLAN 105). 2. Записывается маппинг: VNI_100200 -> VLAN_105. 3. Инициируется создание инфраструктуры в ядре Linux для VRF 105 и veth-linux.105 (команды из предыдущего шага).

Шаг 2: Учет сущностей

Агент инкрементирует (увеличивает на +1) vlan_ref_counters для выбранного VLAN. Это нужно, чтобы знать, когда проект полностью покинет хост.

Шаг 3: Конфигурация датаплана

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

  1. Создает интерфейс виртуальной машины.
  2. Проливает команду в OVS, привязывая порт к выделенному VLAN: ovs-vsctl add-port br-int tap_vm tag=LOCAL_VLAN.

3. Шаги алгоритма при удалении ВМ (Очистка ресурсов)

Когда оркестратор удаляет ВМ, агент обязан выполнить обратную процедуру:

  1. Найти локальный VLAN по VNI проекта.
  2. Удалить порт из OVS: ovs-vsctl del-port br-int tap_vm.
  3. Декрементировать (уменьшить на -1) vlan_ref_counters для этого VLAN.
  4. Проверка на zero-refs: Если счетчик ссылок стал равен 0 (это была последняя ВМ проекта на данном сервере):
  • Удалить VLAN-сабинтерфейс и Linux VRF из ядра, чтобы не засорять память.
    • Удалить запись из карты project_to_vlan.
    • Вернуть номер VLAN обратно в пул vlan_pool для повторного использования другими проектами.

4. Программная реализация алгоритма (Python-псевдокод)

Этот скрипт наглядно показывает логику работы локального агента хоста. Для выполнения сетевых команд используются абстрактные вызовы системных утилит.

import sys
class HostNetworkAgent:
    def __init__(self):
        # Инициализируем пул локальных VLAN (от 10 до 4000)
        self.vlan_pool = set(range(10, 4001))
        
        # Локальное состояние (State)
        self.project_to_vlan = {}     # { vni: local_vlan }
        self.vlan_ref_counters = {}   # { local_vlan: count_of_vms }

    def allocate_vlan_for_vm(self, vm_name, project_id, vni):
        print(f"\n[Запрос] Деплой ВМ {vm_name} в проект VNI {vni}")
        
        # Шаг 1: Проверяем, существует ли уже локальный VLAN для этого VNI
        if vni in self.project_to_vlan:
            local_vlan = self.project_to_vlan[vni]
            print(f" -> Проект уже есть на хосте. Используем существующий локальный VLAN: {local_vlan}")
        else:
            # Шаг 2: Проекта нет, выделяем новый VLAN из пула
            if not self.vlan_pool:
                raise RuntimeError("Критическая ошибка: На хосте закончились свободные VLAN теги!")
            
            local_vlan = min(self.vlan_pool) # Берем наименьший свободный ID
            self.vlan_pool.remove(local_vlan)
            
            # Фиксируем маппинг в состоянии
            self.project_to_vlan[vni] = local_vlan
            self.vlan_ref_counters[local_vlan] = 0
            
            print(f" -> Новый проект на хосте. Выделен локальный VLAN: {local_vlan}")
            self._create_linux_kernel_vrf(local_vlan, vni)

        # Шаг 3: Увеличиваем счетчик ссылок и настраиваем OVS
        self.vlan_ref_counters[local_vlan] += 1
        self._add_ovs_port(vm_name, local_vlan)
        
        return local_vlan

    def deallocate_vlan_for_vm(self, vm_name, vni):
        print(f"\n[Запрос] Удаление ВМ {vm_name} из проекта VNI {vni}")
        
        if vni not in self.project_to_vlan:
            print(" -> Ошибка: Проект не найден в локальном стейте.")
            return

        local_vlan = self.project_to_vlan[vni]
        
        # Шаг 1: Удаляем порт из OVS
        self._del_ovs_port(vm_name)
        
        # Шаг 2: Уменьшаем счетчик ссылок
        self.vlan_ref_counters[local_vlan] -= 1
        print(f" -> ВМ удалена. Осталось ВМ в VLAN {local_vlan}: {self.vlan_ref_counters[local_vlan]}")
        
        # Шаг 3: Если ВМ этого проекта больше нет, сносим инфраструктуру L3
        if self.vlan_ref_counters[local_vlan] == 0:
            print(f" -> В проекте больше нет ВМ. Сносим локальный роутер и возвращаем VLAN {local_vlan} в пул.")
            self._destroy_linux_kernel_vrf(local_vlan)
            
            # Очищаем стейт
            del self.project_to_vlan[vni]
            del self.vlan_ref_counters[local_vlan]
            self.vlan_pool.add(local_vlan)

    # --- Системные вызовы датаплана (Имитация выполнения команд) ---
    def _create_linux_kernel_vrf(self, vlan, vni):
        print(f"   [EXEC] ip link add vrf-{vlan} type vrf table {vlan}")
        print(f"   [EXEC] ip link add link veth-linux name veth-linux.{vlan} type vlan id {vlan}")
        print(f"   [EXEC] ip link set veth-linux.{vlan} master vrf-{vlan}")
        print(f"   [EXEC] Конфигурация FRR для BGP EVPN SRv6 (VNI {vni} -> VLAN {vlan})")

    def _destroy_linux_kernel_vrf(self, vlan):
        print(f"   [EXEC] ip link del veth-linux.{vlan}")
        print(f"   [EXEC] ip link del vrf-{vlan}")

    def _add_ovs_port(self, vm_name, vlan):
        print(f"   [EXEC] ovs-vsctl add-port br-int tap-{vm_name} tag={vlan}")

    def _del_ovs_port(self, vm_name):
        print(f"   [EXEC] ovs-vsctl del-port br-int tap-{vm_name}")
# --- Демонстрация работы на симуляции ---agent = HostNetworkAgent()
# Прилетает первая ВМ Клиента А (VNI 100200)
agent.allocate_vlan_for_vm(vm_name="vm-alpha-1", project_id="proj-A", vni=100200)
# Прилетает вторая ВМ того же Клиента А
agent.allocate_vlan_for_vm(vm_name="vm-alpha-2", project_id="proj-A", vni=100200)
# Прилетает ВМ совершенно другого Клиента Б (VNI 555666)
agent.allocate_vlan_for_vm(vm_name="vm-beta-1", project_id="proj-B", vni=555666)
# Удаляем одну ВМ Клиента А (Локальный VRF должен выжить)
agent.deallocate_vlan_for_vm(vm_name="vm-alpha-1", vni=100200)
# Удаляем последнюю ВМ Клиента А (Локальный VRF и VLAN должны удалиться)
agent.deallocate_vlan_for_vm(vm_name="vm-alpha-2", vni=100200)

За счет этой логики оркестратору абсолютно плевать, какие VLAN-теги используются «внизу» на хостах. Он оперирует глобальным значением VNI (которое у нас идет в BGP EVPN и кодируется в SRv6 uSID), обеспечивая полную независимость слоев управления. Будет ли этот локальный стейт агента храниться строго в оперативной памяти (с синхронизацией при старте хоста путем сканирования текущих портов OVS ovs-vsctl list port), или вы планируете использовать персистентную локальную базу (например, хранить стейт в родной OVSDB или файле json)?


Для масштабирования на 10 000 хостов и 1 000 000 ВМ классический подход безопасности (stateful-фильтрация через Linux Conntrack / iptables на каждом veth-интерфейсе) полностью ломается. Причина — гигантское потребление CPU и RAM ядра на хранение миллионов состояний сессий (Connection Tracking Tables) [5.1]. На таком масштабе группы безопасности (Security Groups) реализуются по принципу Stateless ACL / Flow-based ACL напрямую внутри OVS. Локальный агент считывает глобальную декларацию политик в формате JSON, генерирует точные правила OpenFlow и проливает их в мост br-int. Ниже представлена структура JSON-манифеста, который глобальный оркестратор отправляет на хост, и алгоритм его применения.


1. JSON-манифест конфигурации ВМ и Групп Безопасности

Этот JSON содержит всё необходимое локальному агенту: параметры ВМ, её принадлежность к проекту (vni), выделяемые IP, а также Security Groups со списками входящих (ingress) и исходящих (egress) правил.

{
  "vm_id": "vm-alpha-88",
  "vm_name": "prod-api-v1",
  "project_id": "tenant-cluster-a",
  "vni": 100200,
  "mac_address": "52:54:00:12:34:56",
  "ipv4_address": "192.168.1.50",
  "ipv6_address": "fc00:100200::50",
  "security_groups": [
    {
      "sg_id": "sg-web-servers",
      "rules": [
        {
          "direction": "ingress",
          "protocol": "tcp",
          "port_min": 80,
          "port_max": 80,
          "remote_ip_prefix": "0.0.0.0/0",
          "action": "allow",
          "description": "Allow HTTP from anywhere"
        },
        {
          "direction": "ingress",
          "protocol": "tcp",
          "port_min": 443,
          "port_max": 443,
          "remote_ip_prefix": "0.0.0.0/0",
          "action": "allow",
          "description": "Allow HTTPS from anywhere"
        },
        {
          "direction": "ingress",
          "protocol": "tcp",
          "port_min": 22,
          "port_max": 22,
          "remote_ip_prefix": "10.50.0.0/16",
          "action": "allow",
          "description": "Allow SSH only from Management Network"
        },
        {
          "direction": "egress",
          "protocol": "any",
          "port_min": null,
          "port_max": null,
          "remote_ip_prefix": "0.0.0.0/0",
          "action": "allow",
          "description": "Allow all outbound traffic"
        }
      ]
    }
  ]
}

2. Как агент транслирует этот JSON в OpenFlow (OVS)

Вместо использования iptables, агент преобразует правила из JSON в таблицы потоков OpenFlow (ovs-ofctl). В OVS создается конвейер из нескольких таблиц (Pipeline).

  • Таблица 0: Базовая классификация (какой порт, какой локальный VLAN).
  • Таблица 10 (Egress ACL): Проверка трафика, который выходит из ВМ в сеть (защита от IP/MAC Spoofing).
  • Таблица 20 (Ingress ACL): Проверка трафика, который входит в ВМ из сети (фильтрация портов 80, 443, 22).

Алгоритм генерации команд OVS на основе JSON:

При деплое этой ВМ, агент (с помощью алгоритма из предыдущего шага) выяснил, что для vni: 100200 на данном хосте выделен VLAN 42, а порт виртуальной машины называется tap-vm88. Агент выполняет следующие системные вызовы для настройки безопасности:

=== 0. Защита от Spoofing (Разрешаем отправку только со своего IP/MAC) ===# Если ВМ пытается отправить трафик не со своего IP/MAC — дропаем на месте

sudo ovs-ofctl add-flow br-int "table=0, in_port=tap-vm88, dl_src=52:54:00:12:34:56, arp, arp_spa=192.168.1.50, actions=resubmit(,10)"
sudo ovs-ofctl add-flow br-int "table=0, in_port=tap-vm88, dl_src=52:54:00:12:34:56, ip, nw_src=192.168.1.50, actions=resubmit(,10)"
sudo ovs-ofctl add-flow br-int "table=0, in_port=tap-vm88, actions=drop"
# === 1. Обработка EGRESS (Исходящий трафик из ВМ в сеть) ===# Правило из JSON: Разрешить весь исход наружу (0.0.0.0/0)
sudo ovs-ofctl add-flow br-int "table=10, in_port=tap-vm88, ip, actions=NORMAL"
# === 2. Обработка INGRESS (Входящий трафик из сети в ВМ) ===# По умолчанию входящий трафик на этот порт перенаправляем в таблицу фильтрации 20
sudo ovs-ofctl add-flow br-int "table=0, dl_dst=52:54:00:12:34:56, actions=resubmit(,20)"
# Правило 1: Разрешить HTTP (Port 80) отовсюду
sudo ovs-ofctl add-flow br-int "table=20, dl_dst=52:54:00:12:34:56, tcp, tp_dst=80, actions=output:tap-vm88"
# Правило 2: Разрешить HTTPS (Port 443) отовсюду
sudo ovs-ofctl add-flow br-int "table=20, dl_dst=52:54:00:12:34:56, tcp, tp_dst=443, actions=output:tap-vm88"
# Правило 3: Разрешить SSH (Port 22) только из подсети подложки/менеджмента 10.50.0.0/16
sudo ovs-ofctl add-flow br-int "table=20, dl_dst=52:54:00:12:34:56, tcp, nw_src=10.50.0.0/16, tp_dst=22, actions=output:tap-vm88"
# Дефолтное правило для Таблицы 20: Всё, что не разрешено для этой ВМ — ДРОПАТЬ
sudo ovs-ofctl add-flow br-int "table=20, dl_dst=52:54:00:12:34:56, actions=drop"

Почему это решение масштабируется на 1 000 000 ВМ?

  1. Hardware Offloading Ready: Эти типы правил OpenFlow (совпадения по L3 IP и L4 Ports) являются классическими "Exact Match" и "Wildcard" флоу. Они нативно сбрасываются (offloaded) в ASIC современных сетевых карт (SmartNIC/DPU). Коммутация и фильтрация безопасности происходят прямо в чипе сетевой карты на скорости 100Gbps+ с нулевой утилизацией ядер CPU хоста.
  2. Отсутствие таблиц состояний (Stateless): Память хоста не забивается трекингом TCP-сессий. OVS просто сверяет заголовки каждого пакета с хэш-таблицей правил в памяти. Емкость таких таблиц в OVS (особенно при использовании Megaflows) позволяет держать сотни тысяч правил на один хост без падения производительности.

Если архитектура групп безопасности строго Stateless, для работы некоторых протоколов (например, FTP или пассивный режим некоторых БД) может потребоваться динамическое открытие портов. Будет ли достаточно чистого Stateless для ваших приложений, или мы добавим в схему OVS Conntrack зональную изоляцию (actions=ct), которая чуть "тяжелее", но реализует полноценный Stateful Firewall?


Вместо использования внешней базы данных (SQLite, Redis, JSON-файлы), которая может рассинхронизироваться с реальным состоянием сети при аварийной перезагрузке хоста, лучше всего хранить маппинг прямо внутри самой OVSDB. У OVS есть встроенная распределенная база данных (управляется через ovsdb-server), и в ней для каждой таблицы (например, Bridge, Port, Interface) предусмотрено специальное служебное поле — external_ids. Это ассоциативный массив (строковый словарь вида key=value), куда инженеры и оркестраторы могут записывать любые метаданные. Эти данные сохраняются на диске в /etc/openvswitch/conf.db и переживают любые перезагрузки хоста.

При масштабной архитектуре (10 000 хостов) есть два красивых способа организовать хранение этого маппинга.

Способ 1. Хранение маппинга на Trunk-порту (Рекомендуемый)

Поскольку мы связали OVS и ядро Linux через единый Trunk-кабель (veth-ovs), логично записывать маппинг вида Глобальный_VNI=Локальный_VLAN прямо в метаданные этого порта.

Как записать маппинг в OVSDB:

Когда ваш локальный агент выделяет VLAN 42 для проекта с VNI 100200, он выполняет команду:

sudo ovs-vsctl set Port veth-ovs external_ids:vni_100200="42"

Как это выглядит в OVSDB:

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

sudo ovs-vsctl get Port veth-ovs external_ids# Вывод: {vni_100200="42", vni_555666="105", vni_999999="312"}

Как агенту считать маппинг при перезапуске (Восстановление стейта):

Если локальный агент упал и перезапустился, ему не нужно опрашивать центральный оркестратор. Он просто делает один запрос к OVSDB и мгновенно восстанавливает карту проектов в своей оперативной памяти:

sudo ovs-vsctl get Port veth-ovs external_ids:vni_100200# Вывод: 42

Способ 2. Хранение метаданных на портах самих ВМ

Второй вариант — записывать VNI и Project_ID прямо в свойства виртуального порта (Interface или Port) конкретной ВМ. Это упрощает аудит безопасности и траблшутинг.

Как записать при деплое ВМ:

sudo ovs-vsctl add-port br-int tap-vm88 tag=42 -- \
  set Port tap-vm88 external_ids:vni="100200" external_ids:project="tenant-cluster-a" external_ids:vm_id="vm-alpha-88"

Как найти локальный VLAN по VNI (Парсинг базы OVSDB):

Если к агенту пришел запрос на удаление или деплой новой ВМ для VNI 100200, а локальная память пуста, он может использовать встроенный мощный поисковый движок ovs-vsctl с фильтрацией:

sudo ovs-vsctl --columns=tag find Port external_ids:vni="100200"# Вывод:# _uuid               : c29db972-7647-49db-89c0-5ba1d803673c# tag                 : 42

Агент парсит этот вывод, видит tag : 42 и понимает, что для этого VNI на хосте уже используется 42-й VLAN.


Плюсы хранения маппинга в OVSDB

  1. Атомарность: Создание порта ВМ и запись метаданных происходят в рамках одной транзакции к OVSDB (через операторы -- в команде ovs-vsctl). Не может возникнуть ситуации, когда порт создался, а запись в базу не долетела.
  2. Нулевой оверхед по памяти: OVSDB всё равно запущена и работает на хосте. Мы не тратим RAM на сторонние процессы баз данных.
  3. Готовый инструмент для дебага: Дежурный инженер сетевой службы может зайти на любой из 10 000 хостов и стандартной командой ovs-vsctl show сразу увидеть всю картину: какая ВМ к какому глобальному проекту относится и в каком локальном VLAN изолирована.

Для автоматизации работы с OVSDB из вашего локального агента (например, на Go или Python) вы бы предпочли использовать прямые вызовы утилиты ovs-vsctl (через CLI-обертки) или полноценное подключение по протоколу OVSDB (JSON-RPC сокет /var/run/openvswitch/db.sock), что работает гораздо быстрее при высоких нагрузках?


Сравнительная таблица: Архитектура, пределы масштабирования и ограничения

Технология + Протокол Как работает инкапсуляция и data-plane Пределы роста (Scaling Limits) Главные причины ограничений (Bottlenecks)
OVS + VXLAN MAC-in-UDP. Кадр L2 упаковывается в фиксированный заголовок 8 байт (VNI 24 бита). Обработка в ядре Linux через хэш-таблицы точных совпадений (Megaflows). ~500–1000 нод в кластере. 16 млн изолированных сетей (ограничение VNI). До 1000–2000 туннелей на одну ноду. 1. Ревалидация потоков: При росте сети ядро тратит слишком много CPU на обновление мегафлоу. 2. Мертвый оверлей: Из-за отсутствия метаданных в заголовке VXLAN, каждый хост должен заново просчитывать логику роутинга/ACL для транзитных пакетов.
OVS + Geneve Flexible заголовок + TLV-опции. Позволяет передавать метаданные контекста сети. Логика кэширования аналогична VXLAN, но парсинг переменного заголовка сложнее. ~1000–1500 нод (без OVN-оркестрации). 16 млн сетей. Пропускная способность падает на 5–15% сильнее по сравнению с VXLAN на старых CPU (без HW offload). 1. Нагрузка на CPU: Парсинг динамических TLV-полей в программном data-plane создает повышенную утилизацию процессора. 2. Сложность управления: Ручное ведение таблиц OpenFlow для Geneve-метаданных на больших масштабах становится невозможным без умного контроллера.
OVS + SRv6 IPv6 Extension Header. Кадр инкапсулируется в IPv6, пути жестко задаются через список 128-битных адресов (SRH) на виртуальном порту type=srv6. ~200–300 нод в классическом режиме. Глубина стека сегментов (SID List): обычно до 3–5 хопов без деградации. 1. MTU / Goodput Bottleneck: Каждый IPv6-адрес в SRH добавляет 16 байт. При глубоком стеке накладные расходы съедают полезную полосу (до 15% потерь трафика). 2. Ядро Linux: Программная обработка SRH в OVS Kernel Space не оптимизирована под сверхвысокие нагрузки PPS (пакетов в секунду).
OVN + VXLAN Контроллер транслирует логические сети в OpenFlow для OVS. Используется стандартный VXLAN. Распределенный роутинг и ACL урезаны из-за нехватки метаданных. Ограничено шлюзами: Редко используется для всего кластера, обычно масштабируется до сотен нод как мост к аппаратным VTEP. 1. Архитектурный тупик: Невозможность передать ID входного порта и контекст логического роутера в стандартном 24-битном VNI заставляет трафик ходить через центральные узлы (Chassis), создавая эффект "бутылочного горлышка".
OVN + Geneve Geneve с кастомными OVN TLV. Метаданные (Logical Datapath, Input/Output Ports) передаются внутри заголовка. ovn-controller на хосте вычисляет конвейер локально. 3000+ вычислительных узлов. Десятки тысяч логических портов. 16 млн логических сетей. 1. Центральная БД (OVDB Server): База Southbound DB при синхронизации тысяч нод упирается в память и CPU одного лидера (частично решено через Raft и инкрементальные апдейты). 2. Потребление RAM на нодах: На гигантских кластерах ovn-controller может потреблять гигабайты RAM для хранения копии логической топологии.
OVN + SRv6 IPv6-инфраструктура, где OVN маппит логические сущности на IPv6 SIDs, а трафик ходит через ядро Linux (LWT/seg6) или OVS-интерфейсы. ~500–1000 нод (в зависимости от топологии подложки IPv6). 1. Сложность трансляции: Трансляция динамических правил OVN (например, stateful Firewalls / Conntrack) в чистый SRv6 stateless-роутинг требует тяжелых обходных решений. 2. Слабая зрелость интеграции: Связка OVN с SRv6 развита хуже, чем с Geneve, что ограничивает её применение в крупных production-средах.
VPP + SRv6 (uSID) Векторная обработка (DPDK Userspace) пачками по 256 пакетов. Micro-SID (uSID) упаковывает до 6 инструкций маршрутизации в один стандартный 128-битный IPv6-адрес. 8 млн+ IPv6/SRv6 маршрутов в FIB (при 48GB+ RAM). 10 000+ нод (ограничено только BGP/IGP подложкой). Скорость проливки: десятки тысяч маршрутов/сек. 1. Доступная RAM хоста: Емкость таблиц маршрутизации линейно зависит от объема оперативной памяти выделенной под VPP-кучу (VPP Heap). 2. Выделенные CPU ядра: Для работы на Line-rate (100G+) VPP требует монопольного выделения (CPU pinning) нескольких ядер процессора, которые будут полностью заняты бесконечным циклом опроса (Polling Mode Driver).

Главные выводы по ограничениям (Кратко)

  1. Если у вас OVS/OVN (Geneve/VXLAN): Главный предел — это нагрузка на CPU (из-за прерываний ядра, ревалидации потоков или парсинга TLV) и лимиты центральной базы данных OVN при росте кластера выше 3000 нод.
  2. Если у вас Классический SRv6: Главный предел — падение полезной пропускной способности (Goodput) из-за гигантского размера заголовков (MTU Overhead) при добавлении каждого нового сегмента.
  3. Если у вас VPP + SRv6 uSID: Технология практически не имеет сетевых пределов роста (uSID убирает проблему MTU, а VPP убирает проблему производительности ядра). Главное ограничение здесь — исключительно стоимость железа (нужно выделять много оперативной памяти и отдельные ядра CPU под нужды сетевого стека).

Если вы проектируете архитектуру, подскажите: под какую конкретную задачу выбирается стек (например, крупный Kubernetes-кластер, High-Scale Telco-сеть 5G или мультитенсированная OpenStack-инфраструктура)? Я помогу подобрать оптимальное сочетание.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment