Files
ayurishchevandClaude Opus 5 be34fb0492 Документ по поддержке мультитенантности на ноде 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>
2026-08-17 21:21:29 +03:00

322 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Мультитенантность на ноде 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 тенантов — это предел или проектная точка, от
которой считать запас.