Добавлена локальная локальная балансировка
This commit is contained in:
1 parent
afd4f057be
commit
d80a6c44b0
17 files changed
+1628
-369
No files matched your search
@@ -1,16 +1,21 @@
|
||||
SHELL := /bin/bash
|
||||
DC := docker compose
|
||||
# Скрипты подготовки хоста требуют root. Под root sudo не нужен и может
|
||||
# отсутствовать в системе.
|
||||
SUDO := $(shell [ "$$(id -u)" = 0 ] || echo sudo)
|
||||
|
||||
.PHONY: help prereq shim shim-down build up down restart flows verify logs ports health slots drain enable metrics conns trace shell
|
||||
.PHONY: help prereq shim shim-down build up down restart attach detach flows verify logs ports health slots drain enable metrics conns trace fdb shell
|
||||
|
||||
help:
|
||||
@echo "Стенд hpnn_v2 — шаг 1 (OVS: маршрутизация + балансировка)"
|
||||
@echo "Стенд hpnn_v2 — OVS: маршрутизация, балансировка, коммутация сегментов"
|
||||
@echo
|
||||
@echo " make prereq загрузить модуль openvswitch на хосте"
|
||||
@echo " make shim поднять macvlan-shim (проверка стенда с самого хоста)"
|
||||
@echo " make shim-down снять shim"
|
||||
@echo " make up собрать образы и запустить стенд"
|
||||
@echo " make down остановить стенд и удалить сети"
|
||||
@echo " make attach подключить ВМ сегментов к br-lb (после пересоздания)"
|
||||
@echo " make detach снять порты сегментов с моста"
|
||||
@echo " make flows перечитать pipeline.sh и перезалить пайплайн"
|
||||
@echo " make verify прогнать проверки"
|
||||
@echo " make ports порты моста br-lb"
|
||||
@@ -20,18 +25,19 @@ help:
|
||||
@echo " make enable M=be1 вернуть член в балансировку"
|
||||
@echo " make metrics метрики Prometheus"
|
||||
@echo " make conns таблица соединений ct"
|
||||
@echo " make trace SRC=192.168.5.7 [SPORT=40000] ofproto/trace сессии на VIP"
|
||||
@echo " make trace SRC=192.168.5.7 [SPORT=40000] [SEG=pub|p2|p3] ofproto/trace"
|
||||
@echo " make fdb таблица коммутации сегментов (выученные MAC)"
|
||||
@echo " make logs логи контейнеров"
|
||||
@echo " make shell shell в контейнере lb-router"
|
||||
|
||||
prereq:
|
||||
sudo scripts/host-prereq.sh
|
||||
$(SUDO) scripts/host-prereq.sh
|
||||
|
||||
shim:
|
||||
sudo scripts/host-prereq.sh --shim
|
||||
$(SUDO) scripts/host-prereq.sh --shim
|
||||
|
||||
shim-down:
|
||||
sudo scripts/host-prereq.sh --shim-down
|
||||
$(SUDO) scripts/host-prereq.sh --shim-down
|
||||
|
||||
build:
|
||||
$(DC) build
|
||||
@@ -43,8 +49,19 @@ up: prereq
|
||||
$(DC) exec -T lb-router curl -fsS --max-time 2 http://127.0.0.1:9111/status >/dev/null 2>&1 && break; \
|
||||
sleep 1; \
|
||||
done
|
||||
@$(MAKE) --no-print-directory attach
|
||||
@echo "Ждём, пока health-пробы увидят членов пула..."
|
||||
@sleep 6
|
||||
@$(MAKE) --no-print-directory health
|
||||
|
||||
# Порты ВМ сегментов создаёт гипервизор; в стенде их создаёт этот скрипт.
|
||||
# Повторить нужно после пересоздания любого контейнера сегмента.
|
||||
attach:
|
||||
$(SUDO) scripts/attach-segment.sh
|
||||
|
||||
detach:
|
||||
$(SUDO) scripts/attach-segment.sh --detach
|
||||
|
||||
down:
|
||||
$(DC) down
|
||||
|
||||
@@ -82,7 +99,10 @@ conns:
|
||||
$(DC) exec -T lb-router lbctl conns
|
||||
|
||||
trace:
|
||||
$(DC) exec -T lb-router lbctl trace $(SRC) $(SPORT)
|
||||
$(DC) exec -T lb-router lbctl trace $(SRC) $(SPORT) $(SEG)
|
||||
|
||||
fdb:
|
||||
$(DC) exec -T lb-router lbctl fdb
|
||||
|
||||
shell:
|
||||
$(DC) exec lb-router bash
|
||||
@@ -1,11 +1,16 @@
|
||||
# hpnn_v2 — стенд OVS-маршрутизатора и балансировщика нагрузки
|
||||
|
||||
Контейнерный прототип основы сервиса: один узел на Open vSwitch, который
|
||||
одновременно выполняет две функции — **маршрутизацию** между сегментами и
|
||||
**балансировку нагрузки** на четыре бэкенда. Всё форвардинг-решение принимается
|
||||
в OpenFlow: сетевой стек ядра контейнера транзитный трафик не обрабатывает.
|
||||
Состав пула ведёт подсистема health-check: мёртвые бэкенды выводятся из
|
||||
балансировки, восстановившиеся возвращаются.
|
||||
выполняет три функции — **маршрутизацию** между сегментами, **балансировку
|
||||
нагрузки** на четыре бэкенда и **коммутацию приватных сегментов**. Всё
|
||||
форвардинг-решение принимается в OpenFlow: сетевой стек ядра контейнера
|
||||
транзитный трафик не обрабатывает. Состав пула ведёт подсистема health-check:
|
||||
мёртвые бэкенды выводятся из балансировки, восстановившиеся возвращаются.
|
||||
|
||||
Листенеры есть и в публичном сегменте, и в приватных. Приватный листенер
|
||||
обслуживает клиента, стоящего **в одном сегменте с бэкендами**, сохраняя его
|
||||
исходный IP и транслируя порт 80 на 8080 — при этом в ОС виртуальных машин не
|
||||
настраивается ничего.
|
||||
|
||||
Развивает дизайн-концепцию `../hpnn_v1/docs/design.md`.
|
||||
|
||||
@@ -14,9 +19,14 @@
|
||||
| 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/ROUTING_DATAFLOW.md](docs/ROUTING_DATAFLOW.md).
|
||||
Потоки данных описаны отдельно для каждого вида балансировки:
|
||||
[публичный трафик](docs/PUBLIC_LB_DATAFLOW.md) — клиент из клиентской сети на
|
||||
`192.168.5.21:80`, целиком маршрутизируемый путь;
|
||||
[приватный трафик](docs/PRIVATE_LB_DATAFLOW.md) — клиент внутри приватного
|
||||
сегмента, рядом с бэкендами, на `10.20.0.100:80`.
|
||||
|
||||
## Топология
|
||||
|
||||
@@ -25,23 +35,34 @@
|
||||
│
|
||||
[ 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 (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-lb` | OVS: маршрутизация, балансировка, коммутация сегментов |
|
||||
| `hpnn-be1`, `hpnn-be3` | web-сервис на Go в приватном сегменте 2 |
|
||||
| `hpnn-be2`, `hpnn-be4` | тот же сервис в приватном сегменте 3 |
|
||||
| `hpnn-cli2`, `hpnn-cli3` | клиенты внутри сегментов; в их ОС настроен только адрес |
|
||||
|
||||
### Адресный план
|
||||
|
||||
@@ -49,7 +70,9 @@
|
||||
|---|---|
|
||||
| Клиентская сеть | `192.168.5.0/24` |
|
||||
| Адрес узла в публичном сегменте | `192.168.5.20` |
|
||||
| **VIP балансировщика** | **`192.168.5.21:80`** |
|
||||
| **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` |
|
||||
@@ -64,11 +87,15 @@ OpenFlow** — на интерфейсах они не настроены. ARP-
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
make up # modprobe openvswitch + сборка образов + запуск
|
||||
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 не нужен.
|
||||
@@ -91,6 +118,21 @@ 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
|
||||
@@ -187,6 +229,8 @@ docker compose unpause be1 # возвращается за rise × interval =
|
||||
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
|
||||
@@ -200,20 +244,43 @@ docker compose exec lb-router lbctl status # состояние пула в
|
||||
|
||||
| Таблица | Назначение |
|
||||
|---|---|
|
||||
| 0 | Классификация по входному порту; обучение MAC клиентов для обратного пути |
|
||||
| 5 | ARP-респондер для адресов узла, VIP и источников проб |
|
||||
| 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 выбранным членом пула (`ct(commit, nat)`) |
|
||||
| 15, 16 | Обратный путь: `ct(nat)` снимает DNAT, источник снова становится VIP |
|
||||
| 12 | Применение члена: DNAT и выдача — коммутацией либо через маршрутизацию |
|
||||
| 15, 16 | Обратный путь L3: `ct(nat)` снимает DNAT, источник снова становится VIP |
|
||||
| 20 | Маршрутизация: приоритет = длина префикса, `dec_ttl`, MAC источника |
|
||||
| 21 | Adjacency: MAC next-hop и выходной порт |
|
||||
| 24 | Обратная трансляция коммутируемого трафика сегмента |
|
||||
| 25 | L2-коммутация сегмента: FDB, broadcast и рассылка неизвестного unicast |
|
||||
|
||||
Регистры: `reg1` — номер слота, `reg2` — идентификатор члена пула.
|
||||
Регистры: `reg0` — сегмент (1 — публичный, 2 и 3 — приватные), `reg1` — номер
|
||||
слота, `reg2` — идентификатор члена пула.
|
||||
|
||||
Нумерация таблиц не произвольна: `goto_table` разрешает переход только вперёд,
|
||||
поэтому обратный путь (15/16) стоит до общей маршрутизации (20).
|
||||
поэтому обратный путь (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 узла
|
||||
|
||||
|
||||
+13
-6
@@ -5,17 +5,24 @@
|
||||
# - через балансировщик маршрутизируются только клиентские префиксы и
|
||||
# соседний приватный сегмент, то есть ответы на балансируемые сессии и
|
||||
# транзит между сегментами.
|
||||
#
|
||||
# С шага 3 порт сегмента создаёт гипервизор (в стенде —
|
||||
# scripts/attach-segment.sh), он же назначает адрес и эти маршруты: внутри ОС
|
||||
# бэкенда не настраивается ничего. Переменные ниже оставлены для запуска стенда
|
||||
# в прежней схеме, с docker-сетями.
|
||||
set -eu
|
||||
|
||||
: "${LB_GW:?не задан LB_GW}"
|
||||
: "${LB_GW:=}"
|
||||
: "${VIA_LB_ROUTES:=}"
|
||||
|
||||
for net in $(echo "$VIA_LB_ROUTES" | tr ',' ' '); do
|
||||
ip route replace "$net" via "$LB_GW"
|
||||
echo "[backend] маршрут $net via $LB_GW"
|
||||
done
|
||||
if [ -n "$LB_GW" ]; then
|
||||
for net in $(echo "$VIA_LB_ROUTES" | tr ',' ' '); do
|
||||
ip route replace "$net" via "$LB_GW"
|
||||
echo "[backend] маршрут $net via $LB_GW"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "[backend] таблица маршрутизации:"
|
||||
ip route
|
||||
ip route || true
|
||||
|
||||
exec /usr/local/bin/backend "$@"
|
||||
+50
-32
@@ -30,7 +30,15 @@ services:
|
||||
mac_address: "02:42:0a:1e:00:01"
|
||||
restart: unless-stopped
|
||||
|
||||
# Контейнер 2: бэкенд в приватном сегменте 2.
|
||||
# Бэкенды и клиенты приватных сегментов (шаг 3).
|
||||
#
|
||||
# network_mode: none — у контейнера нет ни одной docker-сети. Порт сегмента
|
||||
# создаёт scripts/attach-segment.sh: veth напрямую в br-lb, адрес, MAC и
|
||||
# маршруты назначаются снаружи. Так стенд повторяет целевую среду, где ВМ
|
||||
# подключены к vSwitch, и балансировщик видит весь трафик сегмента — включая
|
||||
# ответ бэкенда клиенту-соседу по подсети.
|
||||
#
|
||||
# Адреса и MAC перечислены в lb/topology.env — единственном источнике правды.
|
||||
be1:
|
||||
build: ./backend
|
||||
container_name: hpnn-be1
|
||||
@@ -38,16 +46,10 @@ services:
|
||||
cap_add: [NET_ADMIN]
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
LB_GW: 10.20.0.1
|
||||
VIA_LB_ROUTES: "192.168.5.0/24,10.30.0.0/24"
|
||||
networks:
|
||||
priv2:
|
||||
ipv4_address: 10.20.0.2
|
||||
mac_address: "02:42:0a:14:00:02"
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
# Контейнер 3: копия контейнера 2 в приватном сегменте 3.
|
||||
be2:
|
||||
build: ./backend
|
||||
container_name: hpnn-be2
|
||||
@@ -55,18 +57,12 @@ services:
|
||||
cap_add: [NET_ADMIN]
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
LB_GW: 10.30.0.1
|
||||
VIA_LB_ROUTES: "192.168.5.0/24,10.20.0.0/24"
|
||||
networks:
|
||||
priv3:
|
||||
ipv4_address: 10.30.0.2
|
||||
mac_address: "02:42:0a:1e:00:02"
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
# Второй бэкенд в приватном сегменте 2. Четыре члена пула вместо двух дают
|
||||
# содержательную проверку раскладки слотов: при выбытии одного члена его
|
||||
# слоты делятся между тремя оставшимися, а не достаются единственному.
|
||||
# Четыре члена пула вместо двух дают содержательную проверку раскладки слотов:
|
||||
# при выбытии одного члена его слоты делятся между тремя оставшимися.
|
||||
be3:
|
||||
build: ./backend
|
||||
container_name: hpnn-be3
|
||||
@@ -74,16 +70,10 @@ services:
|
||||
cap_add: [NET_ADMIN]
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
LB_GW: 10.20.0.1
|
||||
VIA_LB_ROUTES: "192.168.5.0/24,10.30.0.0/24"
|
||||
networks:
|
||||
priv2:
|
||||
ipv4_address: 10.20.0.3
|
||||
mac_address: "02:42:0a:14:00:03"
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
# Второй бэкенд в приватном сегменте 3.
|
||||
be4:
|
||||
build: ./backend
|
||||
container_name: hpnn-be4
|
||||
@@ -91,12 +81,36 @@ services:
|
||||
cap_add: [NET_ADMIN]
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
LB_GW: 10.30.0.1
|
||||
VIA_LB_ROUTES: "192.168.5.0/24,10.20.0.0/24"
|
||||
networks:
|
||||
priv3:
|
||||
ipv4_address: 10.30.0.3
|
||||
mac_address: "02:42:0a:1e:00:03"
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
# Клиенты внутри приватных сегментов — те самые ВМ, что живут рядом с
|
||||
# бэкендами. Образ бэкенда переиспользован ради curl и iproute2; сервис не
|
||||
# запускается. В их ОС не настраивается ничего, кроме адреса.
|
||||
cli2:
|
||||
build: ./backend
|
||||
container_name: hpnn-cli2
|
||||
hostname: cli2
|
||||
entrypoint: ["sleep", "infinity"]
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
cli3:
|
||||
build: ./backend
|
||||
container_name: hpnn-cli3
|
||||
hostname: cli3
|
||||
entrypoint: ["sleep", "infinity"]
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
network_mode: none
|
||||
depends_on: [lb-router]
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -118,8 +132,12 @@ networks:
|
||||
config:
|
||||
- subnet: 192.168.55.0/24
|
||||
|
||||
# Сеть 2 — приватный сегмент бэкенда be1. Шлюз Docker смещён на .254,
|
||||
# чтобы адрес .1 занял OVS-роутер и не конфликтовал с host-бриджем.
|
||||
# Сеть 2 — приватный сегмент 10.20.0.0/24. С шага 3 к ней подключён только
|
||||
# балансировщик: его порт p2 служит аплинком сегмента к шлюзу Docker (.254),
|
||||
# через который бэкенды выходят наружу (профиль A). ВМ сегмента подключены
|
||||
# напрямую к br-lb — см. scripts/attach-segment.sh.
|
||||
#
|
||||
# Шлюз Docker смещён на .254, чтобы адрес .1 занял OVS-роутер.
|
||||
priv2:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# План: разделение документа потоков данных
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Итоги:** [CHANGE_SPLIT_DATAFLOW_DOCS_SUMMARY.md](CHANGE_SPLIT_DATAFLOW_DOCS_SUMMARY.md)
|
||||
|
||||
## Задача
|
||||
|
||||
`docs/ROUTING_DATAFLOW.md` после шага 3 описывает сразу два разных сценария
|
||||
балансировки — публичный и приватный — в одном наборе диаграмм. Читателю,
|
||||
которому нужен конкретный сценарий, приходится вычитывать его из общего
|
||||
конвейера и мысленно отбрасывать чужие ветки.
|
||||
|
||||
Разделить на два самостоятельных документа: по одному на каждый вид
|
||||
балансировки.
|
||||
|
||||
## Что делаем
|
||||
|
||||
1. **`docs/PUBLIC_LB_DATAFLOW.md`** — балансировка публичного трафика: клиент из
|
||||
`192.168.5.0/24`, VIP `192.168.5.21:80`, маршрутизируемый путь до члена пула
|
||||
и обратно. Сюда же — ARP- и ICMP-респондеры публичного сегмента, обучение
|
||||
adjacency клиента, обратный путь через `ct`, профиль A как условие возврата
|
||||
ответа.
|
||||
2. **`docs/PRIVATE_LB_DATAFLOW.md`** — балансировка приватного трафика: клиент
|
||||
внутри сегмента, VIP `10.20.0.100:80` / `10.30.0.100:80`, два пути выдачи
|
||||
(коммутируемый для соседа по сегменту и маршрутизируемый для члена из
|
||||
другого сегмента), обратная трансляция в таблице 24, коммутация в таблице 25.
|
||||
3. **Удалить `docs/ROUTING_DATAFLOW.md`** — его содержимое целиком переходит в
|
||||
два новых документа.
|
||||
4. Обновить ссылки: `README.md`, `docs/STEP3_IMPLEMENTATION_PLAN.md`.
|
||||
|
||||
## Принципы разделения
|
||||
|
||||
- Каждый документ самодостаточен: своя топология портов, своя карта таблиц,
|
||||
свои точки обрыва потока и свои ограничения. Дублирование общих сведений
|
||||
допустимо — читатель не должен ходить между файлами.
|
||||
- В карте таблиц каждого документа перечислены только таблицы, участвующие в
|
||||
его сценарии, с пометкой о назначении именно в этом пути.
|
||||
- Общие подсистемы (health-check, транзит между сегментами, egress бэкендов)
|
||||
описываются в том документе, где они влияют на путь: health-check — кратко в
|
||||
обоих, транзит и коммутация сегмента — в приватном.
|
||||
- Диаграммы mermaid сохраняются, но каждая перерисовывается под свой сценарий
|
||||
без чужих ветвей.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Ни в одном из двух документов нет диаграммы, смешивающей публичный и
|
||||
приватный путь.
|
||||
- В README есть ссылки на оба документа с пояснением, какой для чего.
|
||||
- Ссылок на удалённый `ROUTING_DATAFLOW.md` в репозитории не осталось.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Итоги: разделение документа потоков данных
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Статус:** выполнено
|
||||
**План:** [CHANGE_SPLIT_DATAFLOW_DOCS_PLAN.md](CHANGE_SPLIT_DATAFLOW_DOCS_PLAN.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
`docs/ROUTING_DATAFLOW.md` удалён, вместо него — два самостоятельных документа,
|
||||
по одному на каждый вид балансировки:
|
||||
|
||||
| Документ | Сценарий | Характер пути |
|
||||
|---|---|---|
|
||||
| [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md) | клиент из `192.168.5.0/24` → `192.168.5.21:80` | целиком маршрутизируемый, `dec_ttl` в обе стороны |
|
||||
| [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) | клиент внутри сегмента → `10.20.0.100:80` | коммутируемый для соседа по сегменту, маршрутизируемый для члена из другого |
|
||||
|
||||
Каждый документ самодостаточен: своя схема участников, свой конвейер таблиц,
|
||||
свои потоки, таблица решений, точки обрыва, команды осмотра и ограничения.
|
||||
Читатель, которому нужен один сценарий, не встречает ветвей другого.
|
||||
|
||||
## Как поделено содержимое
|
||||
|
||||
- **Публичный документ**: ARP- и ICMP-респондеры публичного сегмента, обучение
|
||||
adjacency клиента, листенер `VIP:80`, DNAT, маршрутизация 20/21, обратный путь
|
||||
через таблицу 15, профиль A как условие возврата ответа.
|
||||
- **Приватный документ**: предпосылка «порт ВМ — порт OVS», сегментация и
|
||||
обучение FDB в таблице 0, классификация по MAC назначения в таблице 1, два
|
||||
пути выдачи в таблице 12, обратная трансляция в таблице 24, коммутация в
|
||||
таблице 25, а также остальной трафик сегмента — `be↔be`, прямые обращения
|
||||
клиента, egress, ARP и health-пробы.
|
||||
- **Общее** (таблица слотов, `pool_id`, fail-close, health-check) упомянуто в
|
||||
обоих ровно в той мере, в какой участвует в пути: пул и хэш у листенеров
|
||||
общие, и это сказано в каждом документе.
|
||||
|
||||
Каждая диаграмма перерисована под свой сценарий: в публичном конвейере нет
|
||||
таблиц 24 и 25, в приватном — ветви `reg0=1`.
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
- добавлены `docs/PUBLIC_LB_DATAFLOW.md`, `docs/PRIVATE_LB_DATAFLOW.md`;
|
||||
- удалён `docs/ROUTING_DATAFLOW.md`;
|
||||
- `README.md` — вместо одной ссылки две, с пояснением, какая для чего;
|
||||
- `docs/STEP3_IMPLEMENTATION_PLAN.md` — ссылка в перечне документации.
|
||||
|
||||
Ссылок на удалённый документ в репозитории не осталось, кроме упоминаний в
|
||||
плане и в этих итогах — там они описывают само изменение.
|
||||
+150
-2
@@ -8,6 +8,10 @@
|
||||
нужен, чтобы увидеть поведение стенда своими глазами и понять, что означает
|
||||
каждый результат.
|
||||
|
||||
Что происходит с пакетом на каждом шаге — в документах по потокам данных:
|
||||
[публичная балансировка](PUBLIC_LB_DATAFLOW.md) для блоков A–C и E–I,
|
||||
[приватная балансировка](PRIVATE_LB_DATAFLOW.md) для блока J.
|
||||
|
||||
## Обозначения
|
||||
|
||||
| Метка | Где выполнять |
|
||||
@@ -26,8 +30,9 @@ cd /opt/lvraid/claude/hpnn_v2
|
||||
make up
|
||||
```
|
||||
|
||||
Ожидается: пять контейнеров в состоянии `healthy` и таблица состояния пула,
|
||||
где все четыре члена `up` и у каждого по 256 слотов.
|
||||
Ожидается: семь контейнеров и таблица состояния пула, где все четыре члена
|
||||
`up` и у каждого по 256 слотов. `make up` сам вызывает `make attach` —
|
||||
подключение ВМ приватных сегментов к мосту.
|
||||
|
||||
**Шаг 0.2 [Х]** — убедиться, что всё запустилось:
|
||||
|
||||
@@ -42,9 +47,20 @@ hpnn-be1 Up ... (healthy)
|
||||
hpnn-be2 Up ... (healthy)
|
||||
hpnn-be3 Up ... (healthy)
|
||||
hpnn-be4 Up ... (healthy)
|
||||
hpnn-cli2 Up ...
|
||||
hpnn-cli3 Up ...
|
||||
hpnn-lb Up ... (healthy)
|
||||
```
|
||||
|
||||
Проверьте, что ВМ сегментов подключены к мосту:
|
||||
|
||||
```bash
|
||||
make ports
|
||||
```
|
||||
|
||||
Ожидается 11 портов: `pub0`, `p2`, `p3`, `hcif-p2`, `hcif-p3`, `be1`, `be3`,
|
||||
`cli2`, `be2`, `be4`, `cli3`. Если портов ВМ нет — `make attach`.
|
||||
|
||||
Если `hpnn-lb` в состоянии `Restarting` — смотрите `docker compose logs
|
||||
lb-router` и раздел «Диагностика» в конце документа.
|
||||
|
||||
@@ -538,6 +554,118 @@ make enable M=be2
|
||||
|
||||
---
|
||||
|
||||
## Блок J. Листенер в приватном сегменте
|
||||
|
||||
Главное шага 3: клиент стоит в одном сегменте с бэкендами, и его исходный адрес
|
||||
на бэкенде сохраняется. Все шаги выполняются на хосте — клиентом служит
|
||||
контейнер `cli2`.
|
||||
|
||||
**Шаг J.1 [Х]** — убедиться, что в ОС клиента ничего не настроено:
|
||||
|
||||
```bash
|
||||
docker compose exec cli2 ip route
|
||||
```
|
||||
|
||||
Ожидается ровно одна строка — connected-маршрут `10.20.0.0/24 dev eth0`. Ни
|
||||
default, ни маршрута на VIP: адрес листенера находится в подсети клиента.
|
||||
|
||||
**Шаг J.2 [Х]** — доступность VIP сегмента:
|
||||
|
||||
```bash
|
||||
docker compose exec cli2 ping -c2 10.20.0.100
|
||||
```
|
||||
|
||||
**Шаг J.3 [Х]** — запрос к сервису через балансировщик:
|
||||
|
||||
```bash
|
||||
docker compose exec cli2 curl -s http://10.20.0.100/
|
||||
```
|
||||
|
||||
Ожидается строка вида:
|
||||
|
||||
```
|
||||
backend=be1 time=... client=10.20.0.10:46064 served=10.20.0.2:8080 req=1
|
||||
```
|
||||
|
||||
Здесь два ключевых поля: `client` — исходный адрес клиента (SNAT не
|
||||
выполняется), `served` — адрес и порт бэкенда, то есть листенер принял на 80 и
|
||||
оттранслировал на 8080.
|
||||
|
||||
**Шаг J.4 [Х]** — распределение по пулу:
|
||||
|
||||
```bash
|
||||
docker compose exec cli2 sh -c 'for i in $(seq 24); do curl -s http://10.20.0.100/; done' \
|
||||
| sort | uniq -c -w 12
|
||||
```
|
||||
|
||||
Ожидается: встречаются все четыре бэкенда. `be1` и `be3` — соседи клиента по
|
||||
сегменту (коммутируемый путь), `be2` и `be4` — из другого сегмента
|
||||
(маршрутизируемый).
|
||||
|
||||
**Шаг J.5 [Х]** — увидеть разницу путей по TTL. В одном терминале:
|
||||
|
||||
```bash
|
||||
docker compose exec lb-router tcpdump -ni cli2 -v \
|
||||
'tcp and src host 10.20.0.100 and tcp[tcpflags] & tcp-syn != 0'
|
||||
```
|
||||
|
||||
Во втором — десяток запросов из шага J.3.
|
||||
|
||||
Ожидается: у ответов **ttl 64** и **ttl 63**. Первые пришли от `be1`/`be3` —
|
||||
для них балансировщик не был L3-хопом, он лишь подменил MAC и оттранслировал
|
||||
адрес. Вторые — от `be2`/`be4` через маршрутизацию, с уменьшением TTL.
|
||||
|
||||
**Шаг J.6 [Х]** — путь пакета по таблицам:
|
||||
|
||||
```bash
|
||||
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2
|
||||
```
|
||||
|
||||
Ожидается цепочка `0 → 1 → 10 → 11 → 12`, затем после `ct(...nat(dst=...))` —
|
||||
`Resuming from table 25` и вывод в порт члена. В `Final flow` видно, что
|
||||
`nw_src` остался клиентским, а `nw_ttl` **не уменьшен**.
|
||||
|
||||
**Шаг J.7 [Х]** — внутрисегментный трафик не блокирован:
|
||||
|
||||
```bash
|
||||
docker compose exec cli2 curl -s http://10.20.0.2:8080/ # клиент к бэкенду напрямую
|
||||
docker compose exec be1 curl -s http://10.20.0.3:8080/ # бэкенд к бэкенду
|
||||
docker compose exec be1 ping -c2 10.20.0.254 # шлюз Docker
|
||||
```
|
||||
|
||||
Ожидается: все три работают. Балансировщик коммутирует этот трафик, не
|
||||
транслируя его.
|
||||
|
||||
**Шаг J.8 [Х]** — таблица коммутации:
|
||||
|
||||
```bash
|
||||
make fdb
|
||||
```
|
||||
|
||||
Ожидается: по строке на каждую ВМ сегмента с её MAC и портом, счётчики растут.
|
||||
|
||||
**Шаг J.9 [Х]** — выключить листенер в одном сегменте:
|
||||
|
||||
```bash
|
||||
sed -i 's/^PRIV_LISTENERS=.*/PRIV_LISTENERS="p2"/' lb/topology.env
|
||||
docker compose cp lb/topology.env lb-router:/opt/lb/topology.env
|
||||
docker compose exec lb-router /opt/lb/apply.sh
|
||||
|
||||
docker compose exec cli3 curl -s --max-time 4 http://10.30.0.100/ # таймаут
|
||||
docker compose exec cli3 curl -s http://10.30.0.2:8080/ # связность цела
|
||||
docker compose exec cli2 curl -s http://10.20.0.100/ # второй сегмент работает
|
||||
```
|
||||
|
||||
Вернуть обратно:
|
||||
|
||||
```bash
|
||||
sed -i 's/^PRIV_LISTENERS=.*/PRIV_LISTENERS="p2 p3"/' lb/topology.env
|
||||
docker compose cp lb/topology.env lb-router:/opt/lb/topology.env
|
||||
docker compose exec lb-router /opt/lb/apply.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Итоговый чек-лист
|
||||
|
||||
| # | Проверка | Результат |
|
||||
@@ -560,6 +688,12 @@ make enable M=be2
|
||||
| G | При пустом пуле трафик отбрасывается, счётчик растёт | ☐ |
|
||||
| H | `make flows` и перезапуск не ломают раскладку | ☐ |
|
||||
| I | Установленная сессия переживает дренаж | ☐ |
|
||||
| J | В ОС клиента сегмента нет ни одного маршрута | ☐ |
|
||||
| J | Бэкенд видит исходный IP клиента-соседа | ☐ |
|
||||
| J | Листенер 80 транслирует на порт бэкенда 8080 | ☐ |
|
||||
| J | Ответ от соседа по сегменту приходит с неуменьшенным TTL | ☐ |
|
||||
| J | Трафик между ВМ сегмента не блокирован | ☐ |
|
||||
| J | `PRIV_LISTENERS` выключает листенер, не трогая связность | ☐ |
|
||||
|
||||
---
|
||||
|
||||
@@ -686,6 +820,8 @@ DNAT. Сравнение показателей двух путей даёт ц
|
||||
| Ключ хэша (алгоритм балансировки) | `lb/pipeline.sh`, таблица 10 | `make flows` |
|
||||
| Число слотов, basis хэша | `lb/topology.env`: `SLOTS`, `POOL_ID` | пересборка образа |
|
||||
| Состав пула на лету | — | `make drain` / `make enable` |
|
||||
| Приватные листенеры (какие сегменты) | `lb/topology.env`, `PRIV_LISTENERS` | `make flows` после копирования файла |
|
||||
| Адрес приватного VIP | `lb/topology.env`, `P2_VIP` / `P3_VIP` | то же |
|
||||
|
||||
«Пересборка образа» — это:
|
||||
|
||||
@@ -829,6 +965,18 @@ SLOTS=1024 # гранулярность весов
|
||||
|
||||
## Диагностика
|
||||
|
||||
**Клиент сегмента не получает ответ от приватного VIP, или ответ приходит не
|
||||
от VIP.**
|
||||
|
||||
Первая гипотеза — ВМ не подключены к мосту: `make ports` должен показывать
|
||||
порты `be1`, `be3`, `cli2`, `be2`, `be4`, `cli3`. Вручную созданные veth
|
||||
исчезают вместе с контейнером, поэтому после `docker compose restart be1` или
|
||||
пересоздания любого контейнера сегмента нужен `make attach`.
|
||||
|
||||
Если порты на месте — смотрите `make fdb` (выучен ли MAC клиента) и счётчики
|
||||
таблицы 24 (`docker compose exec lb-router lbctl flows 24`): именно она снимает
|
||||
трансляцию с ответа бэкенда соседу.
|
||||
|
||||
**Контейнер `hpnn-lb` перезапускается.**
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Потоки данных: балансировка приватного трафика
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Область:** путь пакета от клиента внутри приватного сегмента на приватный
|
||||
листенер (`10.20.0.100:80` или `10.30.0.100:80`) и обратно.
|
||||
**Смежный документ:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md) —
|
||||
балансировка публичного трафика.
|
||||
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP3_SUMMARY.md](STEP3_SUMMARY.md).
|
||||
|
||||
Отличие от публичного случая: клиент может стоять **в одном сегменте с
|
||||
бэкендами**. Тогда ответ бэкенда уходит клиенту по L2 напрямую, минуя
|
||||
маршрутизацию, и снять с него трансляцию можно только там, где этот кадр виден.
|
||||
Поэтому в приватном сегменте узел работает **коммутатором**: каждая ВМ
|
||||
подключена отдельным портом моста.
|
||||
|
||||
Предпосылка всей схемы: **порт ВМ — порт OVS**. В целевой среде это выполняется
|
||||
по построению (ВМ подключены к vSwitch), в стенде порты создаёт
|
||||
`scripts/attach-segment.sh` (`make attach`). При этом в ОС виртуальных машин не
|
||||
настраивается ничего, адресация заказчика не меняется, а трафик внутри сегмента
|
||||
не блокируется.
|
||||
|
||||
## 1. Участники сегмента
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BR["br-lb — OpenFlow datapath"]
|
||||
direction TB
|
||||
subgraph S2["сегмент 2 — 10.20.0.0/24 · reg0 = 2"]
|
||||
P2["p2 · 2<br/>аплинк к шлюзу Docker"]
|
||||
HC2["hcif-p2 · 4<br/>10.20.0.253 <b>в ядре</b>"]
|
||||
B1["be1 · 10"]
|
||||
B3["be3 · 11"]
|
||||
C2["cli2 · 12"]
|
||||
end
|
||||
end
|
||||
|
||||
CLI2["cli2 10.20.0.10<br/><i>клиент: настроен только адрес</i>"]
|
||||
BE1["be1 10.20.0.2:8080"]
|
||||
BE3["be3 10.20.0.3:8080"]
|
||||
GW["шлюз Docker 10.20.0.254<br/><i>egress бэкендов</i>"]
|
||||
OTHER["сегмент 3 — 10.30.0.0/24<br/>be2, be4, cli3"]
|
||||
|
||||
C2 <--> CLI2
|
||||
B1 <--> BE1
|
||||
B3 <--> BE3
|
||||
P2 <--> GW
|
||||
S2 -.->|"маршрутизация<br/>таблицы 20/21"| OTHER
|
||||
```
|
||||
|
||||
Шлюз сегмента `10.20.0.1` и VIP листенера `10.20.0.100` отвечают **одним MAC** —
|
||||
`02:42:0a:14:00:01`. По нему пайплайн отличает трафик к маршрутизатору от
|
||||
трафика между ВМ. Сегмент 3 устроен симметрично.
|
||||
|
||||
Какие сегменты имеют листенер, задаёт `PRIV_LISTENERS` в `topology.env`: пусто —
|
||||
ни одного, `"p2"` — один, `"p2 p3"` — оба.
|
||||
|
||||
## 2. Конвейер таблиц приватного пути
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
IN(["Кадр с порта ВМ"]) --> T0["<b>0</b> сегмент по in_port → reg0<br/><b>learn</b> FDB (eth_src → порт)<br/><b>learn</b> adjacency (для адресов своей подсети)"]
|
||||
T0 --> T1{"<b>1</b> классификация<br/>по MAC назначения"}
|
||||
|
||||
T1 -->|"arp"| T5["<b>5</b> ARP-респондер<br/>за VIP, шлюз, .253"]
|
||||
T1 -->|"icmp echo на VIP<br/>или шлюз"| T6["<b>6</b> ICMP-респондер"]
|
||||
T1 -->|"dl_dst = MAC шлюза<br/>+ nw_dst = VIP:80"| T10
|
||||
T1 -->|"dl_dst = MAC шлюза<br/>прочий IP"| T15["<b>15</b> обратный путь L3<br/>ct(nat) — для сессий<br/>из другого сегмента"]
|
||||
T1 -->|"прочее —<br/>трафик между ВМ"| T24
|
||||
|
||||
T5 -->|"не наш адрес"| T25
|
||||
T10{"<b>10</b> листенер<br/>tcp VIP:80"} --> T11["<b>11</b> таблица слотов<br/>reg1 → reg2 (общая<br/>с публичным листенером)"]
|
||||
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
|
||||
T11 --> T12{"<b>12</b> применение члена"}
|
||||
|
||||
T12 -->|"<b>член в сегменте клиента</b><br/>mod_dl_dst + ct(commit,nat)<br/><b>без dec_ttl</b>"| T25
|
||||
T12 -->|"член в другом сегменте<br/>ct(commit,nat)"| T20["<b>20/21</b> маршрутизация<br/>dec_ttl, adjacency"]
|
||||
T20 --> OUTR(["output: порт члена<br/>в другом сегменте"])
|
||||
|
||||
T15 --> T16["<b>16</b> пост-ct"] --> T20
|
||||
T24{"<b>24</b> обратная трансляция<br/>сегмента"}
|
||||
T24 -->|"nw_src = член, tp_src = 8080<br/>ct(nat): src → VIP:80"| T25
|
||||
T24 -->|"прочее — без изменений"| T25
|
||||
T25{"<b>25</b> коммутация<br/>FDB по eth_dst"} --> OUTL(["output: порт ВМ"])
|
||||
T25 -->|"broadcast или<br/>неизвестный MAC"| FLOOD(["рассылка по сегменту,<br/>кроме входного порта"])
|
||||
|
||||
classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff
|
||||
class DROPF d
|
||||
```
|
||||
|
||||
## 3. Основной поток: клиент и член пула в одном сегменте
|
||||
|
||||
Тот самый случай, ради которого сегмент переведён на порты OVS.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant C as cli2 10.20.0.10
|
||||
participant OF as br-lb
|
||||
participant B as be1 10.20.0.2
|
||||
|
||||
C->>OF: ARP who-has 10.20.0.100
|
||||
Note over OF: t1 → t5: респондер за VIP
|
||||
OF-->>C: is-at 02:42:0a:14:00:01 (MAC шлюза сегмента)
|
||||
C->>OF: SYN 10.20.0.10 → 10.20.0.100:80, TTL 64
|
||||
Note over OF: t0: reg0=2, обучение FDB и adjacency
|
||||
Note over OF: t1: dl_dst = MAC шлюза, nw_dst = VIP → листенер
|
||||
Note over OF: t10 → t11: слот → член пула
|
||||
Note over OF: t12: mod_dl_dst = MAC be1,<br/>ct(commit, nat(dst=10.20.0.2:8080))<br/><b>dec_ttl не выполняется</b>
|
||||
OF->>B: t25: FDB → порт be1
|
||||
Note over B: client=10.20.0.10 — исходный адрес клиента<br/>served=10.20.0.2:8080 — трансляция 80 → 8080
|
||||
B->>OF: ответ src=10.20.0.2:8080 → 10.20.0.10,<br/>dst MAC = MAC клиента (сосед по L2)
|
||||
Note over OF: t1: dl_dst ≠ MAC шлюза → t24
|
||||
Note over OF: <b>t24</b>: ct(nat) → src = 10.20.0.100:80
|
||||
OF->>C: t25: FDB → порт cli2
|
||||
Note over C: ответ от 10.20.0.100:80, TTL 64
|
||||
```
|
||||
|
||||
Обратный кадр не адресован балансировщику вовсе — бэкенд отправляет его соседу
|
||||
напрямую. Через мост он проходит только потому, что порт ВМ — порт OVS. Именно
|
||||
здесь, в таблице 24, снимается трансляция.
|
||||
|
||||
Для ВМ ничего не изменилось: TTL не уменьшен, значит клиент и бэкенд по-прежнему
|
||||
L2-соседи, а балансировщик не стал для них маршрутным хопом.
|
||||
|
||||
## 4. Второй поток: член пула в другом сегменте
|
||||
|
||||
Тот же листенер, но слот выбрал члена из соседнего сегмента — работает обычная
|
||||
маршрутизация.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C["cli2<br/>10.20.0.10"] -->|"dst=10.20.0.100:80"| A["t0/t1: reg0=2,<br/>dl_dst = MAC шлюза"]
|
||||
A --> B["t10 → t11 → t12<br/>reg2 указывает на be2:<br/>DNAT 10.30.0.2:8080"]
|
||||
B --> D["t20: LPM /24 10.30.0.0<br/><b>dec_ttl</b>, src MAC = p3"]
|
||||
D --> E["t21: dst MAC = be2,<br/>output порт be2"]
|
||||
E --> BE2["be2<br/><i>client=10.20.0.10</i>"]
|
||||
BE2 -->|"маршрут 10.20.0.0/24<br/>via 10.30.0.1"| F["t1: dl_dst = MAC шлюза → t15<br/>ct(nat): src → 10.20.0.100:80"]
|
||||
F --> G["t20 → t21: adjacency клиента,<br/>выученная в t0"] --> C
|
||||
```
|
||||
|
||||
Клиенту оба пути неотличимы — ответ приходит от `10.20.0.100:80`. Разница видна
|
||||
только по TTL: **64** от соседа по сегменту против **63** через маршрутизацию.
|
||||
|
||||
## 5. Остальной трафик сегмента
|
||||
|
||||
Балансировка не мешает обычной работе сегмента: всё, что не адресовано VIP,
|
||||
коммутируется без трансляции.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["cli2 → be1:8080 напрямую,<br/>минуя VIP"] --> B["t1: dl_dst = MAC be1 → t24"]
|
||||
B --> C["t24: ct без записи —<br/>изменений нет"] --> D["t25: FDB → порт be1"]
|
||||
|
||||
E["be1 → be3:8080<br/>внутри сегмента"] --> F["t1 → t24 → t25"]
|
||||
F --> G["t25: FDB → порт be3"]
|
||||
|
||||
H["be1 → 1.1.1.1<br/>default via 10.20.0.254"] --> I["t1 → t24 → t25"]
|
||||
I --> J["t25: FDB → аплинк p2 →<br/>docker-бридж → NAT хоста<br/><i>профиль A: мимо балансировки</i>"]
|
||||
|
||||
K["ARP между ВМ"] --> L["t5: не наш адрес → t25"]
|
||||
L --> M["t25: рассылка по портам сегмента"]
|
||||
|
||||
N["health-проба<br/>10.20.0.253 → be1:8080"] --> O["t0: in_port = hcif-p2<br/>t1 → t24 → t25 → порт be1"]
|
||||
```
|
||||
|
||||
Про пробы: они уходят с уникального адреса узла `.253`, а не с VIP. Если бы
|
||||
источником был VIP, ответ бэкенда попал бы в обратную трансляцию и до пробера не
|
||||
дошёл. Правило таблицы 24 ответ на пробу видит, но `ct` записи для него не
|
||||
находит и пропускает без изменений.
|
||||
|
||||
## 6. Таблица решений приватного пути
|
||||
|
||||
| Таблица | Матч | Действие | Комментарий |
|
||||
|---|---|---|---|
|
||||
| 0 | `in_port` = порт сегмента | `reg0` + `learn` FDB | мост становится обучающимся коммутатором |
|
||||
| 0 | то же, `ip`, `nw_src` из подсети сегмента | + `learn` adjacency | нужно для возврата ответов из другого сегмента; с аплинка не обучаем, иначе в t21 попал бы внешний мир |
|
||||
| 1 | `dl_dst` = MAC шлюза, `nw_dst` = VIP, `tp_dst` = 80 | → t10 | листенер сегмента |
|
||||
| 1 | `dl_dst` = MAC шлюза, `ip`, `nw_dst` = VIP | drop | адрес занят, листенер выключен — видно по счётчику |
|
||||
| 1 | `dl_dst` = MAC шлюза, `ip` | → t15 | транзит и обратный путь L3 |
|
||||
| 1 | прочее | → t24 | трафик между ВМ |
|
||||
| 5 | `arp_tpa` = VIP, шлюз или `.253` | ответ `IN_PORT` | MAC-ом порта сегмента |
|
||||
| 5 | прочий ARP | → t25 | ВМ резолвят друг друга сами |
|
||||
| 10 | `tcp`, `nw_dst` = VIP сегмента | `multipath` → t11 | тот же `pool_id` и хэш, что у публичного |
|
||||
| 12 | `reg0` = сегмент члена | `mod_dl_dst` + `ct(commit,nat)` → t25 | **без `dec_ttl`** |
|
||||
| 12 | прочее | `ct(commit,nat)` → t20 | член из другого сегмента |
|
||||
| 24 | `nw_src` = член, `tp_src` = 8080 | `ct(nat)` → t25 | снимает DNAT с ответа соседу |
|
||||
| 24 | prio 0 | → t25 | обычный трафик проходит нетронутым |
|
||||
| 25 | `eth_dst` из FDB | `output` | записи `learn`, `idle_timeout` 300 с |
|
||||
| 25 | broadcast или неизвестный MAC | рассылка по сегменту | порт входа OVS исключает сам |
|
||||
|
||||
## 7. Где поток обрывается
|
||||
|
||||
| Точка | Причина | Как увидеть |
|
||||
|---|---|---|
|
||||
| t0 prio 0 | кадр с порта, не отнесённого к сегменту — ВМ не подключена | `make ports`, затем `make attach` |
|
||||
| t1 prio 125 | обращение на VIP при выключенном листенере | `lbctl flows 1` |
|
||||
| t11 prio 0 | пул без живых членов — fail-close | `make slots` |
|
||||
| t20 prio 0 | назначение без маршрута | `lbctl flows 20 \| grep priority=0` |
|
||||
| t21 prio 0 | adjacency клиента неизвестна — запись `learn` истекла | `lbctl flows 21` |
|
||||
| t25 prio 0 | кадр без сегмента в `reg0` | `make fdb` |
|
||||
|
||||
## 8. Осмотр
|
||||
|
||||
```bash
|
||||
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # путь сессии по таблицам
|
||||
make fdb # выученные MAC и счётчики рассылки
|
||||
make ports # подключены ли ВМ сегментов
|
||||
docker compose exec lb-router lbctl flows 24 # счётчики обратной трансляции
|
||||
docker compose exec cli2 curl -s http://10.20.0.100/
|
||||
```
|
||||
|
||||
В `Final flow` трассировки коммутируемого пути `nw_src` остаётся клиентским,
|
||||
`nw_ttl` не уменьшен, а вывод происходит из таблицы 25 в порт члена.
|
||||
|
||||
## 9. Ограничения приватного пути
|
||||
|
||||
- **Порты ВМ должны быть портами OVS.** Если между клиентом и бэкендом окажется
|
||||
коммутатор, который SDN не контролирует, ответ бэкенда мост не увидит, и кейс
|
||||
не решается ничем, кроме SNAT — то есть с потерей IP клиента.
|
||||
- **Весь внутрисегментный трафик проходит через узел.** Пропускная способность
|
||||
узла становится пропускной способностью сегмента.
|
||||
- **Обратная трансляция опирается на `ct`.** Stateless-вариант (`nw_src=member,
|
||||
tp_src=8080 → VIP:80`) потребует аккуратного исключения трафика, который
|
||||
бэкенд сам инициирует с порта 8080.
|
||||
- **FDB без старения короче 300 с и без ограничения на число MAC** в сегменте.
|
||||
- **Неизвестный unicast и unicast-ARP рассылаются флудом** — обычное поведение
|
||||
обучающегося коммутатора.
|
||||
- **В стенде порты ВМ исчезают при пересоздании контейнера** — нужен
|
||||
`make attach`. В целевой среде порты создаёт гипервизор.
|
||||
- **Только TCP и только IPv4**; ICMP-ошибки и фрагменты — как и в публичном
|
||||
пути, вне рамок.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Потоки данных: балансировка публичного трафика
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
|
||||
`192.168.5.21:80` и обратно.
|
||||
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
|
||||
балансировка внутри приватного сегмента.
|
||||
**Источник:** `lb/pipeline.sh`, `lb/topology.env`.
|
||||
|
||||
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
|
||||
разных сегментах, узел выступает L3-хопом в обе стороны, TTL уменьшается на
|
||||
единицу на каждом направлении. Сетевой стек ядра контейнера в передаче трафика
|
||||
не участвует — все решения принимает OpenFlow.
|
||||
|
||||
## 1. Участники
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CL["Клиент<br/>192.168.5.13"]
|
||||
|
||||
subgraph BR["br-lb — OpenFlow datapath"]
|
||||
direction TB
|
||||
PUB["<b>pub0 · ofport 1</b><br/>macvlan @ enp3s0<br/>02:42:c0:a8:05:14<br/><i>адрес узла 192.168.5.20</i><br/><i>VIP 192.168.5.21:80</i>"]
|
||||
B1["be1 · 10"]
|
||||
B3["be3 · 11"]
|
||||
B2["be2 · 20"]
|
||||
B4["be4 · 21"]
|
||||
end
|
||||
|
||||
BE1["be1 10.20.0.2:8080"]
|
||||
BE3["be3 10.20.0.3:8080"]
|
||||
BE2["be2 10.30.0.2:8080"]
|
||||
BE4["be4 10.30.0.3:8080"]
|
||||
|
||||
CL <-->|"L2 клиентской сети"| PUB
|
||||
B1 <--> BE1
|
||||
B3 <--> BE3
|
||||
B2 <--> BE2
|
||||
B4 <--> BE4
|
||||
```
|
||||
|
||||
Адреса `192.168.5.20` и `192.168.5.21` существуют только в правилах OpenFlow —
|
||||
на интерфейсе они не настроены, за них отвечает ARP-респондер MAC-адресом порта
|
||||
`pub0`.
|
||||
|
||||
## 2. Конвейер таблиц публичного пути
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
IN(["Кадр с pub0"]) --> T0["<b>0</b> сегмент по in_port<br/>reg0 = 1"]
|
||||
T0 --> T1{"<b>1</b> классификация"}
|
||||
|
||||
T1 -->|"arp"| T5["<b>5</b> ARP-респондер<br/>за 192.168.5.20 и .21"]
|
||||
T1 -->|"icmp echo на<br/>адрес узла или VIP"| T6["<b>6</b> ICMP-респондер"]
|
||||
T1 -->|"ip на VIP, адрес узла<br/>или приватные сети<br/>+ <b>learn</b> adjacency"| T10
|
||||
T1 -->|"прочий IP"| DROP1(["drop"])
|
||||
|
||||
T10{"<b>10</b> листенер<br/>tcp VIP:80"}
|
||||
T10 -->|"multipath(symmetric_l4)<br/>→ reg1 = слот"| T11["<b>11</b> таблица слотов<br/>reg1 → reg2<br/><i>ведёт hcd</i>"]
|
||||
T10 -->|"IP на VIP без<br/>листенера"| DROPV(["drop"])
|
||||
T10 -->|"прочий IP: клиент<br/>обратился к бэкенду<br/>напрямую"| T20
|
||||
|
||||
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
|
||||
T11 --> T12["<b>12</b> DNAT<br/>ct(commit, nat(dst=member:8080))"]
|
||||
T12 --> T20
|
||||
|
||||
T20{"<b>20</b> маршрутизация<br/>LPM по приоритету"}
|
||||
T20 -->|"dec_ttl,<br/>mod_dl_src = MAC сегмента"| T21["<b>21</b> adjacency<br/>MAC члена + его порт"]
|
||||
T20 -->|"нет маршрута"| DROP20(["drop"])
|
||||
T21 --> OUT(["output: порт члена пула"])
|
||||
|
||||
classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff
|
||||
class DROP1,DROPV,DROPF,DROP20 d
|
||||
```
|
||||
|
||||
Обратный путь идёт по другим таблицам — см. раздел 4.
|
||||
|
||||
## 3. Прямой путь: клиент → VIP → бэкенд
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant C as Клиент 192.168.5.13
|
||||
participant P as pub0 · 1
|
||||
participant OF as Таблицы OpenFlow
|
||||
participant B as be1 10.20.0.2
|
||||
|
||||
C->>P: ARP who-has 192.168.5.21
|
||||
P->>OF: t1 → t5 ARP-респондер
|
||||
OF-->>C: is-at 02:42:c0:a8:05:14
|
||||
C->>P: SYN 192.168.5.13 → 192.168.5.21:80, TTL 64
|
||||
Note over OF: t0: reg0=1
|
||||
Note over OF: t1: адресован стенду →<br/><b>learn</b>: adjacency клиента в t21
|
||||
Note over OF: t10: multipath(symmetric_l4, basis=pool_id)<br/>→ reg1 = номер слота
|
||||
Note over OF: t11: слот → reg2 = член пула
|
||||
Note over OF: t12: ct(commit, nat(dst=10.20.0.2:8080))<br/><b>src не меняется</b>
|
||||
Note over OF: t20: LPM /24 10.20.0.0 → dec_ttl 63,<br/>src MAC = MAC сегмента
|
||||
Note over OF: t21: dst MAC = MAC be1, output порт be1
|
||||
OF->>B: пакет
|
||||
Note over B: client=192.168.5.13 — исходный адрес,<br/>served=10.20.0.2:8080 — после DNAT
|
||||
```
|
||||
|
||||
Ключевое: **SNAT не выполняется**. Транслируется только адрес и порт
|
||||
назначения, поэтому бэкенд видит реальный адрес клиента.
|
||||
|
||||
## 4. Обратный путь: бэкенд → клиент
|
||||
|
||||
Ответ обязан пройти через узел — иначе клиент получит пакет от `10.20.0.2:8080`
|
||||
вместо `192.168.5.21:80` и разорвёт соединение. Это обеспечивается маршрутом на
|
||||
бэкенде (профиль A, см. раздел 5).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BE["be1 10.20.0.2:8080"] -->|"src=10.20.0.2 dst=192.168.5.13<br/>через шлюз сегмента 10.20.0.1"| A["порт be1 · 10"]
|
||||
A --> B["t1: dl_dst = MAC шлюза<br/>сегмента → t15"]
|
||||
B --> C["<b>t15</b>: ct(zone=1, nat)<br/><b>un-DNAT</b>: src → 192.168.5.21:80"]
|
||||
C --> D["t16 → t20: LPM /24 192.168.5.0<br/>dec_ttl, src MAC = pub0"]
|
||||
D --> E["t21 prio 90: MAC клиента<br/>из действия learn"]
|
||||
E --> CL["Клиент видит ответ<br/>от 192.168.5.21:80"]
|
||||
```
|
||||
|
||||
Трансляция снимается conntrack'ом: `ct(commit, nat)` на прямом пути закрепил
|
||||
привязку за соединением, `ct(nat)` без commit на обратном её применяет в
|
||||
обратную сторону. Пакеты, для которых записи нет (транзит между сегментами,
|
||||
собственные обращения бэкендов), проходят без изменений.
|
||||
|
||||
## 5. Условие возврата: профиль A на бэкенде
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Таблица маршрутизации бэкенда"] --> B["192.168.5.0/24 via 10.20.0.1<br/><i>клиентский префикс — через узел</i>"]
|
||||
A --> C["10.30.0.0/24 via 10.20.0.1<br/><i>соседний сегмент — через узел</i>"]
|
||||
A --> D["default via 10.20.0.254<br/><i>шлюз Docker — мимо узла</i>"]
|
||||
B --> E["Ответы на балансируемые сессии<br/>возвращаются на узел и<br/>проходят обратную трансляцию"]
|
||||
D --> F["Собственный egress бэкенда<br/>(обновления, DNS, телеметрия)<br/>узел не маршрутизирует"]
|
||||
```
|
||||
|
||||
Проверить: `docker compose exec be1 ip route`. Признак, что профиль A цел, —
|
||||
кадр к внешнему адресу адресован MAC-у шлюза Docker, а не MAC-у маршрутизатора
|
||||
сегмента (`02:42:0a:14:00:01`).
|
||||
|
||||
## 6. Таблица решений публичного пути
|
||||
|
||||
| Таблица | Матч | Действие | Комментарий |
|
||||
|---|---|---|---|
|
||||
| 0 | `in_port=1` | `reg0 = 1` | публичный сегмент; коммутации в нём нет |
|
||||
| 1 | `arp` | → t5 | штатного ARP-стека у узла нет |
|
||||
| 1 | `ip`, dst ∈ {VIP, `.20`, `10.20/24`, `10.30/24`} | `learn` + → t10 | обучение сужено, чтобы в t21 не попадал широковещательный шум LAN |
|
||||
| 1 | прочий IP с `pub0` | drop | |
|
||||
| 5 | `arp_tpa` = `.20` или `.21` | ответ `IN_PORT` | отвечает MAC-ом `pub0` |
|
||||
| 10 | `tcp`, `nw_dst=VIP`, `tp_dst=80` | `multipath` → t11 | симметричный L4-хэш, basis = `pool_id` |
|
||||
| 10 | `ip`, `nw_dst=VIP` (нет листенера) | drop | видно по счётчику |
|
||||
| 10 | прочий IP | → t20 | прямое обращение клиента к бэкенду |
|
||||
| 11 | `reg1` = слот | `reg2` = член | 1024 правила, заливает `hcd` |
|
||||
| 11 | prio 0 | drop | fail-close: живых членов нет |
|
||||
| 12 | `reg2` = член | `ct(commit, nat(dst=member:8080))` → t20 | без SNAT |
|
||||
| 15 | `tcp` с порта сегмента, адресован шлюзу | `ct(nat)` → t16 | обратная трансляция |
|
||||
| 20 | `/24` префиксы | `dec_ttl` + `mod_dl_src` | LPM через приоритет правила |
|
||||
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | статические MAC из `topology.env` |
|
||||
| 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с |
|
||||
|
||||
## 7. Где поток обрывается
|
||||
|
||||
| Точка | Причина | Как увидеть |
|
||||
|---|---|---|
|
||||
| t1 prio 100 | IP из публичного сегмента адресован не стенду | `lbctl flows 1` |
|
||||
| t10 prio 150 | обращение на VIP, для которого нет листенера | `lbctl flows 10` |
|
||||
| t11 prio 0 | пул без живых членов — fail-close | `make slots` |
|
||||
| t20 prio 0 | назначение без маршрута (дефолта у узла нет) | `lbctl flows 20 \| grep priority=0` |
|
||||
| t21 prio 0 | MAC клиента неизвестен — запись `learn` истекла | `lbctl flows 21` |
|
||||
|
||||
## 8. Осмотр
|
||||
|
||||
```bash
|
||||
make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам
|
||||
make slots # раскладка и per-member счётчики
|
||||
make conns # записи ct
|
||||
docker compose exec lb-router lbctl flows 21
|
||||
```
|
||||
|
||||
## 9. Ограничения публичного пути
|
||||
|
||||
- **Обратный трафик обязан идти через узел.** Без маршрута на клиентский
|
||||
префикс (профиль A) ответ уйдёт мимо и соединение не соберётся.
|
||||
- **Датапас stateful**: обратную трансляцию делает `ct`. Прямой и обратный
|
||||
трафик сессии обязаны проходить через один узел — Active/Active ECMP в такой
|
||||
схеме не работает.
|
||||
- **ICMP-ошибки не генерируются**: ни `TTL exceeded`, ни `fragmentation
|
||||
needed`. PMTUD через стенд не работает, `traceroute` узел не покажет.
|
||||
- **Фрагменты IP не обрабатываются** — правила матчат L4-заголовок.
|
||||
- **Только TCP и только IPv4**; UDP- и L3-листенеров нет.
|
||||
- **MAC клиента живёт 300 секунд** — после простоя первая же новая сессия
|
||||
создаёт запись заново.
|
||||
@@ -1,227 +0,0 @@
|
||||
# Потоки данных подсистемы L3-маршрутизации
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Область:** транзитная маршрутизация узла `hpnn-lb` — всё, что проходит через
|
||||
таблицы 20 (LPM) и 21 (adjacency). Балансировка (таблицы 10–12) показана только
|
||||
как точка ответвления и подробно описана в [STEP2_SUMMARY.md](STEP2_SUMMARY.md).
|
||||
**Источник:** `lb/pipeline.sh`, `lb/topology.env` (состояние на 4 члена пула).
|
||||
|
||||
Ключевое свойство: сетевой стек ядра контейнера в транзите не участвует. В ядре
|
||||
живут только два адреса — `10.20.0.253` и `10.30.0.253` на internal-портах для
|
||||
health-проб. Все решения о пересылке принимает OpenFlow.
|
||||
|
||||
## 1. Топология портов моста `br-lb`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CL["Клиент<br/>192.168.5.0/24"]
|
||||
|
||||
subgraph BR["br-lb — OpenFlow datapath"]
|
||||
direction TB
|
||||
PUB["pub0 — ofport 1<br/>macvlan @ enp3s0<br/>02:42:c0:a8:05:14<br/><i>шлюз 192.168.5.20, VIP .21</i>"]
|
||||
P2["p2 — ofport 2<br/>02:42:0a:14:00:01<br/><i>шлюз 10.20.0.1</i>"]
|
||||
P3["p3 — ofport 3<br/>02:42:0a:1e:00:01<br/><i>шлюз 10.30.0.1</i>"]
|
||||
HC2["hcif-p2 — ofport 4<br/>10.20.0.253 <b>в ядре</b>"]
|
||||
HC3["hcif-p3 — ofport 5<br/>10.30.0.253 <b>в ядре</b>"]
|
||||
end
|
||||
|
||||
BE1["be1 10.20.0.2"]
|
||||
BE3["be3 10.20.0.3"]
|
||||
BE2["be2 10.30.0.2"]
|
||||
BE4["be4 10.30.0.3"]
|
||||
HCD["hcd — демон<br/>health-check"]
|
||||
|
||||
CL <--> PUB
|
||||
P2 <--> BE1
|
||||
P2 <--> BE3
|
||||
P3 <--> BE2
|
||||
P3 <--> BE4
|
||||
HCD <--> HC2
|
||||
HCD <--> HC3
|
||||
```
|
||||
|
||||
## 2. Конвейер таблиц: путь маршрутизируемого пакета
|
||||
|
||||
Красным помечены точки отбрасывания, пунктиром — ответвление в балансировку.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
IN(["Пакет на порту моста"]) --> T0
|
||||
|
||||
T0{"<b>Таблица 0</b><br/>классификация по in_port<br/>и типу трафика"}
|
||||
|
||||
T0 -->|"arp"| T5["<b>5</b> ARP-респондер<br/>для .20/.21/10.20.0.1/<br/>10.30.0.1/.253"]
|
||||
T0 -->|"icmp echo<br/>на адреса узла"| T6["<b>6</b> ICMP echo-респондер"]
|
||||
T0 -->|"in_port=1 (pub)<br/>+ dst ∈ {VIP, узел,<br/>10.20/24, 10.30/24}"| LEARN["<b>learn</b> → запись<br/>adjacency клиента<br/>в таблицу 21<br/>(priority 90, 300 c)"]
|
||||
T0 -->|"in_port=2,3 (priv)"| T15["<b>15</b> обратный путь"]
|
||||
T0 -->|"in_port=4,5 (hcif)"| T20
|
||||
T0 -->|"иначе"| DROP0(["drop"])
|
||||
|
||||
LEARN --> T10{"<b>Таблица 10</b><br/>листенеры"}
|
||||
T10 -.->|"tcp dst=VIP:80"| LB["<b>11/12</b> слот → член пула,<br/>DNAT ct(commit,nat)"]
|
||||
T10 -->|"прочий IP<br/>(прямое обращение<br/>к бэкенду)"| T20
|
||||
T10 -->|"IP на VIP<br/>без листенера"| DROPV(["drop"])
|
||||
LB --> T20
|
||||
|
||||
T15 -->|"tcp"| CT["ct(zone=1, nat)<br/>снятие DNAT,<br/>если сессия известна"] --> T16["<b>16</b> пост-ct"] --> T20
|
||||
T15 -->|"прочий IP"| T20
|
||||
|
||||
T20{"<b>Таблица 20</b> — LPM<br/>приоритет = длина префикса"}
|
||||
T20 -->|"/32 10.20.0.253<br/>/32 10.30.0.253<br/><i>адрес узла, TTL не трогаем</i>"| T21
|
||||
T20 -->|"/24 10.20.0.0<br/>dec_ttl, src MAC = p2"| T21
|
||||
T20 -->|"/24 10.30.0.0<br/>dec_ttl, src MAC = p3"| T21
|
||||
T20 -->|"/24 192.168.5.0<br/>dec_ttl, src MAC = pub0"| T21
|
||||
T20 -->|"нет маршрута<br/><i>default отсутствует</i>"| DROP20(["drop"])
|
||||
|
||||
T21{"<b>Таблица 21</b> — adjacency<br/>next-hop MAC + порт"}
|
||||
T21 -->|"prio 100: be1..be4,<br/>hcif — статически"| OUT(["output: 2 / 3 / 4 / 5"])
|
||||
T21 -->|"prio 90: клиент,<br/>из действия learn"| OUTC(["output: 1"])
|
||||
T21 -->|"MAC неизвестен"| DROP21(["drop"])
|
||||
|
||||
classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff
|
||||
class DROP0,DROPV,DROP20,DROP21 d
|
||||
```
|
||||
|
||||
Почему нумерация именно такая: `goto_table` в OpenFlow разрешает переход только
|
||||
вперёд, поэтому обратный путь (15/16) обязан стоять **до** общей маршрутизации
|
||||
(20), а не после неё.
|
||||
|
||||
## 3. Четыре потока маршрутизации
|
||||
|
||||
### R1. Клиент → бэкенд напрямую (публичный → приватный сегмент)
|
||||
|
||||
Балансировка не участвует: клиент обращается на `10.20.0.2:8080`, имея маршрут
|
||||
через `192.168.5.20`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant C as Клиент 192.168.5.13
|
||||
participant P as pub0 (1)
|
||||
participant OF as Таблицы OpenFlow
|
||||
participant Q as p2 (2)
|
||||
participant B as be1 10.20.0.2
|
||||
|
||||
C->>P: ARP who-has 192.168.5.20
|
||||
P->>OF: таблица 5 — ARP-респондер
|
||||
OF-->>C: is-at 02:42:c0:a8:05:14
|
||||
C->>P: IP src=192.168.5.13 dst=10.20.0.2, TTL=64
|
||||
P->>OF: t0 learn (adj клиента в t21) → t10
|
||||
Note over OF: t10: не VIP → goto 20
|
||||
Note over OF: t20: LPM /24 10.20.0.0<br/>dec_ttl → 63, src MAC = p2
|
||||
Note over OF: t21: dst MAC = 02:42:0a:14:00:02, порт 2
|
||||
OF->>Q: пересылка
|
||||
Q->>B: пакет, src остался клиентским
|
||||
B->>Q: ответ src=10.20.0.2 dst=192.168.5.13
|
||||
Q->>OF: t0 → t15 → ct(nat) сессии нет → t16 → t20
|
||||
Note over OF: t20: LPM /24 192.168.5.0<br/>dec_ttl, src MAC = pub0
|
||||
Note over OF: t21: prio 90 — запись, созданная learn
|
||||
OF->>P: порт 1
|
||||
P->>C: ответ
|
||||
```
|
||||
|
||||
### R2. Транзит между приватными сегментами (be1 → be2)
|
||||
|
||||
Единственный поток, где узел работает как чистый маршрутизатор в обе стороны.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BE1["be1<br/>10.20.0.2"] -->|"dst=10.30.0.2:8080<br/>TTL 64"| A["p2 (2)"]
|
||||
A --> B["t0: in_port=2 → t15"]
|
||||
B --> C["t15: tcp → ct(zone=1,nat)<br/>сессии в таблице нет —<br/>пакет не меняется"]
|
||||
C --> D["t16 → t20"]
|
||||
D --> E["t20: /24 10.30.0.0<br/>dec_ttl → 63<br/>src MAC = 02:42:0a:1e:00:01"]
|
||||
E --> F["t21: dst MAC =<br/>02:42:0a:1e:00:02<br/>output:3"]
|
||||
F --> G["p3 (3)"] --> BE2["be2<br/>10.30.0.2<br/><i>client=10.20.0.2</i>"]
|
||||
```
|
||||
|
||||
Обратный путь `be2 → be1` симметричен: `t0 → t15 → t16 → t20 (/24 10.20.0.0)
|
||||
→ t21 → p2`. Никакой трансляции ни в одном направлении.
|
||||
|
||||
### R3. Обратный трафик балансируемой сессии (бэкенд → клиент)
|
||||
|
||||
Тот же путь, что и R1-ответ, но с обязательным проходом через `ct`: именно там
|
||||
`src` возвращается с адреса бэкенда на VIP.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BE["be1 10.20.0.2:8080"] -->|"src=10.20.0.2<br/>dst=192.168.5.13"| A["p2 (2)"]
|
||||
A --> B["t0: in_port=2 → t15"]
|
||||
B --> C["t15: ct(zone=1, nat)<br/><b>un-DNAT</b>:<br/>src → 192.168.5.21:80"]
|
||||
C --> D["t16 → t20"]
|
||||
D --> E["t20: /24 192.168.5.0<br/>dec_ttl, src MAC = pub0"]
|
||||
E --> F["t21 prio 90:<br/>MAC клиента из learn<br/>output:1"]
|
||||
F --> CL["Клиент<br/>видит ответ от VIP"]
|
||||
```
|
||||
|
||||
Почему обратный трафик вообще приходит на узел: у бэкендов профиль A — маршрут
|
||||
на клиентский префикс `192.168.5.0/24` направлен на шлюз сегмента
|
||||
(`10.20.0.1` / `10.30.0.1`), а `default` остаётся на шлюзе Docker. Собственный
|
||||
исходящий трафик бэкенда через балансировщик не идёт.
|
||||
|
||||
### R4. Health-пробы — единственный трафик, порождённый узлом
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant H as hcd (ядро контейнера)
|
||||
participant I as hcif-p2 (4)
|
||||
participant OF as OpenFlow
|
||||
participant Q as p2 (2)
|
||||
participant B as be1
|
||||
|
||||
Note over H: Dialer.LocalAddr = 10.20.0.253
|
||||
H->>I: SYN 10.20.0.253 → 10.20.0.2:8080
|
||||
I->>OF: t0: in_port=4 → <b>сразу t20</b><br/>без learn и без ct
|
||||
Note over OF: t20: /24 10.20.0.0, t21: output:2
|
||||
OF->>Q: →
|
||||
Q->>B: проба GET /healthz
|
||||
B->>Q: SYN-ACK 10.20.0.2 → 10.20.0.253
|
||||
Q->>OF: t0 → t15 → t16 → t20
|
||||
Note over OF: t20: <b>/32 10.20.0.253</b> prio 32<br/>TTL не уменьшается — пакет<br/>адресован самому узлу
|
||||
Note over OF: t21: output:4
|
||||
OF->>I: →
|
||||
I->>H: ответ, член up
|
||||
```
|
||||
|
||||
Пробы идут с `.253`, а не с VIP, намеренно: иначе ответ бэкенда попал бы в
|
||||
логику обратной трансляции (`t15`) и до пробера не дошёл.
|
||||
|
||||
## 4. Таблица решений
|
||||
|
||||
| Таблица | Матч | Действие | Комментарий |
|
||||
|---|---|---|---|
|
||||
| 0 | `in_port=1`, dst ∈ {VIP, .20, 10.20/24, 10.30/24} | `learn` → t10 | обучение сужено, чтобы в t21 не попадал broadcast-шум LAN |
|
||||
| 0 | `in_port=1`, прочий IP | drop | |
|
||||
| 0 | `in_port=2,3` | → t15 | обратный путь и транзит |
|
||||
| 0 | `in_port=4,5` | → t20 | трафик стека узла, минуя ct и learn |
|
||||
| 10 | `ip`, не VIP | → t20 | точка входа маршрутизации из публичного сегмента |
|
||||
| 15 | `tcp` | `ct(zone=1, nat)` → t16 | un-DNAT для балансируемых сессий, no-op для транзита |
|
||||
| 20 | `/32` адреса `.253` | prio 32, → t21 | TTL не уменьшается |
|
||||
| 20 | `/24` три префикса | prio 24, `dec_ttl` + `mod_dl_src` | LPM через приоритет |
|
||||
| 20 | остальное | prio 0, drop | **дефолтного маршрута нет** |
|
||||
| 21 | `nw_dst` = be1..be4, hcif | prio 100, `mod_dl_dst` + `output` | статические MAC из `docker-compose.yml` |
|
||||
| 21 | `nw_dst` = клиент | prio 90, из `learn` | hard_timeout 300 с |
|
||||
|
||||
## 5. Где поток обрывается
|
||||
|
||||
| Точка | Причина | Как увидеть |
|
||||
|---|---|---|
|
||||
| t0 prio 0 | не-IP и не-ARP: IPv6, DHCP, broadcast | `lbctl flows 0` |
|
||||
| t0 prio 100 (pub) | IP из публичного сегмента не стенду | `lbctl flows 0` |
|
||||
| t10 prio 150 | IP на VIP без листенера | `lbctl flows 10` |
|
||||
| t20 prio 0 | назначение без маршрута | `lbctl flows 20 \| grep priority=0` |
|
||||
| t21 prio 0 | next-hop MAC неизвестен (запись `learn` истекла) | `lbctl flows 21` |
|
||||
|
||||
## 6. Чего в этих потоках нет
|
||||
|
||||
- **ICMP-ошибок узел не генерирует** — ни `TTL exceeded`, ни `fragmentation
|
||||
needed`. Пакет с TTL=1 просто исчезает, `traceroute` через стенд узел не
|
||||
покажет, PMTUD не работает.
|
||||
- **Фрагменты IP не обрабатываются**: правила матчат L4-заголовок, которого во
|
||||
втором и последующих фрагментах нет.
|
||||
- **ARP-резолвера next-hop нет.** MAC бэкендов статичны, MAC клиентов узнаются
|
||||
действием `learn` — то есть только для тех, кто первым обратился к стенду.
|
||||
- **Дефолтного маршрута нет** по построению: узел маршрутизирует только три
|
||||
известных префикса.
|
||||
- **Только IPv4.**
|
||||
@@ -0,0 +1,149 @@
|
||||
# Шаг 3 — план внедрения: листенер в приватном сегменте
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Предыдущий шаг:** [STEP2_SUMMARY.md](STEP2_SUMMARY.md)
|
||||
**Итоги:** [STEP3_SUMMARY.md](STEP3_SUMMARY.md)
|
||||
|
||||
## Задача
|
||||
|
||||
Поддержать листенер балансировщика **в приватном сегменте** — в одном сегменте
|
||||
или во всех сразу — при условиях:
|
||||
|
||||
1. листенер работает как публичный: принимает на порту 80 и транслирует на порт
|
||||
бэкенда 8080;
|
||||
2. бэкенд может стоять в одном сегменте с клиентом, отправившим запрос;
|
||||
3. в ОС клиента и бэкенда не вносится ничего — ни адресов, ни маршрутов, ни
|
||||
интерфейсов;
|
||||
4. адресное пространство подсети задаёт заказчик, дробить её нельзя;
|
||||
5. трафик внутри сегмента не блокируется;
|
||||
6. единственный уровень влияния — OVS как data plane SDN и сама нода
|
||||
балансировщика.
|
||||
|
||||
Ключевое требование: на бэкенд приходит пакет с исходным IP клиента.
|
||||
|
||||
## Почему прежняя схема не годится
|
||||
|
||||
Клиент и бэкенд — L2-соседи в одной подсети. Ответ бэкенда уходит клиенту
|
||||
напрямую, минуя балансировщик: снять DNAT негде, клиент получает пакет от
|
||||
`10.20.0.2:8080` вместо `10.20.0.100:80` и рвёт соединение.
|
||||
|
||||
Условия отсекают все обходные пути:
|
||||
|
||||
| Вариант | Почему не подходит |
|
||||
|---|---|
|
||||
| SNAT | уничтожает сохранение IP клиента |
|
||||
| DSR (VIP на `lo` бэкенда) | нарушает условие 3, не транслирует порт (условие 1) |
|
||||
| Изоляция портов сегмента (private VLAN) | нарушает условие 5, неприменимо к ВМ |
|
||||
| Дробление подсети маршрутами `/25` на бэкенде | нарушает условия 3 и 4 |
|
||||
|
||||
Остаётся единственная предпосылка, совпадающая с условием 6: **порты клиента и
|
||||
бэкенда — порты OVS**. Тогда кадр «бэкенд → клиент-сосед» проходит через
|
||||
OpenFlow-таблицы даже при прямом L2-обмене, и обратную трансляцию есть где
|
||||
выполнить.
|
||||
|
||||
В целевой среде это выполняется по построению: ВМ подключены к vSwitch. Текущий
|
||||
стенд этому не соответствует — приватный сегмент коммутирует docker-бридж
|
||||
`hpnn-p2`, а `br-lb` подключён к нему одним портом. Шаг 3 приводит стенд к
|
||||
целевой модели.
|
||||
|
||||
## Решение
|
||||
|
||||
`br-lb` становится **коммутатором приватного сегмента** и выполняет трансляцию
|
||||
на пути кадра. Для внутрисегментного трафика нет ни маршрутизации, ни
|
||||
`dec_ttl`: с точки зрения ВМ клиент и бэкенд остаются L2-соседями.
|
||||
|
||||
**Прямой путь.** Клиент резолвит VIP через ARP-респондер OVS и шлёт кадр на MAC
|
||||
шлюза сегмента. Таблица листенера кладёт сессию в слот, таблица DNAT
|
||||
переписывает `VIP:80 → member:8080`, `src` не трогает, заменяет dst MAC на MAC
|
||||
члена и отдаёт кадр в коммутацию.
|
||||
|
||||
**Обратный путь.** Бэкенд отвечает соседу напрямую: `src=member:8080`,
|
||||
`dst=клиент`. Кадр приходит на порт OVS, где `ct(nat)` снимает трансляцию —
|
||||
`src` возвращается к `VIP:80` — и кадр коммутируется в порт клиента.
|
||||
|
||||
**Прочий трафик сегмента** (`be↔be`, клиент к бэкенду напрямую, egress к
|
||||
docker-шлюзу, ARP) коммутируется обычным образом, без трансляции.
|
||||
|
||||
## Топология после изменения
|
||||
|
||||
Каждая ВМ сегмента — отдельный порт `br-lb`. Порты `p2`/`p3` остаются как
|
||||
аплинки сегмента к docker-шлюзу: через них идёт egress бэкендов (профиль A).
|
||||
|
||||
| Сегмент | Порт | ofport | Адрес |
|
||||
|---|---|---|---|
|
||||
| priv2 `10.20.0.0/24` | `p2` (аплинк) | 2 | — |
|
||||
| | `hcif-p2` | 4 | `10.20.0.253` |
|
||||
| | `be1` | 10 | `10.20.0.2` |
|
||||
| | `be3` | 11 | `10.20.0.3` |
|
||||
| | `cli2` | 12 | `10.20.0.10` |
|
||||
| priv3 `10.30.0.0/24` | `p3` (аплинк) | 3 | — |
|
||||
| | `hcif-p3` | 5 | `10.30.0.253` |
|
||||
| | `be2` | 20 | `10.30.0.2` |
|
||||
| | `be4` | 21 | `10.30.0.3` |
|
||||
| | `cli3` | 22 | `10.30.0.10` |
|
||||
| pub `192.168.5.0/24` | `pub0` | 1 | маршрутизируемый, без изменений |
|
||||
|
||||
Шлюз сегмента `10.20.0.1` и VIP `10.20.0.100` отвечают одним MAC — `$P2_MAC`.
|
||||
Это и есть признак, по которому пайплайн отличает L3-трафик (адресован
|
||||
маршрутизатору) от L2-трафика сегмента.
|
||||
|
||||
## Карта таблиц
|
||||
|
||||
Новые — 1, 24, 25; остальные сохраняют назначение шага 2.
|
||||
|
||||
| Таблица | Назначение |
|
||||
|---|---|
|
||||
| 0 | определение сегмента по `in_port` (`reg0`), обучение MAC и adjacency |
|
||||
| 1 | классификация: ARP, ICMP, листенер, L3-трафик к шлюзу, коммутация |
|
||||
| 5 | ARP-респондер (адреса узла и VIP), затем коммутация |
|
||||
| 6 | ICMP echo-респондер |
|
||||
| 10 | листенеры: публичный и приватные |
|
||||
| 11 | таблица слотов (заливает `hcd`, не меняется) |
|
||||
| 12 | применение члена пула: DNAT + L2-выдача либо DNAT + маршрутизация |
|
||||
| 15/16 | обратный путь L3 (публичный и межсегментный) |
|
||||
| 20/21 | маршрутизация и adjacency |
|
||||
| **24** | обратная трансляция коммутируемого трафика сегмента |
|
||||
| **25** | L2-коммутация сегмента: FDB, broadcast и unknown flood |
|
||||
|
||||
Регистры: `reg0` — номер сегмента, `reg1` — слот, `reg2` — член пула.
|
||||
|
||||
## Порядок работ
|
||||
|
||||
1. `lb/topology.env` — порты и адреса участников сегментов, VIP приватных
|
||||
листенеров, переключатель `PRIV_LISTENERS`.
|
||||
2. `scripts/attach-segment.sh` — veth из netns контейнера в `br-lb`, адрес, MAC
|
||||
и маршруты назначаются снаружи (эмуляция гипервизора и DHCP).
|
||||
`docker-compose.yml`: участники сегментов переводятся на `network_mode: none`;
|
||||
добавляются клиенты `cli2` и `cli3`.
|
||||
3. `lb/pipeline.sh` — таблицы 0/1/5/25: сегментация, обучение, коммутация.
|
||||
Проверка связности сегмента до всякой балансировки.
|
||||
4. Листенер: таблицы 10/12 и обратная трансляция в таблице 24.
|
||||
5. `lb/lbctl.sh` — `trace` по приватному листенеру, `fdb`.
|
||||
6. `scripts/verify.sh` — новая секция и регрессия существующих проверок.
|
||||
7. Документация: `docs/STEP3_SUMMARY.md`, `README.md`,
|
||||
`docs/PRIVATE_LB_DATAFLOW.md`, `docs/MANUAL_TEST_PLAN.md`.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- `curl http://10.20.0.100/` из `cli2` обслуживается всеми четырьмя членами;
|
||||
- во всех ответах `client=10.20.0.10` — исходный IP клиента;
|
||||
- `served=10.20.0.2:8080` — трансляция порта 80 → 8080 выполняется;
|
||||
- TTL ответа от VIP равен TTL прямого ответа соседа по сегменту — путь остался
|
||||
коммутируемым;
|
||||
- внутрисегментная связность не пострадала: `cli2 → be1:8080` напрямую,
|
||||
`be1 → be3:8080`, egress бэкенда мимо балансировщика;
|
||||
- `PRIV_LISTENERS="p2"` выключает листенер в priv3, не затрагивая связность;
|
||||
- все проверки шага 2 проходят.
|
||||
|
||||
## Риски
|
||||
|
||||
- **`br-lb` получает функцию L2-коммутатора** — обучение, flood, изоляция
|
||||
сегментов. Новый класс правил и главный источник ошибок шага, поэтому этап 3
|
||||
проверяется до подключения балансировки.
|
||||
- **Хрупкость стенда:** veth, добавленные вручную, теряются при пересоздании
|
||||
контейнера. `make attach` восстанавливает; в реальной среде порты создаёт
|
||||
гипервизор.
|
||||
- **Обратная трансляция опирается на `ct`**, как и прежде. Stateless-вариант —
|
||||
задел следующего шага.
|
||||
- **Нагрузка сегмента ложится на балансировщик**: весь внутрисегментный трафик
|
||||
проходит через `br-lb`.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Шаг 3 — итоги: листенер в приватном сегменте
|
||||
|
||||
**Дата:** 2026-08-17
|
||||
**Статус:** выполнено, проверено после холодного перезапуска (86 из 86 проверок)
|
||||
**План:** [STEP3_IMPLEMENTATION_PLAN.md](STEP3_IMPLEMENTATION_PLAN.md)
|
||||
**Предыдущий шаг:** [STEP2_SUMMARY.md](STEP2_SUMMARY.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
Балансировщик получил листенеры в приватных сегментах — `10.20.0.100:80` и
|
||||
`10.30.0.100:80` — и научился обслуживать клиента, который стоит **в одном
|
||||
сегменте с бэкендами**. Исходный IP клиента сохраняется, порт транслируется
|
||||
80 → 8080, в ОС виртуальных машин не настраивается ничего.
|
||||
|
||||
- **`br-lb` стал коммутатором приватного сегмента.** Каждая ВМ подключена
|
||||
отдельным портом (`scripts/attach-segment.sh`), порты `p2`/`p3` остались
|
||||
аплинками сегмента к шлюзу Docker. Появились таблицы 0 (сегментация и
|
||||
обучение), 25 (FDB, broadcast и unknown flood) и 1 (классификация).
|
||||
- **Обратная трансляция коммутируемого трафика — таблица 24.** Ответ бэкенда
|
||||
клиенту-соседу уходит по L2 напрямую, но проходит через мост, где `ct(nat)`
|
||||
возвращает источник к `VIP:80`.
|
||||
- **Выдача на члена своего сегмента — без маршрутизации.** Таблица 12 при
|
||||
совпадении сегмента клиента и члена меняет только MAC назначения и отдаёт кадр
|
||||
в коммутацию: `dec_ttl` не выполняется, клиент и бэкенд остаются L2-соседями.
|
||||
- **Пул общий с публичным листенером** — та же таблица слотов, тот же `pool_id`,
|
||||
тот же дайджест `0a16713c5a9eb94d`. Демон `hcd` не изменён ни строкой.
|
||||
- **Переключатель `PRIV_LISTENERS`** в `topology.env`: пусто — приватных
|
||||
листенеров нет, `"p2"` — один сегмент, `"p2 p3"` — оба.
|
||||
- **Клиенты сегментов** `cli2` (`10.20.0.10`) и `cli3` (`10.30.0.10`) — ВМ без
|
||||
единого маршрута в таблице маршрутизации.
|
||||
|
||||
## Почему именно так
|
||||
|
||||
Клиент и бэкенд — L2-соседи, поэтому ответ бэкенда уходит клиенту напрямую.
|
||||
Снять DNAT можно только там, где этот кадр виден. Остальные пути отпадали по
|
||||
условиям задачи:
|
||||
|
||||
| Вариант | Почему отвергнут |
|
||||
|---|---|
|
||||
| SNAT | уничтожает сохранение IP клиента |
|
||||
| DSR, VIP на `lo` бэкенда | требует настройки ОС ВМ, не транслирует порт |
|
||||
| Изоляция портов сегмента | блокирует внутрисегментный трафик, неприменима к ВМ |
|
||||
| Дробление подсети маршрутами `/25` | вмешательство в ОС ВМ и в адресацию заказчика |
|
||||
|
||||
Осталась единственная предпосылка, совпадающая с зоной влияния SDN: **порты ВМ
|
||||
— порты OVS**. В целевой среде это выполняется по построению; стенд приведён к
|
||||
той же модели.
|
||||
|
||||
## Результаты проверок
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| ВМ сегментов подключены портами OVS | 6 портов: be1, be3, cli2, be2, be4, cli3 |
|
||||
| `ping 10.20.0.100` из `cli2` | отвечают ARP- и ICMP-респондеры OVS |
|
||||
| Балансировка из приватного клиента | 24 запроса, задействованы все четыре члена |
|
||||
| **IP клиента на бэкенде** | `client=10.20.0.10` во всех ответах |
|
||||
| **Трансляция порта** | `served=10.20.0.2:8080` при листенере на 80 |
|
||||
| Член в сегменте клиента | обслуживает запросы, ответ транслируется таблицей 24 |
|
||||
| **TTL ответа от VIP** | 64 для членов своего сегмента, 63 — для членов другого |
|
||||
| В ОС клиента нет маршрутов | `ip route` содержит только connected-запись |
|
||||
| Внутрисегментная связность | `cli2 → be1` напрямую, `be1 → be3`, шлюз Docker — всё работает |
|
||||
| Отказ члена под нагрузкой из `cli2` | слоты разошлись 341/342/341, 2 ошибки в окне обнаружения |
|
||||
| Профиль A | egress бэкенда адресован шлюзу Docker, не маршрутизатору сегмента |
|
||||
| `PRIV_LISTENERS="p2"` | листенер в priv3 выключен (счётчик drop растёт), связность цела |
|
||||
| Регрессия шагов 1–2 | публичный листенер, транзит, health-check, fail-close, дренаж — без изменений |
|
||||
|
||||
## Главное измерение: TTL как признак пути
|
||||
|
||||
Ответы от `be1`/`be3` (сегмент клиента) приходят с **TTL 64**, от `be2`/`be4`
|
||||
(другой сегмент) — с **TTL 63**. Это прямое доказательство того, что для соседей
|
||||
по сегменту балансировщик не стал L3-хопом: он подменил MAC назначения,
|
||||
оттранслировал адрес и отдал кадр коммутацией. С точки зрения ВМ ничего не
|
||||
изменилось — она общается с соседом по подсети, как и раньше.
|
||||
|
||||
Для членов из другого сегмента работает прежний маршрутизируемый путь с
|
||||
`dec_ttl`. Оба пути живут на одном пуле и одной таблице слотов, и клиент их не
|
||||
различает.
|
||||
|
||||
## Отклонения от плана
|
||||
|
||||
1. **`load:0->NXM_OF_IN_PORT[]` не понадобился.** План предполагал hairpin —
|
||||
выдачу в порт входа — и сброс `in_port`, чтобы OVS не подавил вывод. После
|
||||
перевода каждой ВМ на собственный порт hairpin исчез: клиент и бэкенд входят
|
||||
и выходят через разные порты. Риск снят самой архитектурой.
|
||||
|
||||
2. **Отдельная сеть для egress не потребовалась.** План предусматривал служебную
|
||||
docker-сеть под default-маршрут. Оказалось достаточно оставить `p2`/`p3`
|
||||
аплинками сегмента: бэкенд адресует кадр MAC-у шлюза Docker, мост его
|
||||
коммутирует, и профиль A сохраняется в прежнем виде.
|
||||
|
||||
3. **Появилась таблица 1.** В плане классификация оставалась в таблице 0. Но
|
||||
таблица 0 занята определением сегмента и обучением (два действия `learn` на
|
||||
каждый порт), поэтому классификация вынесена отдельно — иначе правила
|
||||
пришлось бы дублировать на каждый порт сегмента.
|
||||
|
||||
4. **`make` больше не требует `sudo`.** Под root его в системе может не быть;
|
||||
цели подготовки хоста теперь подставляют `sudo` только при запуске не от root.
|
||||
|
||||
5. **Проверки 7 и 9 в `verify.sh` переформулированы.** Обе слушали аплинк `p2`,
|
||||
через который health-пробы и egress больше не проходят: пробы идут
|
||||
коммутацией на порт ВМ, а признаком профиля A стал MAC назначения (шлюз
|
||||
Docker, а не маршрутизатор сегмента), а не отсутствие трафика на `p2`.
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
- **Предпосылка архитектуры:** порты ВМ должны быть портами OVS. Если между
|
||||
клиентом и бэкендом окажется коммутатор, который SDN не контролирует, кейс не
|
||||
решается ничем, кроме SNAT.
|
||||
- **Хрупкость стенда:** veth, созданные `attach-segment.sh`, исчезают при
|
||||
пересоздании контейнера — нужен `make attach`. В целевой среде порты создаёт
|
||||
гипервизор.
|
||||
- **Весь внутрисегментный трафик проходит через `br-lb`** — пропускная
|
||||
способность узла становится пропускной способностью сегмента. Замер k6 не
|
||||
выполнен: в системе нет клиента k6; цель для сравнения —
|
||||
`http://10.20.0.100/` против `http://192.168.5.21/`.
|
||||
- **Обратная трансляция опирается на `ct`** — как и прежде. Stateless-вариант
|
||||
(`nw_src=member, tp_src=8080 → VIP:80`) остаётся заделом: в сегменте он
|
||||
потребует аккуратного исключения трафика, который бэкенд сам инициирует с
|
||||
порта 8080.
|
||||
- **Unicast ARP внутри сегмента рассылается флудом**, если MAC ещё не выучен —
|
||||
обычное поведение обучающегося коммутатора, но без таймера старения записей
|
||||
короче 300 с.
|
||||
- **Приватный листенер только TCP и только IPv4**, как и публичный. UDP- и
|
||||
L3-листенеров нет.
|
||||
- Кворум, control plane в OVSDB, ICMP-ошибки и фрагменты — по-прежнему вне рамок.
|
||||
|
||||
## Задел на шаг 4
|
||||
|
||||
1. **Stateless-датапас** для обоих путей: явный un-DNAT вместо `ct(nat)`.
|
||||
На коммутируемом пути это снимет последнюю зависимость от состояния.
|
||||
2. **Control plane:** модель `Load_Balancer → Listener → Pool → Member` в OVSDB
|
||||
вместо `topology.env`. Сейчас листенер, сегмент и состав пула описаны в трёх
|
||||
местах, а добавление ВМ требует правки `topology.env`, `hc-config.sh` и
|
||||
запуска `attach-segment.sh`.
|
||||
3. **Старение FDB и защита от переполнения** — сейчас записи живут 300 с без
|
||||
ограничения на число MAC в сегменте.
|
||||
4. Второй узел LB: кворум наблюдений, генерации пулов, сравнение дайджестов.
|
||||
+25
-3
@@ -19,8 +19,11 @@ lbctl — осмотр датапаса стенда hpnn_v2
|
||||
enable <член> вернуть член в балансировку
|
||||
metrics метрики в формате Prometheus
|
||||
conns таблица соединений ct (зона $CT_ZONE)
|
||||
trace <src_ip> [src_port]
|
||||
ofproto/trace для TCP-сессии клиент -> VIP:$VIP_PORT
|
||||
fdb таблица коммутации сегментов: выученные MAC и порты
|
||||
trace <src_ip> [src_port] [сегмент]
|
||||
ofproto/trace для TCP-сессии клиент -> VIP.
|
||||
Сегмент: pub (по умолчанию), p2, p3 — определяет порт входа
|
||||
и адрес листенера
|
||||
reload перегенерировать и применить пайплайн
|
||||
USAGE
|
||||
}
|
||||
@@ -68,11 +71,30 @@ metrics)
|
||||
conns)
|
||||
ovs-appctl dpctl/dump-conntrack | grep -F "zone=$CT_ZONE" || echo "соединений нет"
|
||||
;;
|
||||
fdb)
|
||||
echo "Выученные MAC (таблица 25, reg0 — номер сегмента):"
|
||||
# priority=100 — только записи, созданные обучением; правила рассылки
|
||||
# (priority 50 и 10) отбрасываются, иначе маска broadcast выглядела бы
|
||||
# как выученный MAC.
|
||||
ovs-ofctl -O "$OF" dump-flows "$BR" table=25 \
|
||||
| grep 'priority=100' \
|
||||
| sed -n 's/.*n_packets=\([0-9]*\).*reg0=\(0x[0-9a-f]*\).*dl_dst=\([0-9a-f:]*\).*output:\([0-9]*\).*/ сегмент \2 MAC \3 -> порт \4 (пакетов \1)/p' \
|
||||
| sort
|
||||
echo "Рассылка по сегменту (неизвестный MAC и broadcast):"
|
||||
ovs-ofctl -O "$OF" dump-flows "$BR" table=25 \
|
||||
| sed -n 's/.*n_packets=\([0-9]*\).*priority=\(50\|10\),reg0=\(0x[0-9a-f]*\).*/ сегмент \3 приоритет \2: пакетов \1/p' \
|
||||
| sort
|
||||
;;
|
||||
trace)
|
||||
src_ip="${2:?укажите IP клиента}"
|
||||
src_port="${3:-40000}"
|
||||
case "${4:-pub}" in
|
||||
p2) in_port=$CLI2_OFPORT; gw_mac=$P2_MAC; vip=$P2_VIP; vip_port=$PRIV_VIP_PORT ;;
|
||||
p3) in_port=$CLI3_OFPORT; gw_mac=$P3_MAC; vip=$P3_VIP; vip_port=$PRIV_VIP_PORT ;;
|
||||
*) in_port=$PUB_OFPORT; gw_mac=$PUB_MAC; vip=$VIP; vip_port=$VIP_PORT ;;
|
||||
esac
|
||||
ovs-appctl ofproto/trace "$BR" \
|
||||
"in_port=$PUB_OFPORT,dl_src=00:11:22:33:44:55,dl_dst=$PUB_MAC,dl_type=0x0800,nw_src=$src_ip,nw_dst=$VIP,nw_proto=6,nw_ttl=64,tp_src=$src_port,tp_dst=$VIP_PORT,tcp_flags=syn"
|
||||
"in_port=$in_port,dl_src=00:11:22:33:44:55,dl_dst=$gw_mac,dl_type=0x0800,nw_src=$src_ip,nw_dst=$vip,nw_proto=6,nw_ttl=64,tp_src=$src_port,tp_dst=$vip_port,tcp_flags=syn"
|
||||
;;
|
||||
reload)
|
||||
"$LB_DIR/apply.sh"
|
||||
|
||||
+222
-57
@@ -3,21 +3,33 @@
|
||||
# Применяется атомарным бандлом: ovs-ofctl --bundle -O OpenFlow15 replace-flows.
|
||||
#
|
||||
# Карта таблиц:
|
||||
# 0 — классификация по in_port и типу трафика, обучение MAC клиентов
|
||||
# 5 — ARP-респондер для собственных адресов узла и VIP
|
||||
# 0 — определение сегмента (reg0) по in_port, обучение MAC и adjacency
|
||||
# 1 — классификация: ARP, ICMP, листенер, L3-трафик к шлюзу, коммутация
|
||||
# 5 — ARP-респондер для собственных адресов узла и VIP, затем коммутация
|
||||
# 6 — ICMP echo-респондер для тех же адресов
|
||||
# 10 — листенеры: хэш сессии в слот (multipath), прочий IP -> маршрутизация
|
||||
# 11 — таблица слотов: слот -> член пула. Заливает и обновляет демон hcd
|
||||
# 12 — применение DNAT выбранным членом пула (по reg2)
|
||||
# 15 — обратный путь из приватных сегментов (снятие DNAT через ct)
|
||||
# 12 — применение выбранного члена: DNAT + выдача (L2 либо маршрутизация)
|
||||
# 15 — обратный путь L3 из приватных сегментов (снятие DNAT через ct)
|
||||
# 16 — пост-ct hook (счётчики, место под будущие проверки)
|
||||
# 20 — маршрутизация (LPM через приоритет = длина префикса)
|
||||
# 21 — adjacency: next-hop MAC + выходной порт
|
||||
# 24 — обратная трансляция коммутируемого трафика сегмента (шаг 3)
|
||||
# 25 — L2-коммутация сегмента: FDB, broadcast и unknown flood (шаг 3)
|
||||
#
|
||||
# Регистры: reg1 — номер слота, reg2 — идентификатор члена пула.
|
||||
# Регистры: reg0 — сегмент (1 — публичный, 2 и 3 — приватные), reg1 — номер
|
||||
# слота, reg2 — идентификатор члена пула.
|
||||
#
|
||||
# Нумерация не произвольна: goto_table в OpenFlow разрешает переход только
|
||||
# вперёд, поэтому обратный путь (15/16) стоит до общей маршрутизации (20).
|
||||
# вперёд, поэтому обратный путь (15/16) стоит до общей маршрутизации (20), а
|
||||
# коммутация сегмента (24/25) — после неё: в неё попадают и кадры, прошедшие
|
||||
# DNAT в таблице 12.
|
||||
#
|
||||
# Ключевое отличие шага 3: приватный сегмент коммутирует сам br-lb, каждая ВМ
|
||||
# подключена отдельным портом. Поэтому ответ бэкенда клиенту-соседу по подсети
|
||||
# проходит через таблицу 24, где ct снимает DNAT, — при этом ни в ОС ВМ, ни в
|
||||
# адресации сегмента ничего не меняется, а внутрисегментный трафик не
|
||||
# блокируется.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
@@ -34,20 +46,57 @@ LB_PUB_IP_H=$(ip2hex "$LB_PUB_IP")
|
||||
VIP_H=$(ip2hex "$VIP")
|
||||
LB_P2_IP_H=$(ip2hex "$LB_P2_IP")
|
||||
LB_P3_IP_H=$(ip2hex "$LB_P3_IP")
|
||||
P2_VIP_H=$(ip2hex "$P2_VIP")
|
||||
P3_VIP_H=$(ip2hex "$P3_VIP")
|
||||
|
||||
HC2_MAC_H=$(mac2hex "$HC2_MAC")
|
||||
HC3_MAC_H=$(mac2hex "$HC3_MAC")
|
||||
HC2_IP_H=$(ip2hex "$HC2_IP")
|
||||
HC3_IP_H=$(ip2hex "$HC3_IP")
|
||||
|
||||
# Действие обучения: по IP-адресу отправителя создаёт в таблице 21 запись
|
||||
# adjacency для обратного пути — куда и с каким MAC отправлять ответы этому
|
||||
# клиенту. Заменяет ARP-резолвер, которого у чисто-OpenFlow узла нет.
|
||||
LEARN="learn(table=21,priority=90,hard_timeout=300,eth_type=0x0800,NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],load:$PUB_MAC_H->NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])"
|
||||
# --- описание сегментов ------------------------------------------------------
|
||||
# Производные величины: номер сегмента в reg0, состав портов и члены пула.
|
||||
P2_REG=0x2
|
||||
P3_REG=0x3
|
||||
P2_PORTS="$P2_OFPORT $HC2_OFPORT $BE1_OFPORT $BE3_OFPORT $CLI2_OFPORT"
|
||||
P3_PORTS="$P3_OFPORT $HC3_OFPORT $BE2_OFPORT $BE4_OFPORT $CLI3_OFPORT"
|
||||
P2_MEMBERS="BE1 BE3"
|
||||
P3_MEMBERS="BE2 BE4"
|
||||
|
||||
# arp_responder <ip> <ip_hex> <mac> <mac_hex>
|
||||
# flood_ports <список ofport> — действие рассылки по сегменту. Порт входа OVS
|
||||
# исключает сам, поэтому отдельного «кроме in_port» не требуется.
|
||||
flood_ports() {
|
||||
local out="" p
|
||||
for p in $1; do out="$out,output:$p"; done
|
||||
echo "${out#,}"
|
||||
}
|
||||
|
||||
# Включён ли приватный листенер в сегменте.
|
||||
listener_on() {
|
||||
local seg="$1" s
|
||||
for s in $PRIV_LISTENERS; do [ "$s" = "$seg" ] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Действие обучения FDB: eth_src -> порт. Заполняет таблицу 25, то есть делает
|
||||
# из br-lb обычный обучающийся коммутатор в пределах сегмента.
|
||||
l2_learn() {
|
||||
echo "learn(table=25,priority=100,idle_timeout=300,NXM_NX_REG0[]=$1,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])"
|
||||
}
|
||||
|
||||
# Действие обучения adjacency: по IP-адресу отправителя создаёт в таблице 21
|
||||
# запись «куда и с каким MAC отправлять ответы этому адресу». Заменяет
|
||||
# ARP-резолвер, которого у чисто-OpenFlow узла нет. Нужна маршрутизируемому
|
||||
# трафику: ответ бэкенда из соседнего сегмента приходит к клиенту через L3.
|
||||
l3_learn() {
|
||||
echo "learn(table=21,priority=90,hard_timeout=300,eth_type=0x0800,NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],load:$1->NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])"
|
||||
}
|
||||
|
||||
LEARN_PUB=$(l3_learn "$PUB_MAC_H")
|
||||
|
||||
# arp_responder <reg0> <ip> <ip_hex> <mac> <mac_hex>
|
||||
arp_responder() {
|
||||
echo "table=5,priority=100,arp,arp_op=1,arp_tpa=$1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:$3,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:$4->NXM_NX_ARP_SHA[],load:$2->NXM_OF_ARP_SPA[],IN_PORT"
|
||||
echo "table=5,priority=100,reg0=$1,arp,arp_op=1,arp_tpa=$2 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:$4,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:$5->NXM_NX_ARP_SHA[],load:$3->NXM_OF_ARP_SPA[],IN_PORT"
|
||||
}
|
||||
|
||||
# icmp_responder <ip> <ip_hex> <mac_hex>
|
||||
@@ -55,55 +104,114 @@ icmp_responder() {
|
||||
echo "table=6,priority=100,icmp,nw_dst=$1,icmp_type=8 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],load:$3->NXM_OF_ETH_SRC[],move:NXM_OF_IP_SRC[]->NXM_OF_IP_DST[],load:$2->NXM_OF_IP_SRC[],load:0->NXM_OF_ICMP_TYPE[],IN_PORT"
|
||||
}
|
||||
|
||||
# segment_ingress <reg0> <mac_hex шлюза> <сеть> <порты> — таблица 0 для сегмента.
|
||||
# Обучение FDB идёт для любого кадра, обучение adjacency — только для адресов
|
||||
# самого сегмента: иначе с аплинка в таблицу 21 попал бы весь внешний мир.
|
||||
segment_ingress() {
|
||||
local reg="$1" gwmac_h="$2" net="$3" ports="$4" p
|
||||
local l2 l3
|
||||
l2=$(l2_learn "$reg")
|
||||
l3=$(l3_learn "$gwmac_h")
|
||||
for p in $ports; do
|
||||
echo "table=0,priority=200,in_port=$p,ip,nw_src=$net actions=load:$reg->NXM_NX_REG0[],$l2,$l3,goto_table:1"
|
||||
echo "table=0,priority=190,in_port=$p actions=load:$reg->NXM_NX_REG0[],$l2,goto_table:1"
|
||||
done
|
||||
}
|
||||
|
||||
# segment_classify <сегмент> <reg0> <mac шлюза> <vip> — таблица 1 для сегмента.
|
||||
segment_classify() {
|
||||
local seg="$1" reg="$2" gwmac="$3" vip="$4"
|
||||
echo "# --- сегмент $seg ---"
|
||||
if listener_on "$seg"; then
|
||||
echo "table=1,priority=130,reg0=$reg,dl_dst=$gwmac,tcp,nw_dst=$vip,tp_dst=$PRIV_VIP_PORT actions=goto_table:10"
|
||||
fi
|
||||
# Трафик на VIP, для которого листенера нет, отбрасывается со счётчиком —
|
||||
# так видно, что адрес занят стендом, а листенер выключен.
|
||||
echo "table=1,priority=125,reg0=$reg,dl_dst=$gwmac,ip,nw_dst=$vip actions=drop"
|
||||
# Кадр адресован маршрутизатору сегмента: транзит, ответы балансируемых
|
||||
# сессий из другого сегмента, обращения наружу через LB.
|
||||
echo "table=1,priority=120,reg0=$reg,dl_dst=$gwmac,ip actions=goto_table:15"
|
||||
# Всё остальное — трафик между ВМ сегмента: коммутация, при необходимости
|
||||
# с обратной трансляцией.
|
||||
echo "table=1,priority=100,reg0=$reg actions=goto_table:24"
|
||||
}
|
||||
|
||||
# segment_untranslate <reg0> <члены> — таблица 24.
|
||||
segment_untranslate() {
|
||||
local reg="$1" m ip port
|
||||
for m in $2; do
|
||||
eval "ip=\$${m}_IP; port=\$${m}_PORT"
|
||||
echo "table=24,priority=100,reg0=$reg,tcp,nw_src=$ip,tp_src=$port actions=ct(table=25,zone=$CT_ZONE,nat)"
|
||||
done
|
||||
}
|
||||
|
||||
cat <<EOF
|
||||
# =============================================================================
|
||||
# Таблица 0 — классификация
|
||||
# Таблица 0 — определение сегмента и обучение
|
||||
# =============================================================================
|
||||
# Публичный сегмент маршрутизируется как прежде: macvlan-порт один, коммутации
|
||||
# внутри него нет. Обучение adjacency для него живёт в таблице 1, где известно,
|
||||
# что трафик адресован стенду.
|
||||
table=0,priority=200,in_port=$PUB_OFPORT actions=load:0x1->NXM_NX_REG0[],goto_table:1
|
||||
|
||||
# Приватные сегменты: каждая ВМ — отдельный порт, br-lb работает коммутатором.
|
||||
$(segment_ingress "$P2_REG" "$P2_MAC_H" "$P2_NET" "$P2_PORTS")
|
||||
$(segment_ingress "$P3_REG" "$P3_MAC_H" "$P3_NET" "$P3_PORTS")
|
||||
|
||||
table=0,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 1 — классификация
|
||||
# =============================================================================
|
||||
# ARP обрабатывает собственный респондер (таблица 5): ядро в форвардинге не
|
||||
# участвует, поэтому штатного ARP-стека у узла нет.
|
||||
table=0,priority=200,arp actions=goto_table:5
|
||||
table=1,priority=200,arp actions=goto_table:5
|
||||
|
||||
# ICMP echo-request на собственные адреса узла и на VIP — в респондер.
|
||||
table=0,priority=190,icmp,nw_dst=$LB_PUB_IP,icmp_type=8 actions=goto_table:6
|
||||
table=0,priority=190,icmp,nw_dst=$VIP,icmp_type=8 actions=goto_table:6
|
||||
table=0,priority=190,icmp,nw_dst=$LB_P2_IP,icmp_type=8 actions=goto_table:6
|
||||
table=0,priority=190,icmp,nw_dst=$LB_P3_IP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$LB_PUB_IP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$VIP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$LB_P2_IP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$LB_P3_IP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$P2_VIP,icmp_type=8 actions=goto_table:6
|
||||
table=1,priority=190,icmp,nw_dst=$P3_VIP,icmp_type=8 actions=goto_table:6
|
||||
|
||||
# Вход из публичного сегмента: запоминаем MAC отправителя как adjacency для
|
||||
# обратного пути (замена ARP-резолвера на клиентской стороне), затем к
|
||||
# листенерам. Записи живут 300 с и обновляются каждым пакетом клиента.
|
||||
# обратного пути, затем к листенерам. Записи живут 300 с и обновляются каждым
|
||||
# пакетом клиента.
|
||||
#
|
||||
# Обучение включено только для трафика, адресованного стенду (VIP, адрес узла,
|
||||
# приватные сегменты). Иначе в таблицу 21 попадал бы весь широковещательный
|
||||
# шум LAN — macvlan-порт видит DHCP, mDNS и SSDP всех соседей по сегменту.
|
||||
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$VIP actions=$LEARN,goto_table:10
|
||||
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$LB_PUB_IP actions=$LEARN,goto_table:10
|
||||
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$P2_NET actions=$LEARN,goto_table:10
|
||||
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$P3_NET actions=$LEARN,goto_table:10
|
||||
table=0,priority=100,in_port=$PUB_OFPORT,ip actions=drop
|
||||
table=1,priority=110,reg0=0x1,ip,nw_dst=$VIP actions=$LEARN_PUB,goto_table:10
|
||||
table=1,priority=110,reg0=0x1,ip,nw_dst=$LB_PUB_IP actions=$LEARN_PUB,goto_table:10
|
||||
table=1,priority=110,reg0=0x1,ip,nw_dst=$P2_NET actions=$LEARN_PUB,goto_table:10
|
||||
table=1,priority=110,reg0=0x1,ip,nw_dst=$P3_NET actions=$LEARN_PUB,goto_table:10
|
||||
table=1,priority=100,reg0=0x1,ip actions=drop
|
||||
|
||||
# Вход из приватных сегментов: обратный путь балансируемых сессий и транзит.
|
||||
table=0,priority=100,in_port=$P2_OFPORT,ip actions=goto_table:15
|
||||
table=0,priority=100,in_port=$P3_OFPORT,ip actions=goto_table:15
|
||||
$(segment_classify p2 "$P2_REG" "$P2_MAC" "$P2_VIP")
|
||||
$(segment_classify p3 "$P3_REG" "$P3_MAC" "$P3_VIP")
|
||||
|
||||
# Health-пробы, порождённые самим узлом: сразу в маршрутизацию, без ct и без
|
||||
# обучения — это трафик из стека узла, а не клиентская сессия.
|
||||
table=0,priority=100,in_port=$HC2_OFPORT,ip actions=goto_table:20
|
||||
table=0,priority=100,in_port=$HC3_OFPORT,ip actions=goto_table:20
|
||||
|
||||
# Всё остальное (не-IP, не-ARP: broadcast, IPv6, DHCP) в прототипе не нужно.
|
||||
table=0,priority=0 actions=drop
|
||||
# Всё остальное (не-IP, не-ARP с публичного порта) в прототипе не нужно.
|
||||
table=1,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 5 — ARP-респондер
|
||||
# =============================================================================
|
||||
$(arp_responder "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC" "$PUB_MAC_H")
|
||||
$(arp_responder "$VIP" "$VIP_H" "$PUB_MAC" "$PUB_MAC_H")
|
||||
$(arp_responder "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC" "$P2_MAC_H")
|
||||
$(arp_responder "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC" "$P3_MAC_H")
|
||||
$(arp_responder 0x1 "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC" "$PUB_MAC_H")
|
||||
$(arp_responder 0x1 "$VIP" "$VIP_H" "$PUB_MAC" "$PUB_MAC_H")
|
||||
$(arp_responder "$P2_REG" "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC" "$P2_MAC_H")
|
||||
$(arp_responder "$P3_REG" "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC" "$P3_MAC_H")
|
||||
# VIP приватных листенеров отвечают тем же MAC, что и шлюз сегмента: по нему
|
||||
# таблица 1 отличает трафик к маршрутизатору от трафика между ВМ.
|
||||
$(arp_responder "$P2_REG" "$P2_VIP" "$P2_VIP_H" "$P2_MAC" "$P2_MAC_H")
|
||||
$(arp_responder "$P3_REG" "$P3_VIP" "$P3_VIP_H" "$P3_MAC" "$P3_MAC_H")
|
||||
# Адреса источника health-проб: бэкенд резолвит их, отвечая на пробу.
|
||||
$(arp_responder "$HC2_IP" "$HC2_IP_H" "$HC2_MAC" "$HC2_MAC_H")
|
||||
$(arp_responder "$HC3_IP" "$HC3_IP_H" "$HC3_MAC" "$HC3_MAC_H")
|
||||
$(arp_responder "$P2_REG" "$HC2_IP" "$HC2_IP_H" "$HC2_MAC" "$HC2_MAC_H")
|
||||
$(arp_responder "$P3_REG" "$HC3_IP" "$HC3_IP_H" "$HC3_MAC" "$HC3_MAC_H")
|
||||
# Остальной ARP сегмента — обычная коммутация: ВМ резолвят друг друга сами,
|
||||
# балансировщик в это не вмешивается.
|
||||
table=5,priority=10,reg0=$P2_REG actions=goto_table:25
|
||||
table=5,priority=10,reg0=$P3_REG actions=goto_table:25
|
||||
table=5,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
@@ -113,24 +221,38 @@ $(icmp_responder "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC_H")
|
||||
$(icmp_responder "$VIP" "$VIP_H" "$PUB_MAC_H")
|
||||
$(icmp_responder "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC_H")
|
||||
$(icmp_responder "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC_H")
|
||||
$(icmp_responder "$P2_VIP" "$P2_VIP_H" "$P2_MAC_H")
|
||||
$(icmp_responder "$P3_VIP" "$P3_VIP_H" "$P3_MAC_H")
|
||||
table=6,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 10 — листенеры
|
||||
# =============================================================================
|
||||
# Единственный листенер прототипа: TCP $VIP:$VIP_PORT.
|
||||
# multipath раскладывает сессию в один из $SLOTS слотов по симметричному
|
||||
# L4-хэшу; basis = POOL_ID, поэтому разные пулы дают независимые раскладки.
|
||||
# Хэш симметричен, то есть прямое и обратное направление дают один слот —
|
||||
# свойство, необходимое для будущего stateless-датапаса.
|
||||
table=10,priority=200,tcp,nw_dst=$VIP,tp_dst=$VIP_PORT actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[]),goto_table:11
|
||||
#
|
||||
# Публичный листенер: TCP $VIP:$VIP_PORT.
|
||||
table=10,priority=200,reg0=0x1,tcp,nw_dst=$VIP,tp_dst=$VIP_PORT actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[]),goto_table:11
|
||||
|
||||
# Трафик на VIP, для которого листенера нет, — отбрасываем со счётчиком.
|
||||
table=10,priority=150,ip,nw_dst=$VIP actions=drop
|
||||
table=10,priority=150,reg0=0x1,ip,nw_dst=$VIP actions=drop
|
||||
|
||||
# Прочий IP из публичного сегмента маршрутизируется обычным образом
|
||||
# (клиент может обратиться напрямую к бэкенду, минуя балансировку).
|
||||
table=10,priority=100,ip actions=goto_table:20
|
||||
table=10,priority=100,reg0=0x1,ip actions=goto_table:20
|
||||
EOF
|
||||
|
||||
# Приватные листенеры: тот же пул, та же таблица слотов, тот же хэш. Отличие
|
||||
# только в адресе и в том, что выдача пойдёт коммутацией (таблица 12).
|
||||
for seg in p2 p3; do
|
||||
listener_on "$seg" || continue
|
||||
if [ "$seg" = p2 ]; then reg=$P2_REG; vip=$P2_VIP; else reg=$P3_REG; vip=$P3_VIP; fi
|
||||
echo "table=10,priority=200,reg0=$reg,tcp,nw_dst=$vip,tp_dst=$PRIV_VIP_PORT actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[]),goto_table:11"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
table=10,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
@@ -145,10 +267,28 @@ table=10,priority=0 actions=drop
|
||||
table=11,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 12 — DNAT выбранным членом пула
|
||||
# Таблица 12 — применение выбранного члена пула
|
||||
# =============================================================================
|
||||
# Слот положил идентификатор члена в reg2. SNAT не выполняется: бэкенд видит
|
||||
# реальный адрес клиента.
|
||||
# Слот положил идентификатор члена в reg2. SNAT не выполняется ни в одном из
|
||||
# путей: бэкенд видит реальный адрес клиента.
|
||||
#
|
||||
# Член в том же сегменте, что и клиент (приоритет 200) — выдача коммутацией:
|
||||
# меняется только MAC назначения, TTL не уменьшается, маршрутизации нет. Для
|
||||
# ВМ клиент и бэкенд остаются L2-соседями, какими и были.
|
||||
EOF
|
||||
|
||||
for seg_reg in "$P2_REG:$P2_MEMBERS" "$P3_REG:$P3_MEMBERS"; do
|
||||
reg="${seg_reg%%:*}"
|
||||
for m in ${seg_reg#*:}; do
|
||||
eval "mip=\$${m}_IP; mport=\$${m}_PORT; mmac=\$${m}_MAC; mid=\$${m}_ID"
|
||||
echo "table=12,priority=200,reg0=$reg,ip,reg2=$mid actions=mod_dl_dst:$mmac,ct(commit,zone=$CT_ZONE,nat(dst=$mip:$mport),table=25)"
|
||||
done
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
|
||||
# Член в другом сегменте либо публичный листенер — обычный путь через
|
||||
# маршрутизацию: адрес назначения уже переписан, дальше работают таблицы 20/21.
|
||||
table=12,priority=100,ip,reg2=0x1 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE1_IP:$BE1_PORT),table=20)
|
||||
table=12,priority=100,ip,reg2=0x2 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE2_IP:$BE2_PORT),table=20)
|
||||
table=12,priority=100,ip,reg2=0x3 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE3_IP:$BE3_PORT),table=20)
|
||||
@@ -156,11 +296,11 @@ table=12,priority=100,ip,reg2=0x4 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE4_I
|
||||
table=12,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 15 — обратный путь из приватных сегментов
|
||||
# Таблица 15 — обратный путь L3
|
||||
# =============================================================================
|
||||
# ct(nat) без commit снимает ранее выполненный DNAT: source возвращается к VIP.
|
||||
# Для сессий, которых нет в таблице соединений (трафик be1<->be2, обращения
|
||||
# бэкендов к клиентским префиксам), пакет проходит без изменений.
|
||||
# Для сессий, которых нет в таблице соединений (транзит между сегментами,
|
||||
# обращения бэкендов к клиентским префиксам), пакет проходит без изменений.
|
||||
table=15,priority=100,tcp actions=ct(table=16,zone=$CT_ZONE,nat)
|
||||
table=15,priority=50,ip actions=goto_table:20
|
||||
table=15,priority=0 actions=drop
|
||||
@@ -189,14 +329,39 @@ table=20,priority=0 actions=drop
|
||||
# =============================================================================
|
||||
# Таблица 21 — adjacency (next-hop MAC + выходной порт)
|
||||
# =============================================================================
|
||||
# Бэкенды прописаны статически: их MAC фиксирован в docker-compose.yml.
|
||||
table=21,priority=100,ip,nw_dst=$BE1_IP actions=mod_dl_dst:$BE1_MAC,output:$P2_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE2_IP actions=mod_dl_dst:$BE2_MAC,output:$P3_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE3_IP actions=mod_dl_dst:$BE3_MAC,output:$P2_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE4_IP actions=mod_dl_dst:$BE4_MAC,output:$P3_OFPORT
|
||||
# Члены пула прописаны статически: их MAC и порт фиксированы в topology.env.
|
||||
table=21,priority=100,ip,nw_dst=$BE1_IP actions=mod_dl_dst:$BE1_MAC,output:$BE1_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE2_IP actions=mod_dl_dst:$BE2_MAC,output:$BE2_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE3_IP actions=mod_dl_dst:$BE3_MAC,output:$BE3_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$BE4_IP actions=mod_dl_dst:$BE4_MAC,output:$BE4_OFPORT
|
||||
# Ответы на health-пробы — в стек узла через internal-порты.
|
||||
table=21,priority=100,ip,nw_dst=$HC2_IP actions=mod_dl_dst:$HC2_MAC,output:$HC2_OFPORT
|
||||
table=21,priority=100,ip,nw_dst=$HC3_IP actions=mod_dl_dst:$HC3_MAC,output:$HC3_OFPORT
|
||||
# priority=90 — записи клиентов, устанавливаемые действием learn из таблицы 0.
|
||||
# priority=90 — записи клиентов, устанавливаемые действием learn из таблиц 0 и 1.
|
||||
table=21,priority=0 actions=drop
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 24 — обратная трансляция коммутируемого трафика сегмента
|
||||
# =============================================================================
|
||||
# Здесь решается задача шага 3. Бэкенд отвечает клиенту-соседу напрямую по L2,
|
||||
# но кадр всё равно проходит через br-lb, потому что порт ВМ — порт OVS.
|
||||
# ct(nat) без commit возвращает source к VIP:80 для балансируемых сессий и
|
||||
# пропускает без изменений всё остальное — обращения be<->be, ответы на
|
||||
# health-пробы, прямые обращения клиента к бэкенду.
|
||||
$(segment_untranslate "$P2_REG" "$P2_MEMBERS")
|
||||
$(segment_untranslate "$P3_REG" "$P3_MEMBERS")
|
||||
table=24,priority=0 actions=goto_table:25
|
||||
|
||||
# =============================================================================
|
||||
# Таблица 25 — L2-коммутация сегмента
|
||||
# =============================================================================
|
||||
# priority=100 — записи FDB, устанавливаемые действием learn из таблицы 0.
|
||||
# Ниже — рассылка по сегменту: broadcast и multicast отдельным правилом ради
|
||||
# счётчика, неизвестный unicast — тем же способом. Порт входа OVS исключает
|
||||
# из рассылки сам.
|
||||
table=25,priority=50,reg0=$P2_REG,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=$(flood_ports "$P2_PORTS")
|
||||
table=25,priority=50,reg0=$P3_REG,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=$(flood_ports "$P3_PORTS")
|
||||
table=25,priority=10,reg0=$P2_REG actions=$(flood_ports "$P2_PORTS")
|
||||
table=25,priority=10,reg0=$P3_REG actions=$(flood_ports "$P3_PORTS")
|
||||
table=25,priority=0 actions=drop
|
||||
EOF
|
||||
+39
-2
@@ -17,18 +17,35 @@ LB_PUB_IP=192.168.5.20
|
||||
VIP=192.168.5.21
|
||||
VIP_PORT=80
|
||||
|
||||
# --- сеть 2: приватный сегмент бэкенда be1 -----------------------------------
|
||||
# --- сеть 2: приватный сегмент 10.20.0.0/24 ----------------------------------
|
||||
# С шага 3 сегмент коммутирует сам br-lb: каждая ВМ сегмента подключена
|
||||
# отдельным портом (см. scripts/attach-segment.sh). Порт p2 остался аплинком к
|
||||
# docker-бриджу: через него идёт egress бэкендов на шлюз .254 (профиль A).
|
||||
#
|
||||
# Адрес шлюза LB_P2_IP и VIP приватного листенера P2_VIP отвечают одним MAC
|
||||
# (P2_MAC) — по нему пайплайн отличает L3-трафик к маршрутизатору от
|
||||
# L2-трафика сегмента.
|
||||
P2_IFNAME=p2
|
||||
P2_OFPORT=2
|
||||
P2_MAC=02:42:0a:14:00:01
|
||||
P2_NET=10.20.0.0/24
|
||||
P2_DOCKER_GW=10.20.0.254
|
||||
LB_P2_IP=10.20.0.1
|
||||
BE1_IP=10.20.0.2
|
||||
BE1_MAC=02:42:0a:14:00:02
|
||||
BE1_PORT=8080
|
||||
BE1_OFPORT=10
|
||||
BE1_ID=0x1
|
||||
BE3_IP=10.20.0.3
|
||||
BE3_MAC=02:42:0a:14:00:03
|
||||
BE3_PORT=8080
|
||||
BE3_OFPORT=11
|
||||
BE3_ID=0x3
|
||||
# Клиент внутри сегмента — та самая ВМ, что живёт рядом с бэкендами.
|
||||
# В его ОС не настраивается ничего, кроме адреса: VIP находится в его подсети.
|
||||
CLI2_IP=10.20.0.10
|
||||
CLI2_MAC=02:42:0a:14:00:0a
|
||||
CLI2_OFPORT=12
|
||||
# Уникальный адрес узла в сегменте — источник health-проб (§7.1 дизайна v1).
|
||||
# Живёт на internal-порту OVS: это единственный адрес, который узел держит в
|
||||
# ядре, весь остальной форвардинг идёт мимо стека.
|
||||
@@ -37,23 +54,43 @@ HC2_OFPORT=4
|
||||
HC2_MAC=02:42:0a:14:00:fd
|
||||
HC2_IP=10.20.0.253
|
||||
|
||||
# --- сеть 3: приватный сегмент бэкенда be2 -----------------------------------
|
||||
# --- сеть 3: приватный сегмент 10.30.0.0/24 ----------------------------------
|
||||
P3_IFNAME=p3
|
||||
P3_OFPORT=3
|
||||
P3_MAC=02:42:0a:1e:00:01
|
||||
P3_NET=10.30.0.0/24
|
||||
P3_DOCKER_GW=10.30.0.254
|
||||
LB_P3_IP=10.30.0.1
|
||||
BE2_IP=10.30.0.2
|
||||
BE2_MAC=02:42:0a:1e:00:02
|
||||
BE2_PORT=8080
|
||||
BE2_OFPORT=20
|
||||
BE2_ID=0x2
|
||||
BE4_IP=10.30.0.3
|
||||
BE4_MAC=02:42:0a:1e:00:03
|
||||
BE4_PORT=8080
|
||||
BE4_OFPORT=21
|
||||
BE4_ID=0x4
|
||||
CLI3_IP=10.30.0.10
|
||||
CLI3_MAC=02:42:0a:1e:00:0a
|
||||
CLI3_OFPORT=22
|
||||
HC3_IFNAME=hcif-p3
|
||||
HC3_OFPORT=5
|
||||
HC3_MAC=02:42:0a:1e:00:fd
|
||||
HC3_IP=10.30.0.253
|
||||
|
||||
# --- приватные листенеры (шаг 3) ---------------------------------------------
|
||||
# VIP берётся из подсети сегмента: клиенту не нужны ни маршруты, ни настройки —
|
||||
# адрес находится в его собственной сети, ARP-респондер OVS отвечает за него.
|
||||
#
|
||||
# PRIV_LISTENERS перечисляет сегменты, где листенер поднят. Пустое значение
|
||||
# выключает приватные листенеры целиком, "p2" — включает один сегмент,
|
||||
# "p2 p3" — оба. Пул и таблица слотов общие с публичным листенером.
|
||||
PRIV_LISTENERS="p2 p3"
|
||||
PRIV_VIP_PORT=80
|
||||
P2_VIP=10.20.0.100
|
||||
P3_VIP=10.30.0.100
|
||||
|
||||
# --- пул и таблица слотов ----------------------------------------------------
|
||||
# POOL_ID служит basis хэша multipath: разные пулы дают независимые раскладки.
|
||||
POOL_ID=1
|
||||
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# Подключение ВМ приватных сегментов к мосту br-lb (шаг 3).
|
||||
#
|
||||
# В целевой среде порты виртуальных машин создаёт гипервизор, и каждая ВМ уже
|
||||
# висит на vSwitch. В контейнерном стенде эту роль выполняет этот скрипт: для
|
||||
# каждого участника сегмента он создаёт veth-пару, один конец кладёт в сетевое
|
||||
# пространство имён контейнера, другой — в netns балансировщика и добавляет в
|
||||
# br-lb с детерминированным ofport.
|
||||
#
|
||||
# Адрес, MAC и маршруты назначаются СНАРУЖИ, как это делают гипервизор и DHCP.
|
||||
# Внутри ОС ВМ ничего не настраивается — это условие шага 3.
|
||||
#
|
||||
# Запускать на хосте с правами root, после docker compose up:
|
||||
# sudo scripts/attach-segment.sh # подключить
|
||||
# sudo scripts/attach-segment.sh --status # показать порты моста
|
||||
# sudo scripts/attach-segment.sh --detach # снять порты
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck disable=SC1091
|
||||
source lb/topology.env
|
||||
|
||||
LB_CONTAINER=hpnn-lb
|
||||
|
||||
log() { echo "[attach] $*"; }
|
||||
die() { echo "[attach] ОШИБКА: $*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" = 0 ] || die "нужны права root (sudo scripts/attach-segment.sh)"
|
||||
|
||||
pid_of() {
|
||||
local pid
|
||||
pid=$(docker inspect -f '{{.State.Pid}}' "$1" 2>/dev/null) || die "контейнер $1 не найден"
|
||||
[ "$pid" != "0" ] || die "контейнер $1 не запущен"
|
||||
echo "$pid"
|
||||
}
|
||||
|
||||
# attach <контейнер> <имя порта> <ofport> <mac> <ip> <префикс> <шлюз|-> [сеть via LB]...
|
||||
attach() {
|
||||
local cname="$1" port="$2" ofport="$3" mac="$4" ip="$5" plen="$6" gw="$7"
|
||||
shift 7
|
||||
local pid pid_lb tmp
|
||||
|
||||
pid=$(pid_of "$cname")
|
||||
pid_lb=$(pid_of "$LB_CONTAINER")
|
||||
|
||||
if nsenter -t "$pid" -n ip link show eth0 >/dev/null 2>&1; then
|
||||
log "$cname уже подключён, пропуск"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Временное имя нужно потому, что оба конца veth рождаются в netns хоста и
|
||||
# имя eth0 там почти наверняка занято.
|
||||
tmp="tmp$$-${ofport}"
|
||||
ip link add "$port" type veth peer name "$tmp"
|
||||
ip link set "$port" netns "$pid_lb"
|
||||
ip link set "$tmp" netns "$pid"
|
||||
|
||||
nsenter -t "$pid" -n ip link set "$tmp" name eth0
|
||||
nsenter -t "$pid" -n ip link set eth0 address "$mac"
|
||||
nsenter -t "$pid" -n ip addr replace "$ip/$plen" dev eth0
|
||||
nsenter -t "$pid" -n ip link set eth0 up
|
||||
# Профиль A: default остаётся на шлюзе Docker, через LB маршрутизируются
|
||||
# только клиентские префиксы и соседний сегмент. Клиентам не выдаётся ничего.
|
||||
[ "$gw" = "-" ] || nsenter -t "$pid" -n ip route replace default via "$gw"
|
||||
while [ $# -ge 1 ]; do
|
||||
nsenter -t "$pid" -n ip route replace "$1" via "$2"
|
||||
shift 2
|
||||
done
|
||||
|
||||
nsenter -t "$pid_lb" -n ip link set "$port" up
|
||||
docker exec "$LB_CONTAINER" ovs-vsctl --may-exist add-port "$BR" "$port" \
|
||||
-- set Interface "$port" ofport_request="$ofport" >/dev/null
|
||||
|
||||
log "$cname -> порт $port (ofport $ofport), $ip/$plen, MAC $mac"
|
||||
}
|
||||
|
||||
detach() {
|
||||
local port
|
||||
for port in be1 be3 cli2 be2 be4 cli3; do
|
||||
docker exec "$LB_CONTAINER" ovs-vsctl --if-exists del-port "$BR" "$port" >/dev/null 2>&1 || true
|
||||
done
|
||||
log "порты сегментов сняты с моста (интерфейсы исчезнут вместе с контейнерами)"
|
||||
}
|
||||
|
||||
status() {
|
||||
docker exec "$LB_CONTAINER" ovs-ofctl -O "$OF" show "$BR" | grep -E '^\s+[0-9]+\('
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--status) status; exit 0 ;;
|
||||
--detach) detach; exit 0 ;;
|
||||
esac
|
||||
|
||||
p2len=${P2_NET##*/}
|
||||
p3len=${P3_NET##*/}
|
||||
|
||||
# Бэкенды сегмента 2: default на docker-шлюз, клиентский префикс и соседний
|
||||
# сегмент — через балансировщик.
|
||||
attach hpnn-be1 be1 "$BE1_OFPORT" "$BE1_MAC" "$BE1_IP" "$p2len" "$P2_DOCKER_GW" \
|
||||
192.168.5.0/24 "$LB_P2_IP" "$P3_NET" "$LB_P2_IP"
|
||||
attach hpnn-be3 be3 "$BE3_OFPORT" "$BE3_MAC" "$BE3_IP" "$p2len" "$P2_DOCKER_GW" \
|
||||
192.168.5.0/24 "$LB_P2_IP" "$P3_NET" "$LB_P2_IP"
|
||||
attach hpnn-be2 be2 "$BE2_OFPORT" "$BE2_MAC" "$BE2_IP" "$p3len" "$P3_DOCKER_GW" \
|
||||
192.168.5.0/24 "$LB_P3_IP" "$P2_NET" "$LB_P3_IP"
|
||||
attach hpnn-be4 be4 "$BE4_OFPORT" "$BE4_MAC" "$BE4_IP" "$p3len" "$P3_DOCKER_GW" \
|
||||
192.168.5.0/24 "$LB_P3_IP" "$P2_NET" "$LB_P3_IP"
|
||||
|
||||
# Клиенты: только адрес и маска. Ни маршрутов, ни дополнительных интерфейсов —
|
||||
# ровно то, что требует условие шага 3.
|
||||
attach hpnn-cli2 cli2 "$CLI2_OFPORT" "$CLI2_MAC" "$CLI2_IP" "$p2len" -
|
||||
attach hpnn-cli3 cli3 "$CLI3_OFPORT" "$CLI3_MAC" "$CLI3_IP" "$p3len" -
|
||||
|
||||
log "готово"
|
||||
status
|
||||
+90
-7
@@ -18,7 +18,7 @@ skip() { echo -e " \033[33mПРОПУСК\033[0m $*"; }
|
||||
head_() { echo; echo "== $*"; }
|
||||
|
||||
head_ "1. Контейнеры"
|
||||
for c in hpnn-lb hpnn-be1 hpnn-be2 hpnn-be3 hpnn-be4; do
|
||||
for c in hpnn-lb hpnn-be1 hpnn-be2 hpnn-be3 hpnn-be4 hpnn-cli2 hpnn-cli3; do
|
||||
state=$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || echo "нет")
|
||||
[ "$state" = "running" ] && ok "$c: $state" || bad "$c: $state"
|
||||
done
|
||||
@@ -28,11 +28,17 @@ ports=$($DC exec -T lb-router ovs-vsctl list-ports br-lb 2>/dev/null | tr '\n' '
|
||||
for p in pub0 p2 p3; do
|
||||
echo "$ports" | grep -qw "$p" && ok "порт $p в мосту" || bad "порт $p отсутствует (есть: $ports)"
|
||||
done
|
||||
# Шаг 3: каждая ВМ сегмента — отдельный порт моста. Без этого балансировщик не
|
||||
# увидит ответ бэкенда клиенту-соседу и приватный листенер работать не сможет.
|
||||
for p in be1 be3 cli2 be2 be4 cli3; do
|
||||
echo "$ports" | grep -qw "$p" && ok "ВМ $p подключена портом OVS" \
|
||||
|| bad "порт $p отсутствует — выполните make attach"
|
||||
done
|
||||
dp=$($DC exec -T lb-router ovs-appctl dpif/show 2>/dev/null | head -1)
|
||||
[ -n "$dp" ] && ok "датапас: $dp" || bad "датапас не поднят"
|
||||
|
||||
head_ "3. Пайплайн"
|
||||
for t in 0 5 6 10 11 12 15 16 20 21; do
|
||||
for t in 0 1 5 6 10 11 12 15 16 20 21 24 25; do
|
||||
n=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb "table=$t" 2>/dev/null | grep -c 'table=')
|
||||
[ "${n:-0}" -gt 0 ] && ok "таблица $t: $n правил" || bad "таблица $t пуста"
|
||||
done
|
||||
@@ -90,9 +96,12 @@ else
|
||||
skip "shim не поднят — выполните: sudo scripts/host-prereq.sh --shim"
|
||||
fi
|
||||
|
||||
head_ "7. Профиль A: собственный egress бэкенда идёт мимо балансировщика"
|
||||
head_ "7. Профиль A: собственный egress бэкенда не маршрутизируется балансировщиком"
|
||||
# С шага 3 br-lb коммутирует сегмент, поэтому egress физически через него
|
||||
# проходит — но только как через коммутатор. Признак: кадр адресован MAC
|
||||
# docker-шлюза, а не MAC маршрутизатора сегмента (02:42:0a:14:00:01).
|
||||
dump=$(mktemp)
|
||||
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -ni p2 -c 2 'host 1.1.1.1' >"$dump" 2>&1 &
|
||||
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -eni be1 -c 2 'host 1.1.1.1' >"$dump" 2>&1 &
|
||||
tcpdump_pid=$!
|
||||
sleep 2
|
||||
code=$($DC exec -T be1 curl -s -o /dev/null -w '%{http_code}' --max-time 6 http://1.1.1.1/ 2>/dev/null)
|
||||
@@ -100,9 +109,13 @@ wait $tcpdump_pid 2>/dev/null
|
||||
[ "${code:-000}" != "000" ] && ok "be1 достучался наружу (HTTP $code) через шлюз Docker" \
|
||||
|| bad "be1 не имеет выхода наружу (HTTP ${code:-000})"
|
||||
if grep -q '^[0-9][0-9]:' "$dump"; then
|
||||
bad "трафик egress виден на порту p2 балансировщика:"; sed 's/^/ /' "$dump"
|
||||
if grep -q '> 02:42:0a:14:00:01' "$dump"; then
|
||||
bad "egress адресован маршрутизатору сегмента — default ушёл на балансировщик"
|
||||
else
|
||||
ok "egress адресован шлюзу Docker, а не маршрутизатору сегмента"
|
||||
fi
|
||||
else
|
||||
ok "на порту p2 балансировщика этого трафика нет"
|
||||
bad "трафик egress не наблюдается на порту ВМ"
|
||||
fi
|
||||
rm -f "$dump"
|
||||
|
||||
@@ -144,7 +157,9 @@ done
|
||||
[ "$uneven" -eq 0 ] && ok "слоты поделены поровну:$dist" || bad "неравномерная раскладка:$dist"
|
||||
|
||||
head_ "9. Источник health-проб — уникальный адрес узла, не VIP"
|
||||
src=$($DC exec -T lb-router timeout 6 tcpdump -ni p2 -c 1 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0' 2>/dev/null \
|
||||
# Слушаем порт самой ВМ: с шага 3 проба идёт коммутацией сегмента, а не через
|
||||
# аплинк p2, поэтому на аплинке её больше не видно.
|
||||
src=$($DC exec -T lb-router timeout 6 tcpdump -ni be1 -c 1 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0' 2>/dev/null \
|
||||
| sed -n 's/.*IP \([0-9.]*\)\.[0-9]* > .*/\1/p' | head -1)
|
||||
[ "$src" = "10.20.0.253" ] && ok "проба к be1 уходит с $src" || bad "проба уходит с ${src:-неизвестно}, ожидался 10.20.0.253"
|
||||
|
||||
@@ -240,6 +255,74 @@ sleep 1
|
||||
&& ok "после apply.sh раскладка та же: $d_before" \
|
||||
|| bad "раскладка изменилась: $(digest), слотов $(snapshot | wc -l)"
|
||||
|
||||
head_ "15. Листенер в приватном сегменте (шаг 3)"
|
||||
# Клиент cli2 живёт в одном сегменте с be1 и be3 — тот самый случай, ради
|
||||
# которого сегмент переведён на порты OVS. В его ОС не настроено ничего, кроме
|
||||
# адреса: VIP находится в его собственной подсети.
|
||||
P2_VIP=10.20.0.100
|
||||
routes=$($DC exec -T cli2 ip route 2>/dev/null | grep -c 'via')
|
||||
[ "${routes:-1}" -eq 0 ] && ok "в ОС клиента нет ни одного маршрута — только адрес" \
|
||||
|| bad "у клиента появились маршруты: их быть не должно"
|
||||
|
||||
$DC exec -T cli2 ping -c1 -W2 "$P2_VIP" >/dev/null 2>&1 \
|
||||
&& ok "ping $P2_VIP из сегмента (ARP- и ICMP-респондеры OVS)" \
|
||||
|| bad "VIP приватного листенера недоступен"
|
||||
|
||||
declare -A phits=()
|
||||
pclient=""; pserved=""; pown=0
|
||||
for _ in $(seq 24); do
|
||||
line=$($DC exec -T cli2 curl -s --max-time 5 "http://$P2_VIP/" 2>/dev/null) || continue
|
||||
b=$(echo "$line" | sed -n 's/.*backend=\([a-z0-9]*\).*/\1/p')
|
||||
c=$(echo "$line" | sed -n 's/.*client=\([0-9.]*\):.*/\1/p')
|
||||
sv=$(echo "$line" | sed -n 's/.*served=\([0-9.]*:[0-9]*\).*/\1/p')
|
||||
[ -n "$b" ] && phits[$b]=$(( ${phits[$b]:-0} + 1 ))
|
||||
[ -n "$c" ] && pclient=$c
|
||||
case "$b" in be1|be3) pown=$((pown + 1)); pserved=$sv ;; esac
|
||||
done
|
||||
echo " распределение: $(for k in "${!phits[@]}"; do printf '%s=%s ' "$k" "${phits[$k]}"; done)"
|
||||
[ "${#phits[@]}" -ge 3 ] && ok "приватный листенер балансирует между ${#phits[@]} членами из 4" \
|
||||
|| bad "задействовано лишь ${#phits[@]} членов"
|
||||
|
||||
# Ключевая проверка шага: бэкенд видит исходный адрес клиента.
|
||||
[ "$pclient" = "10.20.0.10" ] && ok "бэкенд видит исходный IP клиента: $pclient (SNAT не выполняется)" \
|
||||
|| bad "бэкенд видит $pclient вместо 10.20.0.10"
|
||||
|
||||
# Тот самый случай: член пула в одном сегменте с клиентом. Ответ уходит по L2
|
||||
# напрямую, но проходит через br-lb, где таблица 24 снимает трансляцию.
|
||||
[ "$pown" -gt 0 ] && ok "члены из сегмента клиента обслужили $pown запросов" \
|
||||
|| bad "ни один запрос не попал на be1/be3 — коммутируемый путь не проверен"
|
||||
case "$pserved" in
|
||||
10.20.0.[23]:8080) ok "трансляция порта выполнена: листенер 80 -> член $pserved" ;;
|
||||
"") bad "не удалось определить адрес обслуживания" ;;
|
||||
*) bad "неожиданный адрес обслуживания: $pserved" ;;
|
||||
esac
|
||||
|
||||
# Путь остался коммутируемым: TTL ответа от VIP не уменьшен, значит клиент и
|
||||
# бэкенд по-прежнему L2-соседи и балансировщик не стал для них L3-хопом.
|
||||
ttls=$(mktemp)
|
||||
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -ni cli2 -c 4 -v \
|
||||
"tcp and src host $P2_VIP and tcp[tcpflags] & tcp-syn != 0" >"$ttls" 2>/dev/null &
|
||||
tpid=$!
|
||||
sleep 1
|
||||
for _ in $(seq 12); do $DC exec -T cli2 curl -s --max-time 3 -o /dev/null "http://$P2_VIP/" 2>/dev/null; done
|
||||
wait $tpid 2>/dev/null
|
||||
grep -q 'ttl 64' "$ttls" && ok "ответ от VIP приходит с TTL 64 — путь коммутируемый, не маршрутный" \
|
||||
|| bad "TTL ответа уменьшен: путь стал маршрутизируемым"
|
||||
rm -f "$ttls"
|
||||
|
||||
head_ "16. Внутрисегментный трафик не блокирован"
|
||||
out=$($DC exec -T cli2 curl -s --max-time 5 http://10.20.0.2:8080/ 2>/dev/null)
|
||||
echo "$out" | grep -q 'backend=be1' && ok "клиент обращается к бэкенду напрямую, минуя VIP" \
|
||||
|| bad "прямое обращение клиента к бэкенду не работает"
|
||||
out=$($DC exec -T be1 curl -s --max-time 5 http://10.20.0.3:8080/ 2>/dev/null)
|
||||
echo "$out" | grep -q 'backend=be3' && ok "be1 -> be3 внутри сегмента" \
|
||||
|| bad "связность между бэкендами сегмента нарушена"
|
||||
$DC exec -T be1 ping -c1 -W2 10.20.0.254 >/dev/null 2>&1 \
|
||||
&& ok "бэкенд достаёт шлюз Docker через аплинк сегмента" \
|
||||
|| bad "шлюз сегмента недоступен"
|
||||
$DC exec -T cli3 curl -s --max-time 5 http://10.30.0.3:8080/ 2>/dev/null | grep -q 'backend=be4' \
|
||||
&& ok "cli3 -> be4 во втором сегменте" || bad "связность второго сегмента нарушена"
|
||||
|
||||
echo
|
||||
echo "Итог: успешно $pass, провалено $fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in new issue
Block a user