# hpnn_v2 — стенд OVS-маршрутизатора и балансировщика нагрузки Контейнерный прототип основы сервиса: один узел на Open vSwitch, который выполняет три функции — **маршрутизацию** между сегментами, **балансировку нагрузки** на четыре бэкенда и **коммутацию приватных сегментов**. Всё форвардинг-решение принимается в OpenFlow: сетевой стек ядра контейнера транзитный трафик не обрабатывает. Состав пула ведёт подсистема health-check: мёртвые бэкенды выводятся из балансировки, восстановившиеся возвращаются. Листенеры есть и в публичном сегменте, и в приватных. Приватный листенер обслуживает клиента, стоящего **в одном сегменте с бэкендами**, сохраняя его исходный IP и транслируя порт 80 на 8080 — при этом в ОС виртуальных машин не настраивается ничего. Развивает дизайн-концепцию `../hpnn_v1/docs/design.md`. | Шаг | Содержание | План | Итоги | |---|---|---|---| | 1 | Окружение, маршрутизация, балансировка | [план](docs/STEP1_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP1_SUMMARY.md) | | 2 | Health-check и таблица слотов | [план](docs/STEP2_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP2_SUMMARY.md) | | — | Расширение пула до четырёх бэкендов | [план](docs/CHANGE_POOL_4_MEMBERS_PLAN.md) | [итоги](docs/CHANGE_POOL_4_MEMBERS_SUMMARY.md) | | 3 | Листенер в приватном сегменте, коммутация сегментов | [план](docs/STEP3_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP3_SUMMARY.md) | | — | Разделение документации по потокам данных | [план](docs/CHANGE_SPLIT_DATAFLOW_DOCS_PLAN.md) | [итоги](docs/CHANGE_SPLIT_DATAFLOW_DOCS_SUMMARY.md) | Потоки данных описаны отдельно для каждого вида балансировки: [публичный трафик](docs/PUBLIC_LB_DATAFLOW.md) — клиент из клиентской сети на `192.168.5.21:80`, целиком маршрутизируемый путь; [приватный трафик](docs/PRIVATE_LB_DATAFLOW.md) — клиент внутри приватного сегмента, рядом с бэкендами, на `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](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](lb/topology.env). ## Запуск ```bash 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 не нужен. ```bash make shim # поднять shim 192.168.5.13 с маршрутами на .20 и .21 make shim-down ``` Полный сценарий ручной проверки стенда — [docs/MANUAL_TEST_PLAN.md](docs/MANUAL_TEST_PLAN.md) (что выполнять на клиенте, что на хосте, чего ожидать на каждом шаге и что означает отклонение). Автоматический аналог — `make verify`. ## Проверка с клиента ```bash 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 ``` Из приватного сегмента — с клиента, который стоит рядом с бэкендами: ```bash 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)`, поэтому разные соединения одного клиента расходятся по бэкендам, а пакеты одного соединения — нет. Подсистему маршрутизации видно отдельно от балансировки: ```bash # на клиенте: маршруты в приватные сегменты через узел 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` и оборвут бэкендам выход наружу. Транзит между приватными сегментами проверяется изнутри: ```bash 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 — иначе ответ вернулся бы не тому, кто проверял. ```bash 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 ``` Проверить руками: ```bash 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](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 статистика** из счётчиков правил слотов. ## Осмотр датапаса ```bash 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](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](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](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 проверки стенда ```