From 69b9b56ccdd87ffce1c20f465c172f7d59aaffef Mon Sep 17 00:00:00 2001 From: ayurishchev Date: Mon, 17 Aug 2026 21:28:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=B8=D0=BC=20=D0=BB=D0=B8=D1=81=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D0=BC=20=D1=81=20=D0=BD=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D1=8B=D0=BC=D0=B8=20=D0=BF=D1=83?= =?UTF-8?q?=D0=BB=D0=B0=D0=BC=D0=B8=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Анализ текущего состояния и перечень изменений для классического режима облачного балансировщика: несколько листенеров (публичных и/или приватных), каждый со своим независимым пулом. Подтверждено на стенде: сейчас все листенеры делят один POOL_ID и одну таблицу слотов (reg1 -> reg2), member_id глобален. multipath для разных листенеров с одним POOL_ID даёт одинаковый номер слота для совпадающего 5-tuple — пулы физически не разделены. Предложена схема reg4 = pool_id: таблица слотов становится общей для всех пулов с матчем (reg4, reg1), FlowBundle.flow delete фильтруется по reg4 для инкрементального обновления одного пула. Замер: 1024 правила заливаются бандлом за 86 мс. Co-Authored-By: Claude Opus 5 --- README.md | 3 +- docs/MULTI_LISTENER_POOLS.md | 319 +++++++++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 docs/MULTI_LISTENER_POOLS.md diff --git a/README.md b/README.md index 8ad9800..63c4417 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ (справка по `hc/maglev.go` для разработки). Что нужно изменить для перевода ноды на OVS-DPDK в ПРОД — [docs/OVS_DPDK_MIGRATION.md](docs/OVS_DPDK_MIGRATION.md), на мультитенантную схему (100+ тенантов, изоляция уровня VRF) — -[docs/MULTITENANCY.md](docs/MULTITENANCY.md). +[docs/MULTITENANCY.md](docs/MULTITENANCY.md), на несколько листенеров с +независимыми пулами бэкендов — [docs/MULTI_LISTENER_POOLS.md](docs/MULTI_LISTENER_POOLS.md). Потоки данных описаны отдельно для каждого вида балансировки: [публичный трафик](docs/PUBLIC_LB_DATAFLOW.md) — клиент из клиентской сети на diff --git a/docs/MULTI_LISTENER_POOLS.md b/docs/MULTI_LISTENER_POOLS.md new file mode 100644 index 0000000..39e140c --- /dev/null +++ b/docs/MULTI_LISTENER_POOLS.md @@ -0,0 +1,319 @@ +# Несколько листенеров с уникальными пулами: перечень необходимых изменений + +**Дата:** 2026-08-17 +**Зачем:** классический режим облачного балансировщика — на ноде HPNN +одновременно работают несколько листенеров (публичных и/или приватных), и +**каждый обслуживает свой независимый пул бэкендов**. Сейчас это не так: все +листенеры делят один общий пул. +**Основание:** фактическое состояние стенда на 2026-08-17 (шаг 3). +**Смежные документы:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md), +[PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md), +[MAGLEV_SLOT_TABLE.md](MAGLEV_SLOT_TABLE.md), +[MULTITENANCY.md](MULTITENANCY.md) — этот документ описывает многопуловость +внутри одного тенанта; при мультитенантности оба изменения комбинируются. + +## 1. Исходное состояние — проверено на стенде + +| Место | Как сейчас | Подтверждение | +|---|---|---| +| `lb/topology.env` | один `POOL_ID=1`, один `SLOTS=1024`, один `VIP`, `PRIV_VIP_PORT` общий на оба приватных листенера | `grep POOL_ID` даёт одно значение | +| `lb/hc-config.sh` | один блок `"members": [...]` на весь конфиг демона | нет ни `listener`, ни привязки члена к листенеру | +| `hc/main.go`, `Config` | один `PoolID uint32`, один `[]*Member` | структура `Config` не содержит списка пулов | +| `hc/maglev.go`, `BuildSlotTable` | принимает **один** `poolID` и **один** список членов | сигнатура `func BuildSlotTable(poolID uint32, members []*Member, slots int) []int` | +| Таблица 10 (`lb/pipeline.sh`) | три листенера (`VIP:80`, `P2_VIP:80`, `P3_VIP:80`), у каждого свой `multipath(...,POOL_ID,...)` — но `POOL_ID` **один и тот же** | строки с `multipath(symmetric_l4,$POOL_ID,...)` повторяют одну переменную | +| Таблица 11 (слоты) | один `reg1` (0–1023) → `reg2` (1–4); `reg1` не содержит признака пула | 1024 правила `table=11,priority=100,ip,reg1=<слот>` | +| Таблица 12 (DNAT) | матч только по `reg2` (у публичного листенера) или `reg0+reg2` (у приватного); `reg2` — глобальный ID члена | `table=12,priority=100,ip,reg2=0x1 actions=...` | +| `member_id` | 1–4, зашиты в `topology.env` (`BE1_ID=0x1` … `BE4_ID=0x4`) | одно адресное пространство ID на всю ноду | + +**Вывод:** три листенера сейчас формально работают, но `multipath` считает слот +от одного и того же `POOL_ID` для всех — то есть при одинаковом 5-tuple разные +листенеры выбрали бы **один и тот же номер слота**, и любой из них ссылается на +одну и ту же таблицу 11 с одним составом членов. Разделить пулы физически +невозможно без изменения модели идентификаторов. + +## 2. Целевая модель + +``` +Listener (VIP, порт, протокол, сегмент) + └── Pool (pool_id, slots, affinity, health_monitor) + └── Member (id, address, port, weight) +``` + +Один листенер → один пул. Пул может обслуживать несколько листенеров (типовой +кейс — HTTP и HTTPS на общий бэкенд), но никогда наоборот. Правило соответствует +§6 дизайна v1: `Listener.pool` — внешний ключ на `Pool`. + +### 2.1. Идентификаторы + +Два независимых пространства номеров, а не один плоский `member_id`: + +| Что | Ширина | Где хранится | Обоснование | +|---|---|---|---| +| `pool_id` | не менее 16 бит | `reg4` (новый) | сейчас 32-битный, но используется только как seed хэша; для матчей в таблице 11 нужно явное поле | +| `member_id` | 8–16 бит, **уникален внутри пула**, не глобально | `reg2` (как сейчас) | у разных пулов id членов могут и должны повторяться (1, 2, 3…) | + +`reg2` без `reg4` в таблице 11 неоднозначен: `member_id=1` у пула A и +`member_id=1` у пула B — разные бэкенды. Матч обязан включать оба поля. + +### 2.2. Таблица 11 — главное изменение + +Сейчас: `reg1 (слот) → reg2 (член)`, один пул на всю таблицу. + +Целевое: **одна таблица слотов на все пулы**, отдельная таблица на пул не +подходит — в OpenFlow не более 255 таблиц, а пулов на ноде десятки. + +``` +table=11,priority=100,ip,reg4=,reg1= + actions=load:->NXM_NX_REG2[],goto_table:12 +``` + +Требование к слоту: значения `reg1` **переиспользуются между пулами** (у пула A +слот 5 и у пула B слот 5 существуют одновременно, различает их `reg4`), поэтому +после `multipath` перед `goto_table:11` обязательно `load:- +>NXM_NX_REG4[]`. + +### 2.3. Таблица 10 — листенер помечает свой пул + +``` +table=10,priority=200,tcp,nw_dst=,tp_dst=<порт> + actions=load:->NXM_NX_REG4[], + multipath(symmetric_l4,,modulo_n,,0,NXM_NX_REG1[]), + goto_table:11 +``` + +`pool_id` продолжает быть `basis` хэша (как сейчас) — это даёт независимые +раскладки для разных пулов на одном 5-tuple, что уже проверено в шаге 2. +Дополнительно он же явно кладётся в `reg4` для матча таблицы 11. + +### 2.4. Таблица 12 — DNAT с учётом пула + +``` +table=12,priority=100,ip,reg4=,reg2= + actions=ct(commit,zone=,nat(dst=:),table=20) +``` + +Для приватных листенеров (коммутируемый путь, шаг 3) матч дополнительно +включает `reg0` (сегмент), как сейчас — только добавляется `reg4`. + +### 2.5. `SLOTS` и `affinity` — параметры пула, не ноды + +Сейчас `SLOTS=1024` и алгоритм хэша (`multipath(symmetric_l4,...)`, единая +строка в `lb/pipeline.sh`) — общие для всей ноды. При независимых пулах это +обязано стать полем `Pool`: + +- `slots` — гранулярность весов; для пула из 3–5 членов достаточно 256 (см. + расчёт стоимости в разделе 4); + `affinity` (`none`/`source_ip`) — выбор хэш-полей `multipath`, сейчас общий + `symmetric_l4` для всех листенеров, а нужен независимый на пул (§6 дизайна + v1 уже переносит `affinity` на пул). + +## 3. Изменения в `hc/` (демон) + +### 3.1. Модель конфигурации + +```go +type Config struct { + Bridge, OFVersion, API, ApplyScript string + SlotTable, DNATTable int + Pools []*Pool // было: PoolID + Members +} + +type Pool struct { + ID uint32 + Name string + Slots int + Affinity string // "none" | "source_ip" + Probe, HTTPPath, Interval, Timeout string + Rise, Fall int + Members []*Member // как сейчас, но member.ID уникален внутри пула +} +``` + +`Member.ID` перестаёт быть глобальным — валидация уникальности переносится +внутрь `Pool`. + +### 3.2. `hc/maglev.go` — сигнатура меняется минимально + +`BuildSlotTable` уже параметризован `poolID` и `slots` — переиспользуется как +есть, вызывается **в цикле по пулам**: + +```go +for _, pool := range cfg.Pools { + table := BuildSlotTable(pool.ID, pool.Members, pool.Slots) + digest := Digest(table) + // сравнение с pool.lastDigest, не с глобальным digest +} +``` + +`FlowBundle` дополняется полем `pool_id`: + +```go +func FlowBundle(poolID uint32, table []int, slotTable, nextTable int) string { + // flow delete table=,reg4= + // flow add ...,reg4=,reg1= actions=load:->reg2,goto_table: +} +``` + +Ключевое: `flow delete` теперь фильтруется **по `reg4`**, а не сносит всю +таблицу — иначе пересчёт одного пула стирает раскладку остальных на время +применения бандла. Это уже требование инкрементальности, отдельно +зафиксированное в `MULTITENANCY.md`. + +### 3.3. Health-check и API + +- Пробы каждого пула идут независимо — `hcd` уже пробует членов параллельно + (`sync.WaitGroup` в `main.go`), достаточно развернуть цикл на пулы. +- Дайджест раскладки, счётчики `probes`/`failures`, применение бандла — + **на пул**, не на ноду. Изменение состояния в пуле A не должно вызывать + перезаливку пула B. +- HTTP API (`/status`, `/status.txt`, `/metrics`, `/drain`, `/enable`) + получает параметр `pool` (или `pool+member`, если имена членов совпадают + между пулами — см. 3.4). +- `/drain?member=be1` неоднозначен при нескольких пулах с членом `be1` — + обязателен `pool`: `/drain?pool=&member=be1`. + +### 3.4. Имена членов не обязаны быть уникальны глобально + +Сейчас `lbctl slots`/`health` матчат член по имени (`be1`…`be4`) — при +нескольких пулах с непересекающимся списком имён это временно работает, но +это случайность конфигурации, а не гарантия. Уникальность имени должна +проверяться **внутри пула**, вывод `lbctl`/`/status` — группироваться по пулу. + +## 4. Стоимость: сколько правил и как быстро применяются + +Замер на стенде: 1024 правила заливаются бандлом за **86 мс** (`ovs-ofctl +bundle`). + +| Листенеров | Членов на пул | Слотов на пул | Правил в таблице 11 | Оценка времени полной заливки | +|---|---|---|---|---| +| 1 (сейчас) | 4 | 1024 | 1 024 | 0,09 с | +| 5 | 4 | 256 | 1 280 | ≈ 0,11 с | +| 20 | 4 | 256 | 5 120 | ≈ 0,44 с | +| 20 | 8 | 512 | 10 240 | ≈ 0,88 с | + +При десятках листенеров на ноде — приемлемо **при условии**, что заливка +инкрементальна (раздел 3.2, `flow delete` по `reg4`). Полная перезаливка +(`replace-flows`), как делает сейчас `apply.sh` для всего пайплайна, при 20+ +пулах даёт заметную паузу и должна остаться только для применения самого +пайплайна (`pipeline.sh`), а не для обновления состава пула. + +## 5. `lb/apply.sh` и `lb/lbctl.sh` + +- `apply.sh` без изменений в части пайплайна; вызов `/reapply` у демона должен + адресоваться **всем пулам** после `replace-flows` (таблица 11 стирается + целиком вместе с остальным пайплайном) — здесь полная перезаливка неизбежна, + это разовая операция при смене конфигурации листенеров, а не при отказе + бэкенда. +- `lb/lbctl.sh`: команды `health`, `slots`, `drain`, `enable`, `trace`, `fdb` + получают обязательный или контекстный параметр `pool`/`listener`. `trace` + уже принимает `SEG` (шаг 3) — по аналогии добавляется выбор VIP и порта не + по жёстко зашитым `$VIP`/`$P2_VIP`, а по имени листенера из конфигурации. + +## 6. `lb/topology.env` → декларативная модель листенеров + +Плоский файл с одним `VIP`/`POOL_ID` не описывает список листенеров. Минимальный +шаг без полноценного OVSDB (§6 дизайна v1) — секция листенеров в том же файле +или отдельном: + +```bash +# listeners.env — один блок на листенер +LISTENER_1_NAME=public-http +LISTENER_1_SEGMENT=pub +LISTENER_1_VIP=192.168.5.21 +LISTENER_1_PORT=80 +LISTENER_1_POOL=pool-a + +LISTENER_2_NAME=priv2-http +LISTENER_2_SEGMENT=p2 +LISTENER_2_VIP=10.20.0.100 +LISTENER_2_PORT=80 +LISTENER_2_POOL=pool-b + +POOL_A_ID=1 +POOL_A_SLOTS=1024 +POOL_A_MEMBERS="be1:10.20.0.2:8080:1 be2:10.30.0.2:8080:1" + +POOL_B_ID=2 +POOL_B_SLOTS=256 +POOL_B_MEMBERS="be3:10.20.0.3:8080:1 be4:10.30.0.3:8080:1" +``` + +`lb/pipeline.sh` перестаёт быть плоским генератором с фиксированным числом +листенеров (сейчас — ровно три, с условным включением приватных через +`PRIV_LISTENERS`) и переходит на **цикл по списку листенеров**, каждый со своим +пулом. Это прямой шаг к модели `Load_Balancer → Listener → Pool → Member` из +`MULTITENANCY.md` — там же описан переход к OVSDB и control plane, здесь +достаточно локального конфига. + +## 7. Валидации (переносятся из §8.4 дизайна v1, применяются к новой модели) + +| Правило | Уровень | Комментарий | +|---|---|---| +| `(vip, protocol, port)` уникальна | среди листенеров ноды | сейчас неявно верно (три разных VIP), но не проверяется | +| `(member_address, member_port, proto)` не повторяется в **разных** пулах | глобально на ноде | иначе `ct`-обратная трансляция неоднозначна — один и тот же адрес:порт не может принадлежать двум пулам | +| `member_id` уникален внутри пула, не глобально | на пул | пересматривается относительно текущей схемы | +| `pool_id` уникален на ноде | глобально | входит в `reg4`, basis хэша | +| один и тот же член может входить в несколько пулов **разных** листенеров | разрешено | классический кейс — общий бэкенд для HTTP и HTTPS листенеров с разными портами приёма | + +Второе правило — самое важное практическое ограничение: нельзя создать два +пула, у которых есть член с одинаковым `address:port`. Сейчас проверки такой +нет вовсе. + +## 8. Изменения по файлам — сводка + +| Файл | Действие | +|---|---| +| `lb/pipeline.sh` | генератор переходит на цикл по листенерам/пулам; `reg4` во всех матчах таблиц 10/11/12; `flow delete` в бандле слотов — по `reg4` | +| `lb/topology.env` | секция листенеров и пулов вместо одного `VIP`/`POOL_ID`/`SLOTS` (раздел 6) | +| `lb/hc-config.sh` | генерирует список `Pools[]` вместо одного `members` | +| `hc/main.go` | `Config.Pools []*Pool`; API получает параметр `pool`; применение и дайджест — на пул | +| `hc/maglev.go` | `FlowBundle` получает `poolID`, `flow delete` фильтруется по `reg4` | +| `lb/lbctl.sh` | `health`/`slots`/`drain`/`enable`/`trace`/`fdb` — параметр `pool`/`listener` | +| `scripts/verify.sh` | новая секция: два листенера с непересекающимися пулами, отказ члена одного не влияет на другой | +| `docs/MANUAL_TEST_PLAN.md` | блок ручной проверки нескольких пулов | + +## 9. План внедрения + +| Этап | Содержание | Критерий | +|---|---|---| +| 1 | Ввести `reg4` (pool_id) во все матчи, оставаясь на **одном** пуле | все 86 текущих проверок проходят без изменения поведения | +| 2 | Второй независимый пул на существующем публичном листенере (тестовый VIP) | раскладки двух пулов различны, `flow delete` в бандле не трогает чужой `reg4` | +| 3 | `SLOTS`/`affinity` как поля пула, а не ноды | пул A на 1024 слота и пул B на 256 работают одновременно | +| 4 | Многопуловый `hcd`: `Config.Pools`, API с фильтром | `/status?pool=b` не показывает членов пула A | +| 5 | Декларативный список листенеров в `topology.env` | добавление четвёртого листенера — правка одного файла, без изменения `pipeline.sh` | +| 6 | Валидации §7 в `apply.sh` или отдельном линтере конфигурации | конфликт `member_address:port` между пулами отклоняется до заливки | +| 7 | Нагрузочная проверка: 10–20 листенеров, независимые пулы | время применения и корректность раскладок в пределах раздела 4 | + +Порядок аналогичен `MULTITENANCY.md`: этап 1 — чистый рефакторинг, +подтверждаемый существующим набором проверок, прежде чем менять модель данных. + +## 10. Спайки до начала работ + +- **S-ML-1 — `reg4` в `multipath` и матчах.** Подтвердить, что + `load:->NXM_NX_REG4[]` перед `multipath` и последующий матч + `reg4=...,reg1=...` работают детерминированно на OVS 3.1 (в стенде уже + использовался похожий приём с `reg0`/`reg3` — риск низкий, но не проверен + именно для `reg4` + `multipath`). +- **S-ML-2 — `flow delete` с фильтром `reg4` в бандле.** Убедиться, что частичное + удаление подмножества правил таблицы 11 по регистру не создаёт временного + окна с неполной раскладкой для **не изменяемых** пулов. +- **S-ML-3 — совместное использование члена двумя пулами.** Проверить обратную + трансляцию (`ct(nat)` в таблице 15/24), когда один и тот же бэкенд обслуживает + два листенера на разных портах приёма, но с одним `member_port` — граница + правила §7. + +## 11. Открытые вопросы + +1. Нужен ли явный API создания листенера и пула (REST/CLI) на этом этапе, или + пока достаточно декларативного `topology.env`/`listeners.env`. +2. Разрешено ли одному бэкенду входить в пулы двух разных листенеров + одновременно (раздел 7) — это меняет модель валидации. +3. Нужны ли разные health-monitor'ы для разных пулов на одной ноде (например, + TCP-проба для одного пула и HTTP — для другого) — конфиг демона уже это + допускает per-pool, вопрос в приоритете реализации. +4. Ожидаемое число листенеров и пулов на ноде вне мультитенантного сценария — + от этого зависит, достаточно ли плоского конфиг-файла или сразу нужен OVSDB. +5. Как это соотносится с `MULTITENANCY.md`: планируется ли многопуловость как + промежуточный шаг перед тенантами, или тенант тоже может иметь несколько + листенеров с разными пулами (тогда `reg3` и `reg4` комбинируются с самого + начала).