Анализ текущего состояния и перечень изменений для размещения 100+ тенантов на одной ноде с изоляцией уровня VRF. Ключевая проблема — пересекающиеся адресные пространства тенантов: ни один матч в пайплайне не может опираться на IP или MAC без идентификатора тенанта. Предложена схема с tenant_id в reg3 и ct-зоной из регистра (ct(zone=NXM_NX_REG3[0..15]) — синтаксис проверен на стенде). Отдельно оценён масштаб: таблица слотов доминирует (100 пулов по 1024 слота дают 102400 правил и ~8.8 с полной перезаливки при замеренных 88 мс на 1024 правила), поэтому SLOTS должен стать параметром пула, а apply.sh — перейти на инкрементальные бандлы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
23 KiB
Мультитенантность на ноде HPNN: перечень необходимых изменений
Дата: 2026-08-17 Зачем: в ПРОД на одну ноду HPNN (маршрутизация + балансировка) размещается 100 и более тенантов. Тенант — изолированная сущность, аналог VRF: своё адресное пространство, свои сегменты, свои листенеры и пулы. Адреса разных тенантов могут совпадать. Основание: фактическое состояние стенда на 2026-08-17 (шаг 3). Смежные документы: OVS_DPDK_MIGRATION.md, PRIVATE_LB_DATAFLOW.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:<tenant_id>->NXM_NX_REG3[0..15],
load:<segment_id>->NXM_NX_REG0[],
<learn FDB>, 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=<member>:<port>),table=25)
actions=ct(zone=NXM_NX_REG3[0..15],nat,table=25)
Зона — 16 бит, то есть tenant_id ложится в неё один к одному, и 100+ тенантов
помещаются с запасом.
Дополнительно необходимы лимиты соединений на зону, чтобы один тенант не исчерпал таблицу соединений всей ноды:
ovs-appctl dpctl/ct-set-limits zone=<tenant_id>,limit=<N>
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=<pool_id>,reg1=<slot>
actions=load:<member_id>->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 с |
Отсюда два обязательных вывода:
SLOTSдолжен стать параметром пула, а не ноды. Для пула из 4–8 членов 1024 слота избыточны: 256 дают гранулярность веса 0,4 % и вчетверо меньше правил.- Полная перезаливка недопустима.
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-<tenant>, пробер привязывает сокет к 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. Изоляция: что обязательно проверять
Изоляция — не побочный эффект, а требование, которое нужно доказывать тестом на каждой сборке:
- Два тенанта с одинаковой подсетью
10.20.0.0/24и одинаковыми адресами ВМ обслуживаются независимо; трафик одного не появляется на портах другого. - Совпадающие VIP у разных тенантов работают одновременно.
ct-записи лежат в разных зонах:dpctl/dump-conntrackс фильтром по зоне.- Записи
learn(FDB и adjacency) одного тенанта не применяются к трафику другого. - При отказе всех членов пула тенанта A трафик тенанта B не затронут (fail-close локален).
- Исчерпание лимита соединений тенантом A не влияет на B (
ct-set-limits). - Счётчики правил тенанта 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. Открытые вопросы
- Сколько ВМ и сегментов на тенанта в среднем и в пределе — от этого зависят число портов моста и размер групп рассылки.
- Могут ли совпадать публичные VIP разных тенантов, или публичный пул адресов глобально уникален.
- Как сегменты тенантов приходят с фабрики: отдельными портами, VLAN-транком
или туннелями (VXLAN/Geneve) — от этого зависит, чем проставляется
tenant_idв таблице 0. - Нужен ли межтенантный трафик (route leaking между VRF) и по каким правилам.
- Кто владеет IPAM и распределением
tenant_id,pool_id, ct-зон. - Требуется ли гарантия полосы на тенанта (QoS) или достаточно защиты от исчерпания ресурсов.
- Какова целевая плотность: 100 тенантов — это предел или проектная точка, от которой считать запас.