Files
hpnn-proto/README.md
T

328 lines
22 KiB
Markdown
Raw 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_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 проверки стенда
```