Files
hpnn-proto/README.md
T

258 lines
16 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: мёртвые бэкенды выводятся из
балансировки, восстановившиеся возвращаются.
Развивает дизайн-концепцию `../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) |
## Топология
```
клиент из 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 + таблица слотов │
└──┬──────────────────────────────┬─────┘
p2 ·2 │ 10.20.0.1 10.30.0.1 │ p3 ·3
hcif-p2 ·4 10.20.0.253 10.30.0.253 │ hcif-p3 ·5 (источники проб)
сеть 2 │ 10.20.0.0/24 сеть 3 │ 10.30.0.0/24
hpnn-be1 │ 10.20.0.2 hpnn-be2 │ 10.30.0.2
hpnn-be3 │ 10.20.0.3 hpnn-be4 │ 10.30.0.3
```
| Контейнер | Роль |
|---|---|
| `hpnn-lb` | OVS, все три сети, маршрутизация и балансировка |
| `hpnn-be1`, `hpnn-be3` | web-сервис на Go в приватном сегменте 2 |
| `hpnn-be2`, `hpnn-be4` | тот же сервис в приватном сегменте 3 |
### Адресный план
| Назначение | Адрес |
|---|---|
| Клиентская сеть | `192.168.5.0/24` |
| Адрес узла в публичном сегменте | `192.168.5.20` |
| **VIP балансировщика** | **`192.168.5.21:80`** |
| Шлюз 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 + сборка образов + запуск
make verify # проверки
make down # остановка
```
Проверка с самого хоста требует 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
```
```
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 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 | Классификация по входному порту; обучение MAC клиентов для обратного пути |
| 5 | ARP-респондер для адресов узла, VIP и источников проб |
| 6 | ICMP echo-респондер для адресов узла и VIP |
| 10 | Листенеры: `VIP:80` → `multipath` кладёт номер слота в `reg1`; прочий IP → маршрутизация |
| 11 | **Таблица слотов**: `reg1` → `reg2` (член пула). Ведёт демон `hcd` |
| 12 | DNAT выбранным членом пула (`ct(commit, nat)`) |
| 15, 16 | Обратный путь: `ct(nat)` снимает DNAT, источник снова становится VIP |
| 20 | Маршрутизация: приоритет = длина префикса, `dec_ttl`, MAC источника |
| 21 | Adjacency: MAC next-hop и выходной порт |
Регистры: `reg1` — номер слота, `reg2` — идентификатор члена пула.
Нумерация таблиц не произвольна: `goto_table` разрешает переход только вперёд,
поэтому обратный путь (15/16) стоит до общей маршрутизации (20).
### Чего нет у чисто-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 проверки стенда
```