# Мультитенантность на ноде HPNN: перечень необходимых изменений **Дата:** 2026-08-17 **Зачем:** в ПРОД на одну ноду HPNN (маршрутизация + балансировка) размещается **100 и более тенантов**. Тенант — изолированная сущность, аналог VRF: своё адресное пространство, свои сегменты, свои листенеры и пулы. Адреса разных тенантов **могут совпадать**. **Основание:** фактическое состояние стенда на 2026-08-17 (шаг 3). **Смежные документы:** [OVS_DPDK_MIGRATION.md](OVS_DPDK_MIGRATION.md), [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md), [MAGLEV_SLOT_TABLE.md](MAGLEV_SLOT_TABLE.md). ## 1. Исходное состояние: что жёстко привязано к одному тенанту | Место | Как сейчас | Почему не масштабируется | |---|---|---| | `lb/topology.env` | плоский список адресов, MAC и `ofport` | нет понятия тенанта; на 100 тенантов файл нечитаем и неуправляем | | Таблица 20 (маршрутизация) | три префикса `10.20/24`, `10.30/24`, `192.168.5/24` без признака владельца | при совпадении адресов у двух тенантов пакет уйдёт «не туда» | | Таблица 21 (adjacency) | матч только по `nw_dst` | то же | | Таблица 25 (FDB) | матч `reg0` + `eth_dst`, где `reg0` — номер сегмента (1/2/3) | всего три значения; сегменты разных тенантов не различаются | | `ct` | **единственная зона** `CT_ZONE=1` (13 правил с `zone=1`) | записи тенантов с одинаковыми адресами столкнутся в одной таблице соединений | | Таблица 11 (слоты) | один пул, `POOL_ID=1`, 1024 правила | один пул на ноду | | `hc/` (демон) | конфиг с одним `pool_id`, одним `slots`, одним списком `members` | нет модели пулов и тенантов | | Health-пробы | два адреса в ядре узла (`10.20.0.253`, `10.30.0.253`) | при совпадающих подсетях тенантов один стек ядра не может держать одинаковые адреса | | `lb/apply.sh` | `replace-flows` — перезаливка **всего** пайплайна | правка одного тенанта затронет всех | | ARP/ICMP-респондеры | правило на адрес без признака тенанта | VIP разных тенантов могут совпадать | Всего в мосту сейчас **1141 правило** (105 пайплайна + 1024 слота + записи `learn`). ## 2. Ключевая проблема — пересечение адресных пространств Два тенанта с `10.20.0.0/24` должны работать независимо. Отсюда правило, которому обязан удовлетворять каждый матч в пайплайне: > **Ни одно правило не имеет права матчить IP или MAC без идентификатора > тенанта.** Это касается таблиц 5, 6, 10, 12, 20, 21, 24, 25 и всех записей, создаваемых действием `learn`. ## 3. Целевая модель датапаса ### 3.1. Идентификатор тенанта `reg3[0..15]` — `tenant_id` (16 бит, до 65535 тенантов). Проставляется **в таблице 0** по входному порту вместе с номером сегмента: ``` table=0,priority=200,in_port=<порт ВМ> actions=load:->NXM_NX_REG3[0..15], load:->NXM_NX_REG0[], , goto_table:1 ``` Проверено на стенде: `metadata` (64 бита) и `write_metadata` тоже принимаются — их стоит оставить под транспортный уровень (VNI/VLAN), а `tenant_id` держать именно в регистре, потому что из регистра его берёт `ct` (см. 3.3). Распределение регистров после изменения: | Регистр | Назначение | |---|---| | `reg0` | сегмент внутри тенанта | | `reg1` | номер слота (из `multipath`) | | `reg2` | идентификатор члена пула | | `reg3[0..15]` | **`tenant_id`** | | `reg3[16..31]` / `reg4` | `pool_id` | | `metadata` | резерв под VNI/VLAN и транспорт | ### 3.2. Что меняется в каждой таблице | Таблица | Изменение | |---|---| | 0 | порт → `tenant_id` + `segment_id`; `learn` FDB получает `reg3` в матч | | 1 | классификация по MAC шлюза дополняется `reg3`: у разных тенантов MAC шлюзов могут совпадать | | 5 | ARP-респондер: матч `reg3` + `arp_tpa`; рассылка — в порты сегмента **этого** тенанта | | 6 | ICMP-респондер: матч `reg3` + `nw_dst` | | 10 | листенер: матч `reg3` + `nw_dst` + `tp_dst`; `multipath` получает `basis = pool_id` (уже так) | | 11 | одна таблица на все пулы: матч `reg4=pool_id` + `reg1=slot` | | 12 | матч `reg3` + `reg2`; DNAT в адрес члена **этого** тенанта | | 15/16, 20/21 | матч `reg3` + `nw_dst` — LPM внутри VRF тенанта | | 24 | матч `reg3` + `nw_src` + `tp_src` | | 25 | FDB: матч `reg3` + `reg0` + `eth_dst`; рассылка — группой `type=all` на сегмент | | все | `priority=0 actions=drop` сохраняется как есть — это и есть изоляция по умолчанию | ### 3.3. Conntrack: зона на тенанта Единственная зона `CT_ZONE=1` не годится: при совпадающих адресах записи столкнутся. Решение — **зона из регистра**, синтаксис проверен на стенде (OVS 3.1 принимает): ``` actions=ct(commit,zone=NXM_NX_REG3[0..15],nat(dst=:),table=25) actions=ct(zone=NXM_NX_REG3[0..15],nat,table=25) ``` Зона — 16 бит, то есть `tenant_id` ложится в неё один к одному, и 100+ тенантов помещаются с запасом. Дополнительно необходимы **лимиты соединений на зону**, чтобы один тенант не исчерпал таблицу соединений всей ноды: ```bash ovs-appctl dpctl/ct-set-limits zone=,limit= ovs-appctl dpctl/ct-get-limits ``` ### 3.4. Рассылка и группы Список портов сегмента сейчас зашит в правило рассылки. При 100 тенантах и динамическом составе ВМ его придётся перегенерировать на каждое подключение. Замена — **OpenFlow-группа `type=all` на сегмент тенанта**: правило таблицы 25 ссылается на группу, а состав портов меняется независимо от правил. ## 4. Балансировка: пулы и таблица слотов Сейчас пул один: `POOL_ID=1`, `SLOTS=1024`, таблица 11 целиком принадлежит ему. Целевая схема — **одна таблица слотов на все пулы**, матч по паре `(pool_id, slot)`: ``` table=11,priority=100,ip,reg4=,reg1= actions=load:->NXM_NX_REG2[],goto_table:12 ``` Отдельная таблица на пул не годится: в OpenFlow всего 255 таблиц. **Оценка числа правил.** Замер на стенде: 1024 правила заливаются бандлом за **88 мс**. | Пулов на ноде | Слотов на пул | Правил в таблице 11 | Время полной перезаливки | |---|---|---|---| | 1 (сейчас) | 1024 | 1 024 | 0,09 с | | 100 | 1024 | 102 400 | ≈ 8,8 с | | 100 | 256 | 25 600 | ≈ 2,2 с | | 200 | 256 | 51 200 | ≈ 4,4 с | Отсюда два обязательных вывода: 1. **`SLOTS` должен стать параметром пула, а не ноды.** Для пула из 4–8 членов 1024 слота избыточны: 256 дают гранулярность веса 0,4 % и вчетверо меньше правил. 2. **Полная перезаливка недопустима.** `apply.sh` сейчас делает `replace-flows` всего пайплайна, а демон следом перезаливает слоты. При 100 тенантах это моргание всей ноды на секунды. Нужны **инкрементальные бандлы**: изменение состава пула затрагивает только правила своего `pool_id` (88 мс), изменение тенанта — только его правила. ## 5. Health-check: самое сложное место Демон `hcd` рассчитан на один пул и отправляет пробы из сетевого стека ядра (`net.Dialer.LocalAddr` на адресе internal-порта). При мультитенантности возникают три независимые задачи. **5.1. Модель.** Конфиг демона переходит от одного пула к списку: `tenant → pools[] → members[]`, у каждого пула — свои `pool_id`, `slots`, `health_monitor`, свой дайджест раскладки и свой источник проб. API `/status`, `/metrics`, `/drain`, `/enable` получают фильтр по тенанту и пулу. **5.2. Источник проб при совпадающих адресах.** Если у двух тенантов подсеть `10.20.0.0/24`, узел не может держать два одинаковых адреса `10.20.0.253` в одном сетевом стеке. Варианты, которые надо сравнить спайком: | Вариант | Суть | Цена | |---|---|---| | **Linux VRF** (`l3mdev`) | internal-порт тенанта помещается в `vrf-`, пробер привязывает сокет к VRF (`SO_BINDTODEVICE`) | 100+ VRF-устройств и таблиц маршрутизации на ноде; штатный механизм ядра | | **netns на тенанта** | internal-порт в отдельном namespace, пробер — отдельный процесс или горутина с `setns` | полная изоляция, но 100+ namespace и сложнее эксплуатация | | **Пробер с собственным стеком** | проба формируется и разбирается самим демоном (packet-out / AF_XDP / DPDK) | снимает зависимость от ядра и от tap на DPDK-ноде, но это отдельная разработка | На ноде с OVS-DPDK internal-порт — это tap, то есть медленный путь; 100+ tap плюс 100+ VRF требуют отдельной оценки (см. `OVS_DPDK_MIGRATION.md`, раздел 3.5). **5.3. Масштаб проб.** 100 тенантов × 10 членов при интервале 2 с = **500 проб/с** установленных TCP-соединений. Требуется: пул горутин с ограничением параллелизма, ограничение частоты, устойчивость к «шторму» переходов состояния (сейчас при любом изменении вызывается перезаливка — при 100 пулах нужен батчинг и дебаунс). ## 6. Control plane Статические `topology.env` и `hc-config.sh` на 100 тенантов неприменимы. Нужна декларативная модель — та самая, что заложена в дизайне v1 (§6), расширенная сущностью тенанта: ``` Tenant (VRF) ├── Segment (сегменты тенанта, порты ВМ, адрес шлюза, VIP-ы) ├── Route (префиксы VRF: LPM таблицы 20) └── Load_Balancer → Listener → Pool → Member └── Health_Monitor ``` Требования к агенту-рендереру: - **инкрементальность**: изменение одного тенанта не перегенерирует пайплайн целиком (см. 4); - **идемпотентность**: повторное применение не меняет датапас; - **атомарность на тенанта**: бандл на тенанта, а не на ноду; - **валидации §8.4 дизайна v1 в границах тенанта**: уникальность `(vip, protocol, port)` — теперь внутри тенанта, а не глобально; уникальность обратного пути `(member_address, member_port, proto)` — тоже внутри тенанта; - **распределение идентификаторов**: `tenant_id`, `pool_id`, `segment_id`, `ofport` и номера ct-зон выдаёт control plane, а не человек. ## 7. Оценка масштаба на 100 тенантов Ориентир при 10 ВМ и 2 листенерах на тенанта, 8 членов в пуле, 256 слотов: | Таблица | Правил | |---|---| | 0 — сегментация и обучение | ~2 000 (по два на порт ВМ) | | 5/6 — ARP и ICMP-респондеры | ~800 | | 10 — листенеры | ~200 | | 11 — слоты | **25 600** | | 12 — DNAT | ~800 | | 20/21 — маршрутизация и adjacency | ~1 500 + записи `learn` | | 24 — обратная трансляция | ~800 | | 25 — FDB | ~1 000 (`learn`) | | **Итого** | **≈ 33 000** | При 1024 слотах на пул — около 110 000, то есть **таблица слотов доминирует** и определяет и память, и время применения. Что ещё растёт линейно по числу тенантов: число портов моста (100 × 10 = 1000 vhost-user), число ct-зон, число VRF/netns для проб, число целей мониторинга. ## 8. Изоляция: что обязательно проверять Изоляция — не побочный эффект, а требование, которое нужно доказывать тестом на каждой сборке: 1. Два тенанта с **одинаковой** подсетью `10.20.0.0/24` и одинаковыми адресами ВМ обслуживаются независимо; трафик одного не появляется на портах другого. 2. Совпадающие VIP у разных тенантов работают одновременно. 3. `ct`-записи лежат в разных зонах: `dpctl/dump-conntrack` с фильтром по зоне. 4. Записи `learn` (FDB и adjacency) одного тенанта не применяются к трафику другого. 5. При отказе всех членов пула тенанта A трафик тенанта B не затронут (fail-close локален). 6. Исчерпание лимита соединений тенантом A не влияет на B (`ct-set-limits`). 7. Счётчики правил тенанта B не растут во время нагрузки на A. ## 9. Ресурсные лимиты и справедливость При 100 тенантах на одной ноде нужен механизм, не позволяющий одному тенанту занять всю ноду: - **новые соединения**: OpenFlow-метр на листенер (`new_conn_rate` из §4.6 дизайна v1) — особенно важно, потому что каждое новое соединение даёт upcall (см. `OVS_DPDK_MIGRATION.md`, раздел 6); - **соединения в ct**: `dpctl/ct-set-limits` на зону тенанта; - **полоса**: QoS на порт ВМ или на группу портов тенанта; - **правила**: верхняя граница числа членов и слотов на тенанта — иначе один тенант раздует таблицу 11. ## 10. Наблюдаемость - Метрики `hpnn_*` получают лейблы `tenant` и `pool`; сейчас есть только `member`. - `lbctl health|slots|fdb|conns` получают фильтр по тенанту — иначе вывод на 100 тенантов нечитаем. - Дайджест раскладки считается **на пул**, а не на ноду. - Отдельная метрика на тенанта: число ct-записей, upcall rate, отказы проб, срабатывания лимитов. ## 11. Изменения по файлам | Файл | Действие | |---|---| | `lb/topology.env` | заменяется декларативной моделью тенантов (OVSDB или конфиг-файл на тенанта) | | `lb/pipeline.sh` | генератор перестраивается: цикл по тенантам и их сегментам, `reg3` во всех матчах, `ct(zone=NXM_NX_REG3[0..15])`, группы `type=all` для рассылки | | `lb/apply.sh` | инкрементальные бандлы на тенанта и на пул вместо `replace-flows` всей ноды | | `hc/main.go` | модель `tenant → pools → members`, дайджест на пул, батчинг перезаливки, ограничение параллелизма проб, API с фильтрами | | `hc/maglev.go` | без изменений — алгоритм уже параметризован `poolID` и `slots` | | `lb/hc-config.sh` | заменяется генерацией из модели control plane | | `lb/lbctl.sh` | фильтр по тенанту во всех командах | | `scripts/verify.sh` | добавляются проверки изоляции (раздел 8) на паре тенантов с пересекающимися адресами | ## 12. План внедрения | Этап | Содержание | Критерий | |---|---|---| | 1 | Ввести `tenant_id` в `reg3` и `ct(zone=NXM_NX_REG3[0..15])` **на текущем единственном тенанте** | все 86 проверок проходят, поведение не изменилось | | 2 | Генератор пайплайна по списку тенантов; два тенанта с **пересекающимися** адресами | проверки изоляции из раздела 8 зелёные | | 3 | `SLOTS` как параметр пула; таблица 11 с матчем `(pool_id, slot)` | раскладки пулов независимы, дайджест на пул | | 4 | Инкрементальные бандлы на тенанта и пул | изменение одного тенанта не трогает счётчики других | | 5 | Многопуловый `hcd` и источник проб на тенанта (VRF либо netns) | пробы идут в контексте своего VRF при совпадающих адресах | | 6 | Лимиты (`ct-set-limits`, метры) и наблюдаемость с лейблами | тенант не может исчерпать ресурсы ноды | | 7 | Масштабные испытания: 100 тенантов | время применения, память, PPS и CPS в целевых границах | Порядок важен: этап 1 — чистый рефакторинг без изменения поведения, он страхует всё остальное существующим набором проверок. ## 13. Спайки до начала работ - **S-MT-1 — изоляция при пересечении адресов.** Два тенанта с одинаковой подсетью и одинаковыми адресами ВМ. Блокирующий: на нём стоит вся модель. - **S-MT-2 — масштаб таблицы слотов.** 100 пулов: время применения, память `ovs-vswitchd`, влияние на PPS. - **S-MT-3 — источник health-проб.** VRF против netns при совпадающих адресах, в том числе на netdev-датапасе. - **S-MT-4 — ct-зоны под нагрузкой.** Поведение при исчерпании лимита зоны, влияние на соседние зоны. ## 14. Открытые вопросы 1. Сколько ВМ и сегментов на тенанта в среднем и в пределе — от этого зависят число портов моста и размер групп рассылки. 2. Могут ли совпадать **публичные** VIP разных тенантов, или публичный пул адресов глобально уникален. 3. Как сегменты тенантов приходят с фабрики: отдельными портами, VLAN-транком или туннелями (VXLAN/Geneve) — от этого зависит, чем проставляется `tenant_id` в таблице 0. 4. Нужен ли межтенантный трафик (route leaking между VRF) и по каким правилам. 5. Кто владеет IPAM и распределением `tenant_id`, `pool_id`, ct-зон. 6. Требуется ли гарантия полосы на тенанта (QoS) или достаточно защиты от исчерпания ресурсов. 7. Какова целевая плотность: 100 тенантов — это предел или проектная точка, от которой считать запас.