Документ по поддержке мультитенантности на ноде HPNN

Анализ текущего состояния и перечень изменений для размещения 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>
This commit is contained in:
ayurishchevandClaude Opus 5 committed 2026-08-17 21:21:29 +03:00
1 parent 5e9a035f6e
commit be34fb0492
2 files changed
+324 -1

No files matched your search

+3 -1
View File
@@ -24,7 +24,9 @@
Как устроена раскладка слотов — [docs/MAGLEV_SLOT_TABLE.md](docs/MAGLEV_SLOT_TABLE.md)
(справка по `hc/maglev.go` для разработки). Что нужно изменить для перевода ноды
на OVS-DPDK в ПРОД — [docs/OVS_DPDK_MIGRATION.md](docs/OVS_DPDK_MIGRATION.md).
на OVS-DPDK в ПРОД — [docs/OVS_DPDK_MIGRATION.md](docs/OVS_DPDK_MIGRATION.md),
на мультитенантную схему (100+ тенантов, изоляция уровня VRF) —
[docs/MULTITENANCY.md](docs/MULTITENANCY.md).
Потоки данных описаны отдельно для каждого вида балансировки:
[публичный трафик](docs/PUBLIC_LB_DATAFLOW.md) — клиент из клиентской сети на
+321
View File
@@ -0,0 +1,321 @@
# Мультитенантность на ноде 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:<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+ тенантов
помещаются с запасом.
Дополнительно необходимы **лимиты соединений на зону**, чтобы один тенант не
исчерпал таблицу соединений всей ноды:
```bash
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 с |
Отсюда два обязательных вывода:
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-<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. Изоляция: что обязательно проверять
Изоляция — не побочный эффект, а требование, которое нужно доказывать тестом на
каждой сборке:
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 тенантов — это предел или проектная точка, от
которой считать запас.