Files
hpnn-proto/README.md
T

22 KiB
Raw Blame History

hpnn_v2 — стенд OVS-маршрутизатора и балансировщика нагрузки

Контейнерный прототип основы сервиса: один узел на Open vSwitch, который выполняет три функции — маршрутизацию между сегментами, балансировку нагрузки на четыре бэкенда и коммутацию приватных сегментов. Всё форвардинг-решение принимается в OpenFlow: сетевой стек ядра контейнера транзитный трафик не обрабатывает. Состав пула ведёт подсистема health-check: мёртвые бэкенды выводятся из балансировки, восстановившиеся возвращаются.

Листенеры есть и в публичном сегменте, и в приватных. Приватный листенер обслуживает клиента, стоящего в одном сегменте с бэкендами, сохраняя его исходный IP и транслируя порт 80 на 8080 — при этом в ОС виртуальных машин не настраивается ничего.

Развивает дизайн-концепцию ../hpnn_v1/docs/design.md.

Шаг Содержание План Итоги
1 Окружение, маршрутизация, балансировка план итоги
2 Health-check и таблица слотов план итоги
— Расширение пула до четырёх бэкендов план итоги
3 Листенер в приватном сегменте, коммутация сегментов план итоги
— Разделение документации по потокам данных план итоги

Потоки данных описаны отдельно для каждого вида балансировки: публичный трафик — клиент из клиентской сети на 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      проверки стенда