Анализ текущего состояния и перечень изменений для классического режима облачного балансировщика: несколько листенеров (публичных и/или приватных), каждый со своим независимым пулом. Подтверждено на стенде: сейчас все листенеры делят один 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 <noreply@anthropic.com>
hpnn_v2 — стенд OVS-маршрутизатора и балансировщика нагрузки
Контейнерный прототип основы сервиса: один узел на Open vSwitch, который выполняет три функции — маршрутизацию между сегментами, балансировку нагрузки на четыре бэкенда и коммутацию приватных сегментов. Всё форвардинг-решение принимается в OpenFlow: сетевой стек ядра контейнера транзитный трафик не обрабатывает. Состав пула ведёт подсистема health-check: мёртвые бэкенды выводятся из балансировки, восстановившиеся возвращаются.
Листенеры есть и в публичном сегменте, и в приватных. Приватный листенер обслуживает клиента, стоящего в одном сегменте с бэкендами, сохраняя его исходный IP и транслируя порт 80 на 8080 — при этом в ОС виртуальных машин не настраивается ничего.
Развивает дизайн-концепцию ../hpnn_v1/docs/design.md.
| Шаг | Содержание | План | Итоги |
|---|---|---|---|
| 1 | Окружение, маршрутизация, балансировка | план | итоги |
| 2 | Health-check и таблица слотов | план | итоги |
| — | Расширение пула до четырёх бэкендов | план | итоги |
| 3 | Листенер в приватном сегменте, коммутация сегментов | план | итоги |
| — | Разделение документации по потокам данных | план | итоги |
Как устроена раскладка слотов — docs/MAGLEV_SLOT_TABLE.md
(справка по hc/maglev.go для разработки). Что нужно изменить для перевода ноды
на OVS-DPDK в ПРОД — docs/OVS_DPDK_MIGRATION.md,
на мультитенантную схему (100+ тенантов, изоляция уровня VRF) —
docs/MULTITENANCY.md, на несколько листенеров с
независимыми пулами бэкендов — docs/MULTI_LISTENER_POOLS.md.
Потоки данных описаны отдельно для каждого вида балансировки:
публичный трафик — клиент из клиентской сети на
192.168.5.21:80, целиком маршрутизируемый путь;
приватный трафик — клиент внутри приватного
сегмента, рядом с бэкендами, на 10.20.0.100:80.
Топология
клиент из 192.168.5.0/24
│
[ enp3s0, macvlan ] сеть 1 — публичный сегмент
│ pub0 · ofport 1
┌─────────────────┴─────────────────────────────┐
│ hpnn-lb (Open vSwitch, br-lb) │ узел 192.168.5.20
│ маршрутизатор + балансировщик + коммутатор │ VIP 192.168.5.21
│ hcd: health-check + таблица слотов │
└──┬─────────────────────────────────────┬──────┘
сеть 2 │ 10.20.0.0/24 сеть 3 │ 10.30.0.0/24
шлюз ·1 │ VIP 10.20.0.100:80 шлюз ·1 │ VIP 10.30.0.100:80
│ │
p2 ·2 ──┤ аплинк к шлюзу Docker .254 p3 ·3 ├── аплинк .254
hcif-p2·4│ 10.20.0.253 (пробы) hcif-p3 ·5│ 10.30.0.253
be1 ·10 │ 10.20.0.2:8080 be2 ·20│ 10.30.0.2:8080
be3 ·11 │ 10.20.0.3:8080 be4 ·21│ 10.30.0.3:8080
cli2 ·12 │ 10.20.0.10 (клиент) cli3 ·22│ 10.30.0.10
Каждая ВМ приватного сегмента — отдельный порт моста. Так балансировщик
видит и ответ бэкенда клиенту-соседу по подсети, а значит может снять с него
трансляцию. В целевой среде это выполняется само собой: ВМ подключены к
vSwitch. В стенде порты создаёт scripts/attach-segment.sh
(make attach) — он же назначает адреса и маршруты снаружи, как это делают
гипервизор и DHCP.
| Контейнер | Роль |
|---|---|
hpnn-lb |
OVS: маршрутизация, балансировка, коммутация сегментов |
hpnn-be1, hpnn-be3 |
web-сервис на Go в приватном сегменте 2 |
hpnn-be2, hpnn-be4 |
тот же сервис в приватном сегменте 3 |
hpnn-cli2, hpnn-cli3 |
клиенты внутри сегментов; в их ОС настроен только адрес |
Адресный план
| Назначение | Адрес |
|---|---|
| Клиентская сеть | 192.168.5.0/24 |
| Адрес узла в публичном сегменте | 192.168.5.20 |
| VIP публичного листенера | 192.168.5.21:80 |
| VIP приватных листенеров | 10.20.0.100:80, 10.30.0.100:80 |
| Клиенты внутри сегментов | 10.20.0.10, 10.30.0.10 |
| Шлюз OVS в сети 2 / бэкенды be1, be3 | 10.20.0.1 / 10.20.0.2:8080, 10.20.0.3:8080 |
| Шлюз OVS в сети 3 / бэкенды be2, be4 | 10.30.0.1 / 10.30.0.2:8080, 10.30.0.3:8080 |
| Источники health-проб (internal-порты OVS) | 10.20.0.253, 10.30.0.253 |
| Шлюзы Docker (собственный egress бэкендов) | 10.20.0.254, 10.30.0.254 |
Адреса 192.168.5.20 и 192.168.5.21 существуют только в правилах
OpenFlow — на интерфейсах они не настроены. ARP-респондер OVS отвечает на
них MAC-адресом порта pub0. Исключение — адреса .253: это единственные
адреса, которые узел держит в ядре, потому что с них уходят health-пробы.
Единственный источник правды по адресам — lb/topology.env.
Запуск
make up # modprobe openvswitch + сборка образов + запуск + attach
make verify # проверки
make down # остановка
make up вызывает make attach: порты ВМ приватных сегментов создаются
скриптом, а не Docker. Повторить make attach нужно после пересоздания любого
контейнера сегмента — вручную созданные veth вместе с ним исчезают.
Проверка с самого хоста требует macvlan-shim: интерфейс macvlan контейнера и физический интерфейс хоста по устройству macvlan не видят друг друга. Клиенту из 192.168.5.0/24 никакой shim не нужен.
make shim # поднять shim 192.168.5.13 с маршрутами на .20 и .21
make shim-down
Полный сценарий ручной проверки стенда — docs/MANUAL_TEST_PLAN.md
(что выполнять на клиенте, что на хосте, чего ожидать на каждом шаге и что
означает отклонение). Автоматический аналог — make verify.
Проверка с клиента
ping 192.168.5.20 # адрес узла — отвечает ICMP-респондер OVS
ping 192.168.5.21 # VIP
for i in $(seq 20); do curl -s http://192.168.5.21/; done
Из приватного сегмента — с клиента, который стоит рядом с бэкендами:
docker compose exec cli2 curl -s http://10.20.0.100/
docker compose exec cli2 ip route # маршрутов нет: настроен только адрес
backend=be1 time=2026-08-17 15:29:23 MSK client=10.20.0.10:46064 served=10.20.0.2:8080 req=1
client=10.20.0.10 — исходный адрес клиента, served=…:8080 — трансляция
порта 80 → 8080. Ответ бэкенда клиенту-соседу уходит по L2 напрямую, но
проходит через мост, где таблица 24 снимает трансляцию.
backend=be1 time=2026-08-16 21:35:02 MSK client=192.168.5.13:47734 served=10.20.0.2:8080 req=24
backend=be3 time=2026-08-16 21:35:02 MSK client=192.168.5.13:47740 served=10.20.0.3:8080 req=11
backend=be2 time=2026-08-16 21:35:02 MSK client=192.168.5.13:47742 served=10.30.0.2:8080 req=30
backend=be4 time=2026-08-16 21:35:02 MSK client=192.168.5.13:47746 served=10.30.0.3:8080 req=17
Что здесь видно:
- балансировка — ответы приходят от всех четырёх бэкендов примерно поровну;
- сохранение IP клиента — в поле
clientстоит реальный адрес из 192.168.5.0/24, а не адрес балансировщика: выполняется только DNAT, SNAT нигде не делается; - ключ хэша —
(ip_src, tcp_src), поэтому разные соединения одного клиента расходятся по бэкендам, а пакеты одного соединения — нет.
Подсистему маршрутизации видно отдельно от балансировки:
# на клиенте: маршруты в приватные сегменты через узел
ip route add 10.20.0.0/24 via 192.168.5.20
ip route add 10.30.0.0/24 via 192.168.5.20
curl http://10.20.0.2:8080/ # напрямую в бэкенд, мимо VIP
Эти маршруты нужно прописывать на клиентской машине, а не на хосте стенда: на самом хосте они перекроют connected-маршруты docker-бриджей
hpnn-p2/hpnn-p3и оборвут бэкендам выход наружу.
Транзит между приватными сегментами проверяется изнутри:
docker compose exec be1 curl -s http://10.30.0.2:8080/ # be1 -> be2 через OVS
Подсистема health-check
Демон hcd внутри контейнера-балансировщика проверяет живость членов пула и
отражает их состав в датапасе. Пробы уходят с уникального адреса узла в
сегменте бэкенда (10.20.0.253, 10.30.0.253), а не с VIP — иначе ответ
вернулся бы не тому, кто проверял.
make health # состояние членов пула
make slots # раскладка слотов и счётчики пакетов по членам
make drain M=be1 # вывести член из балансировки (пробы продолжаются)
make enable M=be1 # вернуть член в балансировку
make metrics # метрики Prometheus
пул 1: слотов 1024, проба http каждые 2s (rise=2 fall=3)
дайджест раскладки: 0a16713c5a9eb94d
ЧЛЕН АДРЕС СОСТОЯНИЕ ADMIN В ПУЛЕ СЛОТОВ ПРОБ НЕУДАЧ МС ИСТОЧНИК ПРОБ
be1 10.20.0.2:8080 up enabled да 256 120 0 1 10.20.0.253
be2 10.30.0.2:8080 up enabled да 256 120 0 2 10.30.0.253
be3 10.20.0.3:8080 up enabled да 256 120 0 2 10.20.0.253
be4 10.30.0.3:8080 up enabled да 256 120 0 2 10.30.0.253
Проверить руками:
docker compose pause be1 # член уходит в down за fall × interval = 6 с
curl http://192.168.5.21/ # трафик обслуживают три оставшихся члена
docker compose unpause be1 # возвращается за rise × interval = 4 с
Параметры проб (тип, интервал, таймаут, rise/fall) — в
lb/topology.env, раздел HC_*.
Таблица слотов
Балансировка выполняется не группой OVS, а таблицей из 1024 слотов
(таблица 11). Номер слота даёт multipath(symmetric_l4) от заголовков
пакета, слот указывает на члена пула. Раскладку слотов по членам считает
hcd по алгоритму Maglev (§5 дизайна v1) и заливает атомарным бандлом.
Свойства, которые это даёт:
- детерминированность — одинаковый состав пула даёт одинаковую раскладку на любом узле и между перезапусками; контролируется дайджестом SHA-256;
- минимальное возмущение — при выбытии члена переезжают его слоты, а у
оставшихся меняются единицы: на этом стенде вывод одного из четырёх членов
сдвигает 256 его слотов и лишь 8 чужих (0,8 %); проверяется в
make verify; - fail-close — когда живых членов нет, таблица пустеет и трафик на VIP отбрасывается со счётчиком, а не уходит на заведомо мёртвый бэкенд;
- бесплатная per-member статистика из счётчиков правил слотов.
Осмотр датапаса
make ports # порты моста
make conns # таблица соединений ct
make trace SRC=192.168.5.13 # ofproto/trace сессии клиент -> VIP
make trace SRC=10.20.0.10 SEG=p2 # то же для приватного листенера
make fdb # выученные MAC сегментов и счётчики рассылки
make flows # перечитать pipeline.sh и перезалить правила
docker compose exec lb-router lbctl flows 21 # правила конкретной таблицы
docker compose exec lb-router lbctl status # состояние пула в JSON
Как устроен пайплайн
Все правила рендерит lb/pipeline.sh из topology.env и
применяет атомарным бандлом (ovs-ofctl --bundle replace-flows), поэтому
обновление не создаёт окна с полузалитым пайплайном.
| Таблица | Назначение |
|---|---|
| 0 | Сегмент по входному порту (reg0); обучение MAC (FDB) и adjacency |
| 1 | Классификация: ARP, ICMP, листенер, трафик к маршрутизатору, коммутация |
| 5 | ARP-респондер для адресов узла, VIP и источников проб; прочий ARP — в коммутацию |
| 6 | ICMP echo-респондер для адресов узла и VIP |
| 10 | Листенеры: VIP:80 → multipath кладёт номер слота в reg1; прочий IP → маршрутизация |
| 11 | Таблица слотов: reg1 → reg2 (член пула). Ведёт демон hcd |
| 12 | Применение члена: DNAT и выдача — коммутацией либо через маршрутизацию |
| 15, 16 | Обратный путь L3: ct(nat) снимает DNAT, источник снова становится VIP |
| 20 | Маршрутизация: приоритет = длина префикса, dec_ttl, MAC источника |
| 21 | Adjacency: MAC next-hop и выходной порт |
| 24 | Обратная трансляция коммутируемого трафика сегмента |
| 25 | L2-коммутация сегмента: FDB, broadcast и рассылка неизвестного unicast |
Регистры: reg0 — сегмент (1 — публичный, 2 и 3 — приватные), reg1 — номер
слота, reg2 — идентификатор члена пула.
Нумерация таблиц не произвольна: goto_table разрешает переход только вперёд,
поэтому обратный путь (15/16) стоит до общей маршрутизации (20), а коммутация
(24/25) — после неё: в неё попадают и кадры, прошедшие DNAT в таблице 12.
Два пути внутри одного пула
Приватный листенер и публичный делят пул, таблицу слотов и хэш. Различается только выдача:
- член в сегменте клиента — меняется лишь MAC назначения, кадр отдаётся в
коммутацию.
dec_ttlне выполняется: клиент и бэкенд остаются L2-соседями, и ответ приходит к клиенту с исходным TTL. Обратную трансляцию делает таблица 24, когда бэкенд отвечает соседу напрямую; - член в другом сегменте либо публичный листенер — прежний маршрутизируемый
путь через таблицы 20/21 с
dec_ttlи обратной трансляцией в таблице 15.
Для клиента оба пути неотличимы: ответ приходит от VIP:80.
Какие приватные листенеры подняты, задаёт PRIV_LISTENERS в
lb/topology.env: пусто — ни одного, "p2" — один сегмент,
"p2 p3" — оба.
Чего нет у чисто-OpenFlow узла
Ядро контейнера в форвардинге не участвует, поэтому всё, что обычно делает
сетевой стек, реализовано правилами: ARP-ответы (таблица 5), ответы на ping
(таблица 6), поиск MAC клиента (действие learn в таблице 0 вместо
ARP-резолвера). ICMP-ошибки (TTL exceeded, fragmentation needed) на шаге 1
не генерируются — см. ограничения в docs/STEP1_SUMMARY.md.
Особенности окружения
- Kernel datapath OVS namespace-aware, поэтому контейнер держит собственный
ovs-system— это настоящий датапас ядра, а не эмуляция. Нужныprivileged: true, проброс/lib/modulesи загруженный на хосте модульopenvswitch(make prereq, модуль прописывается в автозагрузку). - Публичная сеть объявлена с подсетью
192.168.55.0/24— это фиктивный пул для Docker IPAM. Реальную192.168.5.0/24объявить нельзя: её уже занимает macvlan-сеть соседнего стендаrouter-functest, и Docker отказывается регистрировать пересекающийся пул. Для L2 это безразлично — macvlan включён вenp3s0, а адреса стенда живут в OpenFlow. - Шлюзы приватных сетей смещены на
.254, чтобы адрес.1занял OVS-роутер и не конфликтовал с host-бриджем Docker. - Профиль A (§3.2 дизайна v1): у бэкендов через балансировщик
маршрутизируются только клиентские префиксы и соседний приватный сегмент,
а
defaultостаётся на шлюзе Docker — собственный исходящий трафик бэкендов идёт мимо балансировщика.
Структура
docker-compose.yml три сети и три контейнера
Makefile up / down / flows / verify / health / диагностика
lb/topology.env адреса, MAC, номера портов, параметры проб и слотов
lb/pipeline.sh генератор OpenFlow-правил
lb/apply.sh атомарная заливка правил и запрос слотов у hcd
lb/entrypoint.sh запуск OVS, сборка моста, порты проб, запуск hcd
lb/hc-config.sh конфигурация health-check из topology.env
lb/lbctl.sh осмотр датапаса (ports / flows / health / slots / trace)
hc/maglev.go раскладка слотов, дайджест, рендер бандла
hc/main.go пробер, состояния членов пула, применение, API
backend/main.go web-сервис: имя хоста, дата и время, адрес клиента
scripts/host-prereq.sh модуль ядра и macvlan-shim
scripts/verify.sh проверки стенда