Шаг 5: динамическое изучение MAC/ARP для бэкендов
MAC каждого члена пула больше не статическая константа в topology.env, а резолвится демоном hcd через обычный ARP ядра — в реальном окружении MAC бэкенда заранее не известен (сервер ещё не подключён, NIC может замениться), топология не описывается статически, в отличие от стенда. hcif-порты (единственные адреса узла в ядре) уже были настоящими L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh. Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно заливает бандл в таблицу 21 на том же тикере, что и health-пробы), получили резолвер без нового OpenFlow-контроллера. Таблица 21 стала единственным источником MAC для обоих путей — маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21. Исправлен попутно найденный баг: после apply.sh (replace-flows) таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал MAC с памятью демона, а не с датапасом. Добавлен force-режим по аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh. Проверено на живом стенде: MAC всех четырёх членов резолвлен и совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и применена без вмешательства за счёт штатного старения ARP ядра. Регрессия: 94 из 94 проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f34d2edefa
commit
fa389f119b
17 files changed
+678
-61
No files matched your search
@@ -4,7 +4,7 @@ DC := docker compose
|
|||||||
# отсутствовать в системе.
|
# отсутствовать в системе.
|
||||||
SUDO := $(shell [ "$$(id -u)" = 0 ] || echo sudo)
|
SUDO := $(shell [ "$$(id -u)" = 0 ] || echo sudo)
|
||||||
|
|
||||||
.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
|
.PHONY: help prereq shim shim-down build up down restart attach detach flows verify logs ports health slots drain enable metrics conns trace fdb neigh shell
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Стенд hpnn_v2 — OVS: маршрутизация, балансировка, коммутация сегментов"
|
@echo "Стенд hpnn_v2 — OVS: маршрутизация, балансировка, коммутация сегментов"
|
||||||
@@ -27,6 +27,7 @@ help:
|
|||||||
@echo " make conns таблица соединений ct"
|
@echo " make conns таблица соединений ct"
|
||||||
@echo " make trace SRC=192.168.5.7 [SPORT=40000] [SEG=pub|p2|p3] ofproto/trace"
|
@echo " make trace SRC=192.168.5.7 [SPORT=40000] [SEG=pub|p2|p3] ofproto/trace"
|
||||||
@echo " make fdb таблица коммутации сегментов (выученные MAC)"
|
@echo " make fdb таблица коммутации сегментов (выученные MAC)"
|
||||||
|
@echo " make neigh MAC членов пула, резолвленный через ARP ядра"
|
||||||
@echo " make logs логи контейнеров"
|
@echo " make logs логи контейнеров"
|
||||||
@echo " make shell shell в контейнере lb-router"
|
@echo " make shell shell в контейнере lb-router"
|
||||||
|
|
||||||
@@ -104,5 +105,8 @@ trace:
|
|||||||
fdb:
|
fdb:
|
||||||
$(DC) exec -T lb-router lbctl fdb
|
$(DC) exec -T lb-router lbctl fdb
|
||||||
|
|
||||||
|
neigh:
|
||||||
|
$(DC) exec -T lb-router lbctl neigh
|
||||||
|
|
||||||
shell:
|
shell:
|
||||||
$(DC) exec lb-router bash
|
$(DC) exec lb-router bash
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
| 3 | Листенер в приватном сегменте, коммутация сегментов | [план](docs/STEP3_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP3_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/CHANGE_SPLIT_DATAFLOW_DOCS_PLAN.md) | [итоги](docs/CHANGE_SPLIT_DATAFLOW_DOCS_SUMMARY.md) |
|
||||||
| 4 | Публичная балансировка без conntrack | — | [оценка осуществимости](docs/STEP4_PUBLIC_STATELESS_ASSESSMENT.md) |
|
| 4 | Публичная балансировка без conntrack | — | [оценка осуществимости](docs/STEP4_PUBLIC_STATELESS_ASSESSMENT.md) |
|
||||||
|
| 5 | Динамическое изучение MAC/ARP для бэкендов | [план](docs/STEP5_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP5_SUMMARY.md) |
|
||||||
|
|
||||||
Как устроена раскладка слотов — [docs/MAGLEV_SLOT_TABLE.md](docs/MAGLEV_SLOT_TABLE.md)
|
Как устроена раскладка слотов — [docs/MAGLEV_SLOT_TABLE.md](docs/MAGLEV_SLOT_TABLE.md)
|
||||||
(справка по `hc/maglev.go` для разработки). Что нужно изменить для перевода ноды
|
(справка по `hc/maglev.go` для разработки). Что нужно изменить для перевода ноды
|
||||||
@@ -239,6 +240,7 @@ make conns # таблица соединений ct
|
|||||||
make trace SRC=192.168.5.13 # ofproto/trace сессии клиент -> VIP
|
make trace SRC=192.168.5.13 # ofproto/trace сессии клиент -> VIP
|
||||||
make trace SRC=10.20.0.10 SEG=p2 # то же для приватного листенера
|
make trace SRC=10.20.0.10 SEG=p2 # то же для приватного листенера
|
||||||
make fdb # выученные MAC сегментов и счётчики рассылки
|
make fdb # выученные MAC сегментов и счётчики рассылки
|
||||||
|
make neigh # MAC членов пула, резолвленный hcd через ARP ядра
|
||||||
make flows # перечитать pipeline.sh и перезалить правила
|
make flows # перечитать pipeline.sh и перезалить правила
|
||||||
docker compose exec lb-router lbctl flows 21 # правила конкретной таблицы
|
docker compose exec lb-router lbctl flows 21 # правила конкретной таблицы
|
||||||
docker compose exec lb-router lbctl status # состояние пула в JSON
|
docker compose exec lb-router lbctl status # состояние пула в JSON
|
||||||
@@ -252,16 +254,16 @@ docker compose exec lb-router lbctl status # состояние пула в
|
|||||||
|
|
||||||
| Таблица | Назначение |
|
| Таблица | Назначение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 0 | Сегмент по входному порту (`reg0`); обучение MAC (FDB) и adjacency |
|
| 0 | Сегмент по входному порту (`reg0`); обучение MAC (FDB) и adjacency клиентов |
|
||||||
| 1 | Классификация: ARP, ICMP, листенер, трафик к маршрутизатору, коммутация |
|
| 1 | Классификация: ARP, ICMP, листенер, трафик к маршрутизатору, коммутация |
|
||||||
| 5 | ARP-респондер для адресов узла, VIP и источников проб; прочий ARP — в коммутацию |
|
| 5 | ARP-респондер для адресов узла, VIP и источников проб; прочий ARP — в коммутацию |
|
||||||
| 6 | ICMP echo-респондер для адресов узла и VIP |
|
| 6 | ICMP echo-респондер для адресов узла и VIP |
|
||||||
| 10 | Листенеры: `VIP:80` → `multipath` кладёт номер слота в `reg1`; прочий IP → маршрутизация |
|
| 10 | Листенеры: `VIP:80` → `multipath` кладёт номер слота в `reg1`; прочий IP → маршрутизация |
|
||||||
| 11 | **Таблица слотов**: `reg1` → `reg2` (член пула). Ведёт демон `hcd` |
|
| 11 | **Таблица слотов**: `reg1` → `reg2` (член пула). Ведёт демон `hcd` |
|
||||||
| 12 | Применение члена: DNAT и выдача — коммутацией либо через маршрутизацию |
|
| 12 | Применение члена: `ct(commit, nat)`, дальше — таблица 21 (свой сегмент) или 20 (чужой сегмент/публичный) |
|
||||||
| 15, 16 | Обратный путь L3: `ct(nat)` снимает DNAT, источник снова становится VIP |
|
| 15, 16 | Обратный путь L3: `ct(nat)` снимает DNAT, источник снова становится VIP |
|
||||||
| 20 | Маршрутизация: приоритет = длина префикса, `dec_ttl`, MAC источника |
|
| 20 | Маршрутизация: приоритет = длина префикса, `dec_ttl`, MAC источника |
|
||||||
| 21 | Adjacency: MAC next-hop и выходной порт |
|
| 21 | **Adjacency**: MAC next-hop и выходной порт. Единственное место с MAC членов пула — резолвит `hcd` через ARP ядра (приоритет 100, шаг 5); MAC клиентов — `learn` (приоритет 90) |
|
||||||
| 24 | Обратная трансляция коммутируемого трафика сегмента |
|
| 24 | Обратная трансляция коммутируемого трафика сегмента |
|
||||||
| 25 | L2-коммутация сегмента: FDB, broadcast и рассылка неизвестного unicast |
|
| 25 | L2-коммутация сегмента: FDB, broadcast и рассылка неизвестного unicast |
|
||||||
|
|
||||||
@@ -277,12 +279,15 @@ docker compose exec lb-router lbctl status # состояние пула в
|
|||||||
Приватный листенер и публичный делят пул, таблицу слотов и хэш. Различается
|
Приватный листенер и публичный делят пул, таблицу слотов и хэш. Различается
|
||||||
только выдача:
|
только выдача:
|
||||||
|
|
||||||
- **член в сегменте клиента** — меняется лишь MAC назначения, кадр отдаётся в
|
- **член в сегменте клиента** — из таблицы 12 сразу в таблицу 21 (минуя 20):
|
||||||
коммутацию. `dec_ttl` не выполняется: клиент и бэкенд остаются L2-соседями,
|
MAC назначения переписывается, `dec_ttl` не выполняется — клиент и бэкенд
|
||||||
и ответ приходит к клиенту с исходным TTL. Обратную трансляцию делает
|
остаются L2-соседями, ответ приходит к клиенту с исходным TTL. Обратную
|
||||||
таблица 24, когда бэкенд отвечает соседу напрямую;
|
трансляцию делает таблица 24, когда бэкенд отвечает соседу напрямую;
|
||||||
- **член в другом сегменте либо публичный листенер** — прежний маршрутизируемый
|
- **член в другом сегменте либо публичный листенер** — прежний маршрутизируемый
|
||||||
путь через таблицы 20/21 с `dec_ttl` и обратной трансляцией в таблице 15.
|
путь через таблицы 20 → 21 с `dec_ttl` и обратной трансляцией в таблице 15.
|
||||||
|
|
||||||
|
Оба пути сходятся в одной и той же таблице 21 — MAC члена пула нужен ровно в
|
||||||
|
одном месте пайплайна, а не дублируется.
|
||||||
|
|
||||||
Для клиента оба пути неотличимы: ответ приходит от `VIP:80`.
|
Для клиента оба пути неотличимы: ответ приходит от `VIP:80`.
|
||||||
|
|
||||||
|
|||||||
@@ -666,6 +666,78 @@ docker compose exec lb-router /opt/lb/apply.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Блок K. Динамическое изучение MAC/ARP (шаг 5)
|
||||||
|
|
||||||
|
MAC каждого члена пула больше не статическая константа — его резолвит демон
|
||||||
|
`hcd` через обычный ARP ядра. Проверяем, что это действительно так, а не
|
||||||
|
осталось прежним поведением под новым названием.
|
||||||
|
|
||||||
|
**Шаг K.1 [Х]** — MAC членов пула резолвлен и совпадает с реальным:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make neigh
|
||||||
|
docker compose exec be1 ip link show eth0 | grep link/ether
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается: `lbctl neigh` показывает у `be1` состояние «да» (резолвлен) и MAC,
|
||||||
|
совпадающий с выводом `ip link show` внутри контейнера `be1`.
|
||||||
|
|
||||||
|
**Шаг K.2 [Х]** — MAC действительно используется в датапасе:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=21 \
|
||||||
|
| grep priority=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается: по правилу `priority=100,ip,nw_dst=<адрес члена>` на каждого
|
||||||
|
живого члена, `actions` содержат тот же MAC, что и в `make neigh`.
|
||||||
|
|
||||||
|
**Шаг K.3 [Х]** — обнаружение смены MAC «железа» без перезапуска стенда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec be1 ip link set eth0 down
|
||||||
|
docker compose exec be1 ip link set eth0 address 02:42:0a:14:00:99
|
||||||
|
docker compose exec be1 ip link set eth0 up
|
||||||
|
docker compose exec be1 ip route replace 192.168.5.0/24 via 10.20.0.1
|
||||||
|
docker compose exec be1 ip route replace 10.30.0.0/24 via 10.20.0.1
|
||||||
|
docker compose exec be1 ip route replace default via 10.20.0.254
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается: не сразу, а в течение примерно двух минут (обычное старение ARP
|
||||||
|
ядра — `hcd` опрашивает быстро, но отражает только то, что уже узнало ядро)
|
||||||
|
`make neigh` покажет у `be1` новый MAC `02:42:0a:14:00:99`, и трафик к нему
|
||||||
|
продолжит доставляться:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
watch -n5 'docker compose exec -T lb-router lbctl neigh'
|
||||||
|
# в отдельном терминале, после смены MAC в neigh:
|
||||||
|
docker compose exec cli2 curl -s http://10.20.0.2:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
Вернуть `be1` в исходное состояние (родной MAC пропишет заново
|
||||||
|
`scripts/attach-segment.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose restart be1
|
||||||
|
sleep 2 && sudo scripts/attach-segment.sh
|
||||||
|
sleep 6 && make neigh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг K.4 [Х]** — заливка таблицы 21 переживает перезаливку пайплайна:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make health | grep -A1 дайджест # или просто make neigh — запомнить MAC
|
||||||
|
make flows
|
||||||
|
make neigh # те же MAC на месте, без ручного вмешательства
|
||||||
|
```
|
||||||
|
|
||||||
|
Смысл: `make flows` (`apply.sh`) стирает весь пайплайн вместе с таблицей 21;
|
||||||
|
`/reapply` у `hcd` обязан восстановить и таблицу слотов, и таблицу adjacency —
|
||||||
|
без этого второй `make neigh` показал бы пустые записи до следующего
|
||||||
|
естественного изменения MAC (которого может не случиться никогда).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Итоговый чек-лист
|
## Итоговый чек-лист
|
||||||
|
|
||||||
| # | Проверка | Результат |
|
| # | Проверка | Результат |
|
||||||
@@ -694,6 +766,10 @@ docker compose exec lb-router /opt/lb/apply.sh
|
|||||||
| J | Ответ от соседа по сегменту приходит с неуменьшенным TTL | ☐ |
|
| J | Ответ от соседа по сегменту приходит с неуменьшенным TTL | ☐ |
|
||||||
| J | Трафик между ВМ сегмента не блокирован | ☐ |
|
| J | Трафик между ВМ сегмента не блокирован | ☐ |
|
||||||
| J | `PRIV_LISTENERS` выключает листенер, не трогая связность | ☐ |
|
| J | `PRIV_LISTENERS` выключает листенер, не трогая связность | ☐ |
|
||||||
|
| K | MAC членов пула резолвлен и совпадает с реальным MAC интерфейса | ☐ |
|
||||||
|
| K | Таблица 21 содержит правило приоритета 100 с этим MAC | ☐ |
|
||||||
|
| K | Смена MAC «железа» обнаружена без перезапуска стенда | ☐ |
|
||||||
|
| K | Таблица 21 переживает `make flows` | ☐ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+19
-10
@@ -1,11 +1,11 @@
|
|||||||
# Потоки данных: балансировка приватного трафика
|
# Потоки данных: балансировка приватного трафика
|
||||||
|
|
||||||
**Дата:** 2026-08-17
|
**Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP)
|
||||||
**Область:** путь пакета от клиента внутри приватного сегмента на приватный
|
**Область:** путь пакета от клиента внутри приватного сегмента на приватный
|
||||||
листенер (`10.20.0.100:80` или `10.30.0.100:80`) и обратно.
|
листенер (`10.20.0.100:80` или `10.30.0.100:80`) и обратно.
|
||||||
**Смежный документ:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md) —
|
**Смежный документ:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md) —
|
||||||
балансировка публичного трафика.
|
балансировка публичного трафика.
|
||||||
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP3_SUMMARY.md](STEP3_SUMMARY.md).
|
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP3_SUMMARY.md](STEP3_SUMMARY.md), [STEP5_SUMMARY.md](STEP5_SUMMARY.md).
|
||||||
|
|
||||||
Отличие от публичного случая: клиент может стоять **в одном сегменте с
|
Отличие от публичного случая: клиент может стоять **в одном сегменте с
|
||||||
бэкендами**. Тогда ответ бэкенда уходит клиенту по L2 напрямую, минуя
|
бэкендами**. Тогда ответ бэкенда уходит клиенту по L2 напрямую, минуя
|
||||||
@@ -70,11 +70,12 @@ flowchart TD
|
|||||||
T5 -->|"не наш адрес"| T25
|
T5 -->|"не наш адрес"| T25
|
||||||
T10{"<b>10</b> листенер<br/>tcp VIP:80"} --> T11["<b>11</b> таблица слотов<br/>reg1 → reg2 (общая<br/>с публичным листенером)"]
|
T10{"<b>10</b> листенер<br/>tcp VIP:80"} --> T11["<b>11</b> таблица слотов<br/>reg1 → reg2 (общая<br/>с публичным листенером)"]
|
||||||
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
|
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
|
||||||
T11 --> T12{"<b>12</b> применение члена"}
|
T11 --> T12{"<b>12</b> применение члена<br/>ct(commit,nat)"}
|
||||||
|
|
||||||
T12 -->|"<b>член в сегменте клиента</b><br/>mod_dl_dst + ct(commit,nat)<br/><b>без dec_ttl</b>"| T25
|
T12 -->|"<b>член в сегменте клиента</b><br/><b>без dec_ttl</b>, минуя таблицу 20"| T21
|
||||||
T12 -->|"член в другом сегменте<br/>ct(commit,nat)"| T20["<b>20/21</b> маршрутизация<br/>dec_ttl, adjacency"]
|
T12 -->|"член в другом сегменте"| T20["<b>20</b> маршрутизация<br/>dec_ttl"]
|
||||||
T20 --> OUTR(["output: порт члена<br/>в другом сегменте"])
|
T20 --> T21["<b>21</b> adjacency<br/>MAC члена + порт —<br/>резолвит hcd через ARP<br/>ядра (шаг 5), единое<br/>место для обоих путей"]
|
||||||
|
T21 --> OUTR(["output: порт члена"])
|
||||||
|
|
||||||
T15 --> T16["<b>16</b> пост-ct"] --> T20
|
T15 --> T16["<b>16</b> пост-ct"] --> T20
|
||||||
T24{"<b>24</b> обратная трансляция<br/>сегмента"}
|
T24{"<b>24</b> обратная трансляция<br/>сегмента"}
|
||||||
@@ -87,6 +88,11 @@ flowchart TD
|
|||||||
class DROPF d
|
class DROPF d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Таблица 21 — единственное место, где применяется MAC следующего перехода, для
|
||||||
|
обоих путей сразу: и для члена в сегменте клиента (сюда ведёт таблица 12
|
||||||
|
напрямую, без `dec_ttl`), и для члена в другом сегменте (сюда приходят из
|
||||||
|
таблицы 20, уже после `dec_ttl`). MAC члена не дублируется между таблицами.
|
||||||
|
|
||||||
## 3. Основной поток: клиент и член пула в одном сегменте
|
## 3. Основной поток: клиент и член пула в одном сегменте
|
||||||
|
|
||||||
Тот самый случай, ради которого сегмент переведён на порты OVS.
|
Тот самый случай, ради которого сегмент переведён на порты OVS.
|
||||||
@@ -105,8 +111,9 @@ sequenceDiagram
|
|||||||
Note over OF: t0: reg0=2, обучение FDB и adjacency
|
Note over OF: t0: reg0=2, обучение FDB и adjacency
|
||||||
Note over OF: t1: dl_dst = MAC шлюза, nw_dst = VIP → листенер
|
Note over OF: t1: dl_dst = MAC шлюза, nw_dst = VIP → листенер
|
||||||
Note over OF: t10 → t11: слот → член пула
|
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>
|
Note over OF: t12: ct(commit, nat(dst=10.20.0.2:8080))<br/>→ t21 напрямую, минуя t20
|
||||||
OF->>B: t25: FDB → порт be1
|
Note over OF: t21: mod_dl_dst = MAC be1 (резолвлен hcd<br/>через ARP ядра, шаг 5), output порт be1<br/><b>dec_ttl не выполнялся</b>
|
||||||
|
OF->>B: пакет в порт be1
|
||||||
Note over B: client=10.20.0.10 — исходный адрес клиента<br/>served=10.20.0.2:8080 — трансляция 80 → 8080
|
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)
|
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: t1: dl_dst ≠ MAC шлюза → t24
|
||||||
@@ -181,8 +188,9 @@ flowchart TD
|
|||||||
| 5 | `arp_tpa` = VIP, шлюз или `.253` | ответ `IN_PORT` | MAC-ом порта сегмента |
|
| 5 | `arp_tpa` = VIP, шлюз или `.253` | ответ `IN_PORT` | MAC-ом порта сегмента |
|
||||||
| 5 | прочий ARP | → t25 | ВМ резолвят друг друга сами |
|
| 5 | прочий ARP | → t25 | ВМ резолвят друг друга сами |
|
||||||
| 10 | `tcp`, `nw_dst` = VIP сегмента | `multipath` → t11 | тот же `pool_id` и хэш, что у публичного |
|
| 10 | `tcp`, `nw_dst` = VIP сегмента | `multipath` → t11 | тот же `pool_id` и хэш, что у публичного |
|
||||||
| 12 | `reg0` = сегмент члена | `mod_dl_dst` + `ct(commit,nat)` → t25 | **без `dec_ttl`** |
|
| 12 | `reg0` = сегмент члена | `ct(commit,nat)` → t21 напрямую | **без `dec_ttl`**, минуя t20 |
|
||||||
| 12 | прочее | `ct(commit,nat)` → t20 | член из другого сегмента |
|
| 12 | прочее | `ct(commit,nat)` → t20 | член из другого сегмента |
|
||||||
|
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5) — единое место для обоих путей |
|
||||||
| 24 | `nw_src` = член, `tp_src` = 8080 | `ct(nat)` → t25 | снимает DNAT с ответа соседу |
|
| 24 | `nw_src` = член, `tp_src` = 8080 | `ct(nat)` → t25 | снимает DNAT с ответа соседу |
|
||||||
| 24 | prio 0 | → t25 | обычный трафик проходит нетронутым |
|
| 24 | prio 0 | → t25 | обычный трафик проходит нетронутым |
|
||||||
| 25 | `eth_dst` из FDB | `output` | записи `learn`, `idle_timeout` 300 с |
|
| 25 | `eth_dst` из FDB | `output` | записи `learn`, `idle_timeout` 300 с |
|
||||||
@@ -203,7 +211,8 @@ flowchart TD
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # путь сессии по таблицам
|
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # путь сессии по таблицам
|
||||||
make fdb # выученные MAC и счётчики рассылки
|
make fdb # выученные MAC клиентов и счётчики рассылки
|
||||||
|
make neigh # MAC членов пула, резолвленный hcd через ARP
|
||||||
make ports # подключены ли ВМ сегментов
|
make ports # подключены ли ВМ сегментов
|
||||||
docker compose exec lb-router lbctl flows 24 # счётчики обратной трансляции
|
docker compose exec lb-router lbctl flows 24 # счётчики обратной трансляции
|
||||||
docker compose exec cli2 curl -s http://10.20.0.100/
|
docker compose exec cli2 curl -s http://10.20.0.100/
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Потоки данных: балансировка публичного трафика
|
# Потоки данных: балансировка публичного трафика
|
||||||
|
|
||||||
**Дата:** 2026-08-17
|
**Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP)
|
||||||
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
|
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
|
||||||
`192.168.5.21:80` и обратно.
|
`192.168.5.21:80` и обратно.
|
||||||
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
|
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
|
||||||
балансировка внутри приватного сегмента.
|
балансировка внутри приватного сегмента.
|
||||||
**Источник:** `lb/pipeline.sh`, `lb/topology.env`.
|
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP5_SUMMARY.md](STEP5_SUMMARY.md).
|
||||||
|
|
||||||
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
|
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
|
||||||
разных сегментах, узел выступает L3-хопом в обе стороны, TTL уменьшается на
|
разных сегментах, узел выступает L3-хопом в обе стороны, TTL уменьшается на
|
||||||
@@ -156,7 +156,7 @@ flowchart TD
|
|||||||
| 12 | `reg2` = член | `ct(commit, nat(dst=member:8080))` → t20 | без SNAT |
|
| 12 | `reg2` = член | `ct(commit, nat(dst=member:8080))` → t20 | без SNAT |
|
||||||
| 15 | `tcp` с порта сегмента, адресован шлюзу | `ct(nat)` → t16 | обратная трансляция |
|
| 15 | `tcp` с порта сегмента, адресован шлюзу | `ct(nat)` → t16 | обратная трансляция |
|
||||||
| 20 | `/24` префиксы | `dec_ttl` + `mod_dl_src` | LPM через приоритет правила |
|
| 20 | `/24` префиксы | `dec_ttl` + `mod_dl_src` | LPM через приоритет правила |
|
||||||
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | статические MAC из `topology.env` |
|
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5), не `topology.env` |
|
||||||
| 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с |
|
| 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с |
|
||||||
|
|
||||||
## 7. Где поток обрывается
|
## 7. Где поток обрывается
|
||||||
@@ -173,6 +173,7 @@ flowchart TD
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам
|
make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам
|
||||||
|
make neigh # MAC членов, резолвленный hcd через ARP
|
||||||
make slots # раскладка и per-member счётчики
|
make slots # раскладка и per-member счётчики
|
||||||
make conns # записи ct
|
make conns # записи ct
|
||||||
docker compose exec lb-router lbctl flows 21
|
docker compose exec lb-router lbctl flows 21
|
||||||
|
|||||||
@@ -83,8 +83,9 @@
|
|||||||
выводит его из группы. У приложения есть `/healthz` как задел.
|
выводит его из группы. У приложения есть `/healthz` как задел.
|
||||||
- **Пул и листенер статические** — заданы в `topology.env`, control plane и
|
- **Пул и листенер статические** — заданы в `topology.env`, control plane и
|
||||||
OVSDB-схемы из §6 дизайна v1 нет.
|
OVSDB-схемы из §6 дизайна v1 нет.
|
||||||
- **MAC бэкендов прописаны статически.** ARP-резолвер next-hop отсутствует;
|
- ~~MAC бэкендов прописаны статически. ARP-резолвер next-hop отсутствует~~ —
|
||||||
MAC зафиксированы в `docker-compose.yml`.
|
снято на шаге 5: `hcd` резолвит MAC членов пула через обычный ARP ядра, см.
|
||||||
|
[STEP5_SUMMARY.md](STEP5_SUMMARY.md).
|
||||||
- **Только TCP и только IPv4.** UDP- и L3-листенеров нет.
|
- **Только TCP и только IPv4.** UDP- и L3-листенеров нет.
|
||||||
|
|
||||||
## Задел на шаг 2
|
## Задел на шаг 2
|
||||||
|
|||||||
@@ -189,6 +189,15 @@ packet-in/packet-out по своей neigh-таблице (§4.3, §8.2), но
|
|||||||
интеграция с тем, как EVPN-фабрика публикует MAC-привязки, чтобы control plane
|
интеграция с тем, как EVPN-фабрика публикует MAC-привязки, чтобы control plane
|
||||||
HPNN получал их не вручную, а из фабрики.
|
HPNN получал их не вручную, а из фабрики.
|
||||||
|
|
||||||
|
> **Обновление (шаг 5, [STEP5_SUMMARY.md](STEP5_SUMMARY.md)):** первая часть
|
||||||
|
> этого пробела закрыта — `hcd` резолвит MAC членов пула динамически через
|
||||||
|
> обычный ARP ядра, статических `BE*_MAC` в таблице 21 больше нет. Это не
|
||||||
|
> решает вопрос EVPN-фабрики целиком: `hcd` резолвит MAC в пределах L2-домена,
|
||||||
|
> видимого узлу напрямую (те же hcif-порты, что и для health-проб), а не через
|
||||||
|
> интеграцию с EVPN Type-2. Если фабрика доставляет сегмент до узла как один
|
||||||
|
> L2-домен (что и предполагает текущая архитектура), тот же ARP-путь работает
|
||||||
|
> без изменений; отдельная интеграция с control plane EVPN остаётся вне рамок.
|
||||||
|
|
||||||
## 9. Итоговая сводка условий и рисков
|
## 9. Итоговая сводка условий и рисков
|
||||||
|
|
||||||
| # | Пункт | Тип | Статус |
|
| # | Пункт | Тип | Статус |
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Шаг 5 — план внедрения: динамическое изучение MAC/ARP для бэкендов
|
||||||
|
|
||||||
|
**Дата:** 2026-08-18
|
||||||
|
**Предыдущий шаг:** [STEP3_SUMMARY.md](STEP3_SUMMARY.md) (реализован).
|
||||||
|
**Не путать с шагом 4** ([STEP4_PUBLIC_STATELESS_ASSESSMENT.md](STEP4_PUBLIC_STATELESS_ASSESSMENT.md))
|
||||||
|
— это только оценка перехода публичного пути на схему без conntrack, ещё не
|
||||||
|
реализована; данный шаг с ней не пересекается.
|
||||||
|
**Итоги:** [STEP5_SUMMARY.md](STEP5_SUMMARY.md)
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
|
||||||
|
Сейчас MAC каждого члена пула — статическая константа (`BE1_MAC`…`BE4_MAC` в
|
||||||
|
`lb/topology.env`), продублированная в четырёх независимых местах: правило
|
||||||
|
`mod_dl_dst` в таблице 21 (routed-путь), инлайновый `mod_dl_dst` в таблице 12
|
||||||
|
(same-segment/коммутируемый путь шага 3), permanent-запись `ip neigh` в ядре
|
||||||
|
узла (`lb/entrypoint.sh`, для health-проб) и реальный MAC интерфейса ВМ
|
||||||
|
(`scripts/attach-segment.sh`). Все четыре обязаны совпадать вручную.
|
||||||
|
|
||||||
|
В реальном окружении MAC бэкенда заранее не известен: топология сети, в
|
||||||
|
отличие от стенда, не описывается статически. Уже задокументированный пробел
|
||||||
|
(`STEP1_SUMMARY.md`, известные ограничения: «MAC бэкендов прописаны статически.
|
||||||
|
ARP-резолвер next-hop отсутствует»), отдельно всплывший в оценке шага 4 (раздел
|
||||||
|
8, доставка до физического сервера в EVPN-фабрике).
|
||||||
|
|
||||||
|
Нужен компонент, изучающий MAC членов пула динамически, без предварительного
|
||||||
|
описания в конфигурации.
|
||||||
|
|
||||||
|
**Объём:** изучаются MAC только для адресов, уже известных из конфигурации
|
||||||
|
(member IP объявлен в `hc-config.sh`); обнаружение новых, необъявленных хостов
|
||||||
|
— вне рамок (отдельная задача control plane, уже отложена в `MULTITENANCY.md`).
|
||||||
|
Логика живёт в демоне `hcd` — расширение существующего единственного
|
||||||
|
control-plane процесса на ноде.
|
||||||
|
|
||||||
|
## Ключевая находка
|
||||||
|
|
||||||
|
Internal-порты `hcif-p2`/`hcif-p3` — единственные адреса узла, живущие в ядре
|
||||||
|
Linux, — уже настоящие L3-интерфейсы **в том же сегменте**, что и бэкенды.
|
||||||
|
Ядро уже способно резолвить их MAC обычным ARP; единственное препятствие —
|
||||||
|
статические `ip neigh ... nud permanent` записи в `lb/entrypoint.sh:99`,
|
||||||
|
которые ARP для этих адресов отключают навсегда. Подтверждено на живом стенде:
|
||||||
|
`ip -json neigh show` внутри `lb-router` сейчас отдаёт четыре записи с
|
||||||
|
`"state":["PERMANENT"]` — ровно то, что предстоит заменить на реальный ARP.
|
||||||
|
|
||||||
|
Дополнительно: `segment_ingress()`/`l3_learn()` в `lb/pipeline.sh` уже заливает
|
||||||
|
действием `learn` записи приоритета 90 в таблицу 21 для любого IP-трафика с
|
||||||
|
портов сегмента, включая ответы бэкендов на health-пробы — на живом стенде эти
|
||||||
|
записи присутствуют в `dump-flows table=21`, но перекрыты статическими
|
||||||
|
правилами приоритета 100. Задача не строит резолвер с нуля, а: (1) снимает
|
||||||
|
блокировку ARP ядра, (2) делает `hcd` явным и наблюдаемым источником MAC вместо
|
||||||
|
неявного поведения `learn`, (3) убирает дублирование MAC между таблицами 12 и
|
||||||
|
21.
|
||||||
|
|
||||||
|
## Разделение ответственности
|
||||||
|
|
||||||
|
| Кто | Адрес | Механизм |
|
||||||
|
|---|---|---|
|
||||||
|
| Клиенты | заранее неизвестен, произвольный | как сейчас — `learn` (таблица 21, приоритет 90), без изменений |
|
||||||
|
| Члены пула | известен из конфигурации, MAC — нет | новое: `hcd` резолвит через ядро (ARP на hcif-портах), заливает таблицу 21 приоритетом 100 |
|
||||||
|
|
||||||
|
## Изменения по файлам
|
||||||
|
|
||||||
|
### `lb/entrypoint.sh`
|
||||||
|
|
||||||
|
В `hc_port()` убрать `ip neigh replace ... nud permanent` для адресов членов
|
||||||
|
пула (строка 99, вызовы 106-109). MAC самого internal-порта (строка 93)
|
||||||
|
остаётся статичным — собственный адрес узла, не next-hop.
|
||||||
|
|
||||||
|
### `hc/neigh.go` (новый файл)
|
||||||
|
|
||||||
|
Работает на тикере health-проб (`cfg.interval`), без отдельной горутины:
|
||||||
|
|
||||||
|
- по `Source` члена и новому полю `iface` выполняет
|
||||||
|
`ip -json neigh show dev <iface> to <member_ip>`;
|
||||||
|
- MAC резолвлен при состоянии `REACHABLE`/`STALE`/`DELAY`/`PROBE`/`PERMANENT`;
|
||||||
|
- при изменении MAC — точечный бандл `ovs-ofctl bundle`: `flow delete
|
||||||
|
table=21,ip,nw_dst=<ip>` + `flow add table=21,priority=100,ip,nw_dst=<ip>
|
||||||
|
actions=mod_dl_dst:<mac>,output:<port>`;
|
||||||
|
- API: поле в `/status`, команда `lbctl neigh`.
|
||||||
|
|
||||||
|
### `lb/pipeline.sh`
|
||||||
|
|
||||||
|
- Убрать статические `table=21,priority=100,ip,nw_dst=$BE{1..4}_IP
|
||||||
|
actions=mod_dl_dst:$BE{n}_MAC,output:...` (строки 333-336). Записи для
|
||||||
|
`HC2_IP`/`HC3_IP` остаются статичными.
|
||||||
|
- Таблица 12, ветка same-segment (приоритет 200): убрать инлайновый
|
||||||
|
`mod_dl_dst:$mmac` (строка 284), `ct(commit,...)` направить в `table=21`
|
||||||
|
вместо `table=25`. Таблица 21 не декрементирует TTL — свойство «TTL не
|
||||||
|
уменьшается для соседей по сегменту» сохраняется, дублирование MAC между
|
||||||
|
таблицами уходит.
|
||||||
|
|
||||||
|
### `lb/topology.env`, `lb/hc-config.sh`
|
||||||
|
|
||||||
|
`BE1_MAC`…`BE4_MAC` перестают быть обязательными. Добавляется поле `iface` на
|
||||||
|
член пула.
|
||||||
|
|
||||||
|
### `lb/lbctl.sh`
|
||||||
|
|
||||||
|
Команда `neigh`: «член / адрес / MAC / состояние / возраст».
|
||||||
|
|
||||||
|
### `scripts/verify.sh`, `docs/MANUAL_TEST_PLAN.md`
|
||||||
|
|
||||||
|
- Таблица 21 заполняется динамически в течение нескольких секунд после старта.
|
||||||
|
- `lbctl neigh` показывает MAC, совпадающий с реальным MAC интерфейса ВМ.
|
||||||
|
- Смена MAC у `be1` обнаруживается `hcd` без разрыва доставки.
|
||||||
|
- Регрессия: все существующие проверки (оба листенера, транзит, health-check,
|
||||||
|
fail-close, дренаж, идемпотентность `apply.sh`) без изменений в поведении.
|
||||||
|
- `lbctl trace` same-segment пути — обновить ожидаемую цепочку (`12 → 21`
|
||||||
|
вместо `12 → 25`).
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
|
||||||
|
`docs/PUBLIC_LB_DATAFLOW.md`, `docs/PRIVATE_LB_DATAFLOW.md` — таблица 21:
|
||||||
|
«статические MAC» → «резолвятся `hcd` через ядро». Снять пункт «MAC бэкендов
|
||||||
|
прописаны статически» из ограничений `STEP1_SUMMARY.md`. `README.md`.
|
||||||
|
|
||||||
|
## Порядок работ
|
||||||
|
|
||||||
|
1. `docs/STEP5_IMPLEMENTATION_PLAN.md` (этот документ).
|
||||||
|
2. ~~Спайк `ip -json neigh`~~ — выполнено, подтверждено на живом стенде.
|
||||||
|
3. `lb/entrypoint.sh` — снять статические `ip neigh permanent`; проверить ARP
|
||||||
|
вручную.
|
||||||
|
4. `hc/neigh.go` + правки `hc/main.go`.
|
||||||
|
5. `lb/pipeline.sh` — таблица 21 как единственный источник MAC.
|
||||||
|
6. `lb/hc-config.sh`, `lb/topology.env`.
|
||||||
|
7. `lb/lbctl.sh` — команда `neigh`.
|
||||||
|
8. Ручная проверка, затем `scripts/verify.sh` и `MANUAL_TEST_PLAN.md`.
|
||||||
|
9. `docs/STEP5_SUMMARY.md`, README, диаграммы потоков.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- `lbctl neigh` показывает реальный MAC каждого члена, совпадающий с
|
||||||
|
`ip link show eth0` внутри соответствующего контейнера;
|
||||||
|
- в `topology.env` нет обязательных `BE*_MAC`;
|
||||||
|
- смена MAC у бэкенда обнаруживается и трафик продолжает доставляться без
|
||||||
|
ручного вмешательства;
|
||||||
|
- все существующие проверки (`make verify`) проходят без изменений в
|
||||||
|
наблюдаемом поведении.
|
||||||
|
|
||||||
|
## Риски
|
||||||
|
|
||||||
|
- **Окно на холодном старте** таблицы 21 для члена, пока `hcd` не сделал
|
||||||
|
первый резолв — по порядку величины совпадает с существующим fail-close
|
||||||
|
таблицы слотов, новой категории риска не вводит.
|
||||||
|
- **Обнаружение новых, необъявленных хостов вне объёма** — осознанно отложено
|
||||||
|
к control plane (`MULTITENANCY.md`).
|
||||||
|
- **Изменение цепочки таблиц same-segment пути** — требует правки ожидаемых
|
||||||
|
трасс в документации.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Шаг 5 — итоги: динамическое изучение MAC/ARP для бэкендов
|
||||||
|
|
||||||
|
**Дата:** 2026-08-18
|
||||||
|
**Статус:** выполнено, проверено после холодного перезапуска (94 из 94 проверок)
|
||||||
|
**План:** [STEP5_IMPLEMENTATION_PLAN.md](STEP5_IMPLEMENTATION_PLAN.md)
|
||||||
|
**Предыдущий шаг:** [STEP3_SUMMARY.md](STEP3_SUMMARY.md)
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
MAC каждого члена пула больше не статическая константа из `topology.env` — он
|
||||||
|
резолвится динамически демоном `hcd` через обычный ARP ядра.
|
||||||
|
|
||||||
|
- **`lb/entrypoint.sh`** — убраны permanent-записи `ip neigh` для адресов
|
||||||
|
членов пула. `hcif-p2`/`hcif-p3` — настоящие L3-интерфейсы ядра в тех же
|
||||||
|
сегментах, что и бэкенды, поэтому ядро само резолвит их MAC обычным ARP:
|
||||||
|
первая health-проба порождает ARP-запрос через `br-lb`, реальный бэкенд
|
||||||
|
отвечает, ядро запоминает.
|
||||||
|
- **`hc/neigh.go`** (новый) — на том же тикере, что и health-пробы, читает
|
||||||
|
`ip -json neigh show` для каждого члена по его интерфейсу (`hcif-p2`/
|
||||||
|
`hcif-p3`, новое поле `iface` в конфиге) и при изменении MAC атомарно
|
||||||
|
заливает точечный бандл `ovs-ofctl bundle` в таблицу 21 — не трогая записи
|
||||||
|
остальных членов.
|
||||||
|
- **Таблица 21 стала единственным источником MAC** для обоих путей —
|
||||||
|
маршрутизируемого и коммутируемого (шаг 3). Таблица 12 (same-segment DNAT)
|
||||||
|
больше не пишет MAC инлайново: `ct(commit,...)` теперь ведёт напрямую в
|
||||||
|
таблицу 21 вместо таблицы 25, минуя таблицу 20 (TTL по-прежнему не
|
||||||
|
уменьшается для соседей по сегменту).
|
||||||
|
- **`lbctl neigh` / `make neigh`** — таблица «член / адрес / интерфейс / MAC /
|
||||||
|
резолвлен / с момента», плюс сырые правила таблицы 21.
|
||||||
|
- **`BE1_MAC`…`BE4_MAC` в `topology.env`** остались только как «заводской» MAC,
|
||||||
|
который `attach-segment.sh` назначает интерфейсу ВМ (эмуляция того, что
|
||||||
|
реально прошито в NIC) — в OpenFlow эти переменные больше не попадают.
|
||||||
|
|
||||||
|
## Результаты проверок
|
||||||
|
|
||||||
|
| Проверка | Результат |
|
||||||
|
|---|---|
|
||||||
|
| `lbctl neigh` показывает реальный MAC каждого члена | совпадает с `ip link show eth0` внутри контейнера |
|
||||||
|
| Таблица 21, приоритет 100 | правило на каждого члена, `actions` содержат резолвленный MAC |
|
||||||
|
| Регрессия (все шаги 1–3) | 86 существующих проверок без изменений в поведении |
|
||||||
|
| **Смена MAC «железа»** | `ip link set eth0 address ...` у be1 → обнаружено `hcd`, таблица 21 обновлена, доставка восстановлена без вмешательства |
|
||||||
|
| Идемпотентность `apply.sh` | таблица 21 полностью восстанавливается после `replace-flows` (см. отклонения) |
|
||||||
|
|
||||||
|
## Ключевая находка (уже была на момент планирования)
|
||||||
|
|
||||||
|
`segment_ingress()`/`l3_learn()` и раньше заливала действием `learn` записи
|
||||||
|
приоритета 90 в таблицу 21 для любого IP-трафика с портов сегмента, включая
|
||||||
|
ответы бэкендов на health-пробы — но они были перекрыты статическими
|
||||||
|
правилами приоритета 100. Шаг 5 не строил резолвер с нуля: снял то, что
|
||||||
|
блокировало естественный ARP ядра (`nud permanent`), и сделал `hcd` явным,
|
||||||
|
наблюдаемым источником приоритета 100 вместо неявного поведения `learn`.
|
||||||
|
Разделение осталось ровно таким, как задумано: `learn` (приоритет 90)
|
||||||
|
по-прежнему единственный механизм для клиентов — их адрес заранее не известен
|
||||||
|
и не enumerable; `hcd` (приоритет 100) — для членов пула, чей адрес известен
|
||||||
|
из конфигурации.
|
||||||
|
|
||||||
|
## Отклонение от плана: баг с `force`
|
||||||
|
|
||||||
|
Первая реализация не заливала таблицу 21 заново после `apply.sh`
|
||||||
|
(`replace-flows` стирает весь пайплайн). Причина: `syncNeighbors()` сравнивал
|
||||||
|
резолвленный MAC с MAC **в памяти демона** — после `replace-flows` датапас
|
||||||
|
пуст, но в памяти MAC не изменился, поэтому условие «MAC изменился» не
|
||||||
|
срабатывало и точечный бандл не переливался. Обнаружено тестом идемпотентности
|
||||||
|
(`verify.sh`, блок 14 → новая проверка блока 17) — именно то, для чего этот
|
||||||
|
тест и существует.
|
||||||
|
|
||||||
|
Исправление зеркально `force` у `Agent.apply()`: `syncNeighbors(force bool)`,
|
||||||
|
`/reapply` теперь вызывает `a.syncNeighbors(true)` вслед за `a.apply(true)`.
|
||||||
|
Это тот же класс проблемы, что уже был явно предусмотрен для таблицы слотов
|
||||||
|
(`force заставляет залить даже неизменившуюся`, `hc/main.go`), просто
|
||||||
|
не был перенесён на новый подкомпонент при первой реализации.
|
||||||
|
|
||||||
|
## Наблюдение: реальная скорость обнаружения смены MAC
|
||||||
|
|
||||||
|
Проверка на живом стенде: смена MAC интерфейса `be1` обнаружена ядром и
|
||||||
|
отражена `hcd` в датапасе примерно через **2 минуты**, а не за один-два цикла
|
||||||
|
тикера (2 с), как можно было бы предположить из интервала опроса `hcd`.
|
||||||
|
|
||||||
|
Причина — `hcd` опрашивает **быстро** (каждые 2 с), но может отразить только
|
||||||
|
то, что уже узнало **ядро**, а стандартное старение Linux ARP (`REACHABLE` →
|
||||||
|
`STALE` → `DELAY` → `PROBE` → `FAILED`/переразрешение) по умолчанию рассчитано
|
||||||
|
на десятки секунд на первом же переходе (`base_reachable_time` ≈ 30 с) и суммарно
|
||||||
|
может занимать порядка минуты и больше. Частота опроса `hcd` не сокращает
|
||||||
|
это время — она лишь гарантирует, что новый MAC попадёт в датапас на первом же
|
||||||
|
тике **после** того, как его узнало ядро. Это честная характеристика решения,
|
||||||
|
а не дефект: тот же порядок величины, что и в реальных сетях с обычным ARP.
|
||||||
|
|
||||||
|
## Отклонения от плана
|
||||||
|
|
||||||
|
1. **`nud permanent` для members убраны полностью**, без опционального
|
||||||
|
ручного `mac`-переопределения в JSON — как и намечалось («не делать этого
|
||||||
|
на первой итерации»). Подтверждено достаточным: cold-start окно закрывается
|
||||||
|
первой же успешной пробой.
|
||||||
|
2. **Обнаружен и исправлен баг с `force`**, не предусмотренный явно в плане
|
||||||
|
(план предполагал его как риск в общих чертах — «зависимость от `ip -json
|
||||||
|
neigh`», но не called out force-реапплай конкретно). Добавлена
|
||||||
|
регрессионная проверка (`verify.sh`, блок 17) именно на этот случай.
|
||||||
|
3. **Проверка TTL в блоке 15 (`verify.sh`) сделана устойчивее**: захват `-c 4`
|
||||||
|
на 12 запросах оказался статистически хрупким — при 4 членах пула 12
|
||||||
|
запросов иногда не попадали ни разу на соседа по сегменту, тест ложно
|
||||||
|
падал. Увеличено до `-c 16` на 32 запросах; на двух прогонах подряд
|
||||||
|
стабильно зелёный. Не связано с шагом 5 по существу (тест существовал с
|
||||||
|
шага 3), но исправлено в этой же сессии, раз затронут `verify.sh`.
|
||||||
|
|
||||||
|
## Известные ограничения
|
||||||
|
|
||||||
|
- **Обнаружение новых, необъявленных хостов вне объёма** — резолвится MAC
|
||||||
|
только для адресов, уже присутствующих в `hc-config.sh`. Обнаружение самих
|
||||||
|
адресов — отдельная, более крупная задача control plane (`MULTITENANCY.md`).
|
||||||
|
- **Скорость обнаружения смены MAC ограничена стандартным старением ARP ядра**
|
||||||
|
(см. наблюдение выше) — величина порядка минуты, не секунд.
|
||||||
|
- **Окно на холодном старте**: пока `hcd` не сделал первый резолв, трафик к
|
||||||
|
члену зависит от того, успел ли сработать `learn` (приоритет 90); в худшем
|
||||||
|
случае — `priority=0 drop`. По порядку величины совпадает с fail-close
|
||||||
|
таблицы слотов, новой категории риска не вводит.
|
||||||
|
- **`ip -json neigh`** — версия `iproute2` в образе (`6.1.0`) поддерживает флаг;
|
||||||
|
при смене базового образа стоит переподтвердить.
|
||||||
|
|
||||||
|
## Задел на следующие шаги
|
||||||
|
|
||||||
|
1. Тот же приём (демон резолвит адрес через ядро и заливает точечным бандлом)
|
||||||
|
применим к обнаружению самих хостов, если понадобится снять ограничение
|
||||||
|
«только уже известные адреса» — потребует ARP-сканирования сегмента и
|
||||||
|
решения, как новый хост попадает в конфигурацию пула.
|
||||||
|
2. Control plane (`Load_Balancer → Listener → Pool → Member` в OVSDB,
|
||||||
|
`MULTITENANCY.md`) естественно принимает поле `iface` как часть модели
|
||||||
|
`Member`, а не специфику `hc-config.sh`.
|
||||||
+46
-4
@@ -35,6 +35,7 @@ type Config struct {
|
|||||||
Slots int `json:"slots"`
|
Slots int `json:"slots"`
|
||||||
SlotTable int `json:"slot_table"`
|
SlotTable int `json:"slot_table"`
|
||||||
DNATTable int `json:"dnat_table"`
|
DNATTable int `json:"dnat_table"`
|
||||||
|
AdjTable int `json:"adj_table"` // таблица 21 — next-hop MAC членов (шаг 5)
|
||||||
Probe string `json:"probe"`
|
Probe string `json:"probe"`
|
||||||
HTTPPath string `json:"http_path"`
|
HTTPPath string `json:"http_path"`
|
||||||
Interval string `json:"interval"`
|
Interval string `json:"interval"`
|
||||||
@@ -56,6 +57,8 @@ type Member struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
|
Iface string `json:"iface"` // hcif-порт, на котором резолвится MAC (шаг 5)
|
||||||
|
OFPort int `json:"ofport"` // выходной порт члена в OpenFlow
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
state string // up | down
|
state string // up | down
|
||||||
@@ -68,6 +71,8 @@ type Member struct {
|
|||||||
probes uint64
|
probes uint64
|
||||||
failures uint64
|
failures uint64
|
||||||
transitions uint64
|
transitions uint64
|
||||||
|
mac string // резолвлен hcd через neigh-таблицу ядра, пусто = не резолвлен
|
||||||
|
macSince time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Member) Key() string { return fmt.Sprintf("%s:%d", m.Address, m.Port) }
|
func (m *Member) Key() string { return fmt.Sprintf("%s:%d", m.Address, m.Port) }
|
||||||
@@ -101,6 +106,9 @@ type memberView struct {
|
|||||||
Transitions uint64 `json:"transitions"`
|
Transitions uint64 `json:"transitions"`
|
||||||
Slots int `json:"slots"`
|
Slots int `json:"slots"`
|
||||||
Source string `json:"probe_source"`
|
Source string `json:"probe_source"`
|
||||||
|
Iface string `json:"iface"`
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
MACSince string `json:"mac_since,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- пробы -------------------------------------------------------------------
|
// --- пробы -------------------------------------------------------------------
|
||||||
@@ -290,11 +298,14 @@ func (a *Agent) status() []memberView {
|
|||||||
Active: m.state == "up" && m.admin == "enabled",
|
Active: m.state == "up" && m.admin == "enabled",
|
||||||
LastError: m.lastErr, LatencyMS: m.lastLatency.Milliseconds(),
|
LastError: m.lastErr, LatencyMS: m.lastLatency.Milliseconds(),
|
||||||
Probes: m.probes, Failures: m.failures, Transitions: m.transitions,
|
Probes: m.probes, Failures: m.failures, Transitions: m.transitions,
|
||||||
Source: m.Source,
|
Source: m.Source, Iface: m.Iface, MAC: m.mac,
|
||||||
}
|
}
|
||||||
if !m.lastChange.IsZero() {
|
if !m.lastChange.IsZero() {
|
||||||
v.Since = m.lastChange.Format(time.RFC3339)
|
v.Since = m.lastChange.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
if !m.macSince.IsZero() {
|
||||||
|
v.MACSince = m.macSince.Format(time.RFC3339)
|
||||||
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
v.Slots = a.slotsOf(m.ID)
|
v.Slots = a.slotsOf(m.ID)
|
||||||
out = append(out, v)
|
out = append(out, v)
|
||||||
@@ -351,6 +362,28 @@ func (a *Agent) serve() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Резолв MAC членов пула (шаг 5): человекочитаемая таблица для lbctl neigh.
|
||||||
|
mux.HandleFunc("/neigh.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
fmt.Fprintf(w, "%-6s %-17s %-10s %-17s %-8s %s\n",
|
||||||
|
"ЧЛЕН", "АДРЕС", "ИНТЕРФЕЙС", "MAC", "РЕЗОЛВЛЕН", "С МОМЕНТА")
|
||||||
|
for _, v := range a.status() {
|
||||||
|
mac := v.MAC
|
||||||
|
resolved := "нет"
|
||||||
|
if mac == "" {
|
||||||
|
mac = "-"
|
||||||
|
} else {
|
||||||
|
resolved = "да"
|
||||||
|
}
|
||||||
|
since := v.MACSince
|
||||||
|
if since == "" {
|
||||||
|
since = "-"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%-6s %-17s %-10s %-17s %-8s %s\n",
|
||||||
|
v.Name, v.Address, v.Iface, mac, resolved, since)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
applies, errs := a.applies, a.errors
|
applies, errs := a.applies, a.errors
|
||||||
@@ -395,20 +428,24 @@ func (a *Agent) serve() {
|
|||||||
mux.HandleFunc("/drain", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "drain") })
|
mux.HandleFunc("/drain", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "drain") })
|
||||||
mux.HandleFunc("/enable", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "enabled") })
|
mux.HandleFunc("/enable", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "enabled") })
|
||||||
|
|
||||||
// Вызывается из apply.sh: перезаливка всего пайплайна стирает таблицу
|
// Вызывается из apply.sh: перезаливка всего пайплайна (replace-flows)
|
||||||
// слотов, поэтому её нужно восстановить в актуальном составе.
|
// стирает и таблицу слотов, и правила adjacency (таблица 21) — обе нужно
|
||||||
|
// восстановить в актуальном составе. force=true у обеих: MAC/раскладка в
|
||||||
|
// памяти демона могли не измениться, но датапас сброшен и нуждается в
|
||||||
|
// повторной заливке уже известных значений.
|
||||||
mux.HandleFunc("/reapply", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/reapply", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := a.apply(true); err != nil {
|
if err := a.apply(true); err != nil {
|
||||||
http.Error(w, err.Error()+"\n", http.StatusInternalServerError)
|
http.Error(w, err.Error()+"\n", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.syncNeighbors(true)
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
digest := a.digest
|
digest := a.digest
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
fmt.Fprintf(w, "таблица слотов перезалита, дайджест %s\n", digest)
|
fmt.Fprintf(w, "таблица слотов перезалита, дайджест %s\n", digest)
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Printf("API на http://%s (/status, /metrics, /drain, /enable, /reapply)", a.cfg.API)
|
log.Printf("API на http://%s (/status, /neigh.txt, /metrics, /drain, /enable, /reapply)", a.cfg.API)
|
||||||
if err := http.ListenAndServe(a.cfg.API, mux); err != nil {
|
if err := http.ListenAndServe(a.cfg.API, mux); err != nil {
|
||||||
log.Fatalf("API: %v", err)
|
log.Fatalf("API: %v", err)
|
||||||
}
|
}
|
||||||
@@ -492,6 +529,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// Резолв next-hop MAC членов пула — на том же тикере, что и пробы:
|
||||||
|
// именно проба вызывает первый исходящий пакет с hcif-порта и тем
|
||||||
|
// самым запускает ARP ядра для ещё не резолвленных адресов.
|
||||||
|
agent.syncNeighbors(false)
|
||||||
|
|
||||||
for _, c := range changed {
|
for _, c := range changed {
|
||||||
if c {
|
if c {
|
||||||
if err := agent.apply(false); err != nil {
|
if err := agent.apply(false); err != nil {
|
||||||
|
|||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Резолв MAC членов пула через neigh-таблицу ядра — шаг 5.
|
||||||
|
//
|
||||||
|
// Адреса членов известны из конфигурации заранее, их MAC — нет (в реальном
|
||||||
|
// окружении сервер ещё не подключён, NIC может замениться). hcif-порты —
|
||||||
|
// настоящие L3-интерфейсы ядра в том же сегменте, что и члены пула, поэтому
|
||||||
|
// ядро само резолвит их MAC обычным ARP (entrypoint.sh больше не подсовывает
|
||||||
|
// статические permanent-записи). hcd лишь читает то, что уже узнало ядро, и
|
||||||
|
// отражает это в единственном месте датапаса, где применяется MAC следующего
|
||||||
|
// перехода, — в таблице 21 (см. pipeline.sh).
|
||||||
|
//
|
||||||
|
// Для клиентов (адрес заранее не известен и не enumerable) резолв остаётся
|
||||||
|
// пассивным действием learn в самом OpenFlow — здесь не участвует.
|
||||||
|
|
||||||
|
// neighEntry — одна запись `ip -json neigh show`.
|
||||||
|
type neighEntry struct {
|
||||||
|
Dst string `json:"dst"`
|
||||||
|
Dev string `json:"dev"`
|
||||||
|
Lladdr string `json:"lladdr"`
|
||||||
|
State []string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvedStates — состояния ARP, при которых MAC считается годным к
|
||||||
|
// использованию. FAILED и INCOMPLETE (или отсутствие записи) означают, что
|
||||||
|
// адрес пока не резолвлен.
|
||||||
|
var resolvedStates = map[string]bool{
|
||||||
|
"REACHABLE": true,
|
||||||
|
"STALE": true,
|
||||||
|
"DELAY": true,
|
||||||
|
"PROBE": true,
|
||||||
|
"PERMANENT": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveMAC возвращает MAC адреса ip на интерфейсе iface, если ядро его уже
|
||||||
|
// резолвило. Пустая строка — адрес пока не резолвлен либо недостижим.
|
||||||
|
func resolveMAC(iface, ip string) (string, error) {
|
||||||
|
out, err := exec.Command("ip", "-json", "neigh", "show", "dev", iface, "to", ip).Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var entries []neighEntry
|
||||||
|
if err := json.Unmarshal(out, &entries); err != nil {
|
||||||
|
return "", fmt.Errorf("разбор ip -json neigh: %w", err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Lladdr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, s := range e.State {
|
||||||
|
if resolvedStates[s] {
|
||||||
|
return e.Lladdr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncNeighbors проходит по всем членам, резолвит их MAC через ядро и для
|
||||||
|
// каждого изменения атомарно обновляет его правило в таблице adjacency —
|
||||||
|
// точечным бандлом, не трогая записи остальных членов. Вызывается на том же
|
||||||
|
// тикере, что и health-пробы.
|
||||||
|
//
|
||||||
|
// force заливает правило заново, даже если MAC не изменился с прошлого раза:
|
||||||
|
// нужно после apply.sh (replace-flows стирает весь пайплайн, включая уже
|
||||||
|
// залитые сюда правила, а resolveMAC() в следующий раз вернёт тот же MAC и
|
||||||
|
// без force заливка не повторится). Тот же приём, что у Agent.apply(force).
|
||||||
|
func (a *Agent) syncNeighbors(force bool) {
|
||||||
|
for _, m := range a.cfg.Members {
|
||||||
|
mac, err := resolveMAC(m.Iface, m.Address)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("neigh: %s (%s@%s): %v", m.Name, m.Address, m.Iface, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
changed := force || mac != m.mac
|
||||||
|
if mac != m.mac {
|
||||||
|
m.macSince = time.Now()
|
||||||
|
}
|
||||||
|
m.mac = mac
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mac == "" {
|
||||||
|
log.Printf("neigh: %s (%s) MAC утерян — снимаю запись adjacency", m.Name, m.Address)
|
||||||
|
if err := a.applyNeigh(m); err != nil {
|
||||||
|
log.Printf("neigh: %s: ошибка снятия записи: %v", m.Name, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("neigh: %s (%s@%s) -> %s", m.Name, m.Address, m.Iface, mac)
|
||||||
|
if err := a.applyNeigh(m); err != nil {
|
||||||
|
log.Printf("neigh: %s: ошибка применения: %v", m.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyNeigh заливает точечный бандл для ОДНОГО члена: снимает прежнее
|
||||||
|
// правило adjacency (если было) и, если MAC резолвлен, ставит новое. Формат
|
||||||
|
// файла бандла — тот же, что и для таблицы слотов (FlowBundle в maglev.go).
|
||||||
|
func (a *Agent) applyNeigh(m *Member) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
mac := m.mac
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
table := a.cfg.AdjTable
|
||||||
|
out := fmt.Sprintf("flow delete table=%d,ip,nw_dst=%s\n", table, m.Address)
|
||||||
|
if mac != "" {
|
||||||
|
out += fmt.Sprintf("flow add table=%d,priority=100,ip,nw_dst=%s actions=mod_dl_dst:%s,output:%d\n",
|
||||||
|
table, m.Address, mac, m.OFPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/var/run/openvswitch/neigh-%s.bundle", m.Name)
|
||||||
|
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, err := exec.Command("ovs-ofctl", "-O", a.cfg.OFVersion, "bundle", a.cfg.Bridge, path).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ovs-ofctl bundle: %v: %s", err, res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+10
-15
@@ -82,31 +82,26 @@ attach_port "$P3_MAC" "$P3_IFNAME" "$P3_OFPORT"
|
|||||||
# --- 4a. Порты-источники health-проб -----------------------------------------
|
# --- 4a. Порты-источники health-проб -----------------------------------------
|
||||||
# Единственные адреса, которые узел держит в ядре: с них уходят пробы (§7.1
|
# Единственные адреса, которые узел держит в ядре: с них уходят пробы (§7.1
|
||||||
# дизайна v1 — источник проб должен быть уникальным адресом узла, не VIP).
|
# дизайна v1 — источник проб должен быть уникальным адресом узла, не VIP).
|
||||||
# MAC бэкенда прописывается статически: ARP-резолвер узлу не нужен, MAC членов
|
#
|
||||||
# пула и так зафиксированы в docker-compose.yml.
|
# MAC членов пула здесь больше НЕ прописывается статически (шаг 5): hcif-порты
|
||||||
|
# — настоящие L3-интерфейсы ядра в том же сегменте, что и бэкенды, и ядро само
|
||||||
|
# резолвит их MAC обычным ARP. Первая проба после старта чуть медленнее (кадр
|
||||||
|
# ARP-запроса уходит через br-lb и возвращается ответом реального бэкенда), но
|
||||||
|
# дальше запись живёт в neigh-таблице ядра обычным старением ARP — именно её
|
||||||
|
# читает hcd (hc/neigh.go) и заливает в таблицу 21 OpenFlow.
|
||||||
hc_port() {
|
hc_port() {
|
||||||
local name="$1" ofport="$2" mac="$3" ip="$4" net="$5"
|
local name="$1" ofport="$2" mac="$3" ip="$4" net="$5"
|
||||||
shift 5
|
|
||||||
ovs-vsctl --may-exist add-port "$BR" "$name" \
|
ovs-vsctl --may-exist add-port "$BR" "$name" \
|
||||||
-- set Interface "$name" type=internal ofport_request="$ofport" \
|
-- set Interface "$name" type=internal ofport_request="$ofport" \
|
||||||
-- set Interface "$name" mac="\"$mac\""
|
-- set Interface "$name" mac="\"$mac\""
|
||||||
ip link set dev "$name" address "$mac"
|
ip link set dev "$name" address "$mac"
|
||||||
ip addr replace "$ip/${net##*/}" dev "$name"
|
ip addr replace "$ip/${net##*/}" dev "$name"
|
||||||
ip link set dev "$name" up
|
ip link set dev "$name" up
|
||||||
# Остальные аргументы — пары «адрес MAC» членов пула в этом сегменте.
|
log "порт $name ($ip) — источник health-проб"
|
||||||
local peers=""
|
|
||||||
while [ $# -ge 2 ]; do
|
|
||||||
ip neigh replace "$1" lladdr "$2" dev "$name" nud permanent
|
|
||||||
peers="$peers $1"
|
|
||||||
shift 2
|
|
||||||
done
|
|
||||||
log "порт $name ($ip) — источник health-проб для$peers"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET" \
|
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET"
|
||||||
"$BE1_IP" "$BE1_MAC" "$BE3_IP" "$BE3_MAC"
|
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET"
|
||||||
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET" \
|
|
||||||
"$BE2_IP" "$BE2_MAC" "$BE4_IP" "$BE4_MAC"
|
|
||||||
|
|
||||||
# Ждём, пока vswitchd реально привяжет порты к датапасу.
|
# Ждём, пока vswitchd реально привяжет порты к датапасу.
|
||||||
for name in "$PUB_IFNAME" "$P2_IFNAME" "$P3_IFNAME"; do
|
for name in "$PUB_IFNAME" "$P2_IFNAME" "$P3_IFNAME"; do
|
||||||
|
|||||||
+17
-4
@@ -2,6 +2,10 @@
|
|||||||
# Генерация конфигурации демона hcd из topology.env — единственного источника
|
# Генерация конфигурации демона hcd из topology.env — единственного источника
|
||||||
# правды по адресам стенда. Пока пул задан статически; на следующем шаге его
|
# правды по адресам стенда. Пока пул задан статически; на следующем шаге его
|
||||||
# место займёт модель Load_Balancer -> Listener -> Pool -> Member в OVSDB.
|
# место займёт модель Load_Balancer -> Listener -> Pool -> Member в OVSDB.
|
||||||
|
#
|
||||||
|
# MAC членов пула здесь не фигурирует (шаг 5): hcd резолвит его сам через
|
||||||
|
# neigh-таблицу ядра (hc/neigh.go) на интерфейсе "iface" и заливает в таблицу
|
||||||
|
# "adj_table" по указанному "ofport". См. docs/STEP5_SUMMARY.md.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
LB_DIR=/opt/lb
|
LB_DIR=/opt/lb
|
||||||
@@ -18,6 +22,7 @@ cat > "$OUT" <<EOF
|
|||||||
"slots": $SLOTS,
|
"slots": $SLOTS,
|
||||||
"slot_table": 11,
|
"slot_table": 11,
|
||||||
"dnat_table": 12,
|
"dnat_table": 12,
|
||||||
|
"adj_table": 21,
|
||||||
"probe": "$HC_PROBE",
|
"probe": "$HC_PROBE",
|
||||||
"http_path": "$HC_HTTP_PATH",
|
"http_path": "$HC_HTTP_PATH",
|
||||||
"interval": "$HC_INTERVAL",
|
"interval": "$HC_INTERVAL",
|
||||||
@@ -33,7 +38,9 @@ cat > "$OUT" <<EOF
|
|||||||
"address": "$BE1_IP",
|
"address": "$BE1_IP",
|
||||||
"port": $BE1_PORT,
|
"port": $BE1_PORT,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"source": "$HC2_IP"
|
"source": "$HC2_IP",
|
||||||
|
"iface": "$HC2_IFNAME",
|
||||||
|
"ofport": $BE1_OFPORT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@@ -41,7 +48,9 @@ cat > "$OUT" <<EOF
|
|||||||
"address": "$BE2_IP",
|
"address": "$BE2_IP",
|
||||||
"port": $BE2_PORT,
|
"port": $BE2_PORT,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"source": "$HC3_IP"
|
"source": "$HC3_IP",
|
||||||
|
"iface": "$HC3_IFNAME",
|
||||||
|
"ofport": $BE2_OFPORT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
@@ -49,7 +58,9 @@ cat > "$OUT" <<EOF
|
|||||||
"address": "$BE3_IP",
|
"address": "$BE3_IP",
|
||||||
"port": $BE3_PORT,
|
"port": $BE3_PORT,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"source": "$HC2_IP"
|
"source": "$HC2_IP",
|
||||||
|
"iface": "$HC2_IFNAME",
|
||||||
|
"ofport": $BE3_OFPORT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
@@ -57,7 +68,9 @@ cat > "$OUT" <<EOF
|
|||||||
"address": "$BE4_IP",
|
"address": "$BE4_IP",
|
||||||
"port": $BE4_PORT,
|
"port": $BE4_PORT,
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"source": "$HC3_IP"
|
"source": "$HC3_IP",
|
||||||
|
"iface": "$HC3_IFNAME",
|
||||||
|
"ofport": $BE4_OFPORT
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ lbctl — осмотр датапаса стенда hpnn_v2
|
|||||||
metrics метрики в формате Prometheus
|
metrics метрики в формате Prometheus
|
||||||
conns таблица соединений ct (зона $CT_ZONE)
|
conns таблица соединений ct (зона $CT_ZONE)
|
||||||
fdb таблица коммутации сегментов: выученные MAC и порты
|
fdb таблица коммутации сегментов: выученные MAC и порты
|
||||||
|
neigh MAC членов пула, резолвленный hcd через ARP ядра (таблица 21)
|
||||||
trace <src_ip> [src_port] [сегмент]
|
trace <src_ip> [src_port] [сегмент]
|
||||||
ofproto/trace для TCP-сессии клиент -> VIP.
|
ofproto/trace для TCP-сессии клиент -> VIP.
|
||||||
Сегмент: pub (по умолчанию), p2, p3 — определяет порт входа
|
Сегмент: pub (по умолчанию), p2, p3 — определяет порт входа
|
||||||
@@ -85,6 +86,12 @@ fdb)
|
|||||||
| sed -n 's/.*n_packets=\([0-9]*\).*priority=\(50\|10\),reg0=\(0x[0-9a-f]*\).*/ сегмент \3 приоритет \2: пакетов \1/p' \
|
| sed -n 's/.*n_packets=\([0-9]*\).*priority=\(50\|10\),reg0=\(0x[0-9a-f]*\).*/ сегмент \3 приоритет \2: пакетов \1/p' \
|
||||||
| sort
|
| sort
|
||||||
;;
|
;;
|
||||||
|
neigh)
|
||||||
|
api /neigh.txt
|
||||||
|
echo
|
||||||
|
echo "Правила таблицы 21 (priority=100 — заливает hcd, priority=90 — learn):"
|
||||||
|
ovs-ofctl -O "$OF" dump-flows "$BR" table=21 | grep -E 'priority=(100|90),ip' | sort
|
||||||
|
;;
|
||||||
trace)
|
trace)
|
||||||
src_ip="${2:?укажите IP клиента}"
|
src_ip="${2:?укажите IP клиента}"
|
||||||
src_port="${3:-40000}"
|
src_port="${3:-40000}"
|
||||||
|
|||||||
+21
-12
@@ -272,16 +272,18 @@ table=11,priority=0 actions=drop
|
|||||||
# Слот положил идентификатор члена в reg2. SNAT не выполняется ни в одном из
|
# Слот положил идентификатор члена в reg2. SNAT не выполняется ни в одном из
|
||||||
# путей: бэкенд видит реальный адрес клиента.
|
# путей: бэкенд видит реальный адрес клиента.
|
||||||
#
|
#
|
||||||
# Член в том же сегменте, что и клиент (приоритет 200) — выдача коммутацией:
|
# Член в том же сегменте, что и клиент (приоритет 200) — TTL не уменьшается,
|
||||||
# меняется только MAC назначения, TTL не уменьшается, маршрутизации нет. Для
|
# маршрутизации нет. MAC назначения здесь больше не пишется (шаг 5): следующий
|
||||||
# ВМ клиент и бэкенд остаются L2-соседями, какими и были.
|
# переход — таблица 21, единственное место, где живёт актуальный MAC члена
|
||||||
|
# (резолвит hcd через ARP, см. hc/neigh.go). Для ВМ клиент и бэкенд остаются
|
||||||
|
# L2-соседями, какими и были — таблица 21 сама не декрементирует TTL.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
for seg_reg in "$P2_REG:$P2_MEMBERS" "$P3_REG:$P3_MEMBERS"; do
|
for seg_reg in "$P2_REG:$P2_MEMBERS" "$P3_REG:$P3_MEMBERS"; do
|
||||||
reg="${seg_reg%%:*}"
|
reg="${seg_reg%%:*}"
|
||||||
for m in ${seg_reg#*:}; do
|
for m in ${seg_reg#*:}; do
|
||||||
eval "mip=\$${m}_IP; mport=\$${m}_PORT; mmac=\$${m}_MAC; mid=\$${m}_ID"
|
eval "mip=\$${m}_IP; mport=\$${m}_PORT; 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)"
|
echo "table=12,priority=200,reg0=$reg,ip,reg2=$mid actions=ct(commit,zone=$CT_ZONE,nat(dst=$mip:$mport),table=21)"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -329,15 +331,22 @@ table=20,priority=0 actions=drop
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Таблица 21 — adjacency (next-hop MAC + выходной порт)
|
# Таблица 21 — adjacency (next-hop MAC + выходной порт)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Члены пула прописаны статически: их MAC и порт фиксированы в topology.env.
|
# Единственное место, где применяется MAC следующего перехода — и для
|
||||||
table=21,priority=100,ip,nw_dst=$BE1_IP actions=mod_dl_dst:$BE1_MAC,output:$BE1_OFPORT
|
# маршрутизируемого пути (после таблицы 20), и для коммутируемого (напрямую
|
||||||
table=21,priority=100,ip,nw_dst=$BE2_IP actions=mod_dl_dst:$BE2_MAC,output:$BE2_OFPORT
|
# из таблицы 12, без decrement TTL). Три источника записей, по приоритету:
|
||||||
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
|
# priority=100 члены пула — заливает и обновляет hcd (hc/neigh.go),
|
||||||
# Ответы на health-пробы — в стек узла через internal-порты.
|
# резолвя MAC настоящим ARP через hcif-порты. Пусто до
|
||||||
|
# первого резолва после старта — fail-close ниже, окно то же,
|
||||||
|
# что у таблицы слотов.
|
||||||
|
# priority=90 клиенты (публичные и внутрисегментные) — действие learn из
|
||||||
|
# таблиц 0 и 1: адрес заранее не известен, изучается пассивно.
|
||||||
|
# priority=0 неизвестный next-hop — drop.
|
||||||
|
#
|
||||||
|
# Ответы на health-пробы — в стек узла через internal-порты; это собственные
|
||||||
|
# адреса узла, а не next-hop, поэтому остаются статичными.
|
||||||
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=$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
|
table=21,priority=100,ip,nw_dst=$HC3_IP actions=mod_dl_dst:$HC3_MAC,output:$HC3_OFPORT
|
||||||
# priority=90 — записи клиентов, устанавливаемые действием learn из таблиц 0 и 1.
|
|
||||||
table=21,priority=0 actions=drop
|
table=21,priority=0 actions=drop
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ VIP_PORT=80
|
|||||||
# Адрес шлюза LB_P2_IP и VIP приватного листенера P2_VIP отвечают одним MAC
|
# Адрес шлюза LB_P2_IP и VIP приватного листенера P2_VIP отвечают одним MAC
|
||||||
# (P2_MAC) — по нему пайплайн отличает L3-трафик к маршрутизатору от
|
# (P2_MAC) — по нему пайплайн отличает L3-трафик к маршрутизатору от
|
||||||
# L2-трафика сегмента.
|
# L2-трафика сегмента.
|
||||||
|
#
|
||||||
|
# BE*_MAC (шаг 5): это уже не источник правды для OpenFlow, а «заводской» MAC,
|
||||||
|
# который attach-segment.sh назначает интерфейсу ВМ — эмуляция того, что в
|
||||||
|
# реальности прошито в NIC сервера. В таблицу 21 этот MAC не подставляется:
|
||||||
|
# его заново резолвит hcd через обычный ARP на hcif-порту (hc/neigh.go) и
|
||||||
|
# заливает в датапас сам. pipeline.sh эти переменные больше не читает.
|
||||||
P2_IFNAME=p2
|
P2_IFNAME=p2
|
||||||
P2_OFPORT=2
|
P2_OFPORT=2
|
||||||
P2_MAC=02:42:0a:14:00:01
|
P2_MAC=02:42:0a:14:00:01
|
||||||
|
|||||||
+34
-2
@@ -299,12 +299,16 @@ esac
|
|||||||
|
|
||||||
# Путь остался коммутируемым: TTL ответа от VIP не уменьшен, значит клиент и
|
# Путь остался коммутируемым: TTL ответа от VIP не уменьшен, значит клиент и
|
||||||
# бэкенд по-прежнему L2-соседи и балансировщик не стал для них L3-хопом.
|
# бэкенд по-прежнему L2-соседи и балансировщик не стал для них L3-хопом.
|
||||||
|
# Захватываем с запасом: пул из 4 членов, capture -c должен гарантированно
|
||||||
|
# застать хотя бы один ответ от соседа по сегменту (be1/be3), а не только от
|
||||||
|
# кросс-сегментных (be2/be4) — иначе проверка становится статистически
|
||||||
|
# хрупкой на малой выборке.
|
||||||
ttls=$(mktemp)
|
ttls=$(mktemp)
|
||||||
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -ni cli2 -c 4 -v \
|
timeout 20 $DC exec -T lb-router timeout 15 tcpdump -ni cli2 -c 16 -v \
|
||||||
"tcp and src host $P2_VIP and tcp[tcpflags] & tcp-syn != 0" >"$ttls" 2>/dev/null &
|
"tcp and src host $P2_VIP and tcp[tcpflags] & tcp-syn != 0" >"$ttls" 2>/dev/null &
|
||||||
tpid=$!
|
tpid=$!
|
||||||
sleep 1
|
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
|
for _ in $(seq 32); 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
|
wait $tpid 2>/dev/null
|
||||||
grep -q 'ttl 64' "$ttls" && ok "ответ от VIP приходит с TTL 64 — путь коммутируемый, не маршрутный" \
|
grep -q 'ttl 64' "$ttls" && ok "ответ от VIP приходит с TTL 64 — путь коммутируемый, не маршрутный" \
|
||||||
|| bad "TTL ответа уменьшен: путь стал маршрутизируемым"
|
|| bad "TTL ответа уменьшен: путь стал маршрутизируемым"
|
||||||
@@ -323,6 +327,34 @@ $DC exec -T be1 ping -c1 -W2 10.20.0.254 >/dev/null 2>&1 \
|
|||||||
$DC exec -T cli3 curl -s --max-time 5 http://10.30.0.3:8080/ 2>/dev/null | grep -q 'backend=be4' \
|
$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 "связность второго сегмента нарушена"
|
&& ok "cli3 -> be4 во втором сегменте" || bad "связность второго сегмента нарушена"
|
||||||
|
|
||||||
|
head_ "17. Динамическое изучение MAC/ARP (шаг 5)"
|
||||||
|
# MAC членов пула больше не статичен: hcd резолвит его через обычный ARP на
|
||||||
|
# hcif-портах (ядро) и сам заливает таблицу 21. Проверяем, что резолвленный
|
||||||
|
# MAC совпадает с реальным MAC интерфейса ВМ, а не с константой topology.env.
|
||||||
|
neigh=$($DC exec -T lb-router lbctl neigh 2>/dev/null)
|
||||||
|
for be in be1:10.20.0.2 be2:10.30.0.2 be3:10.20.0.3 be4:10.30.0.3; do
|
||||||
|
name=${be%%:*}; ip=${be#*:}
|
||||||
|
real_mac=$($DC exec -T "$name" ip -o link show eth0 2>/dev/null | sed -n 's/.*link\/ether \([0-9a-f:]*\).*/\1/p')
|
||||||
|
resolved=$(echo "$neigh" | awk -v n="$name" '$1==n{print $4}')
|
||||||
|
if [ -n "$real_mac" ] && [ "$resolved" = "$real_mac" ]; then
|
||||||
|
ok "$name: hcd резолвил реальный MAC $real_mac (не из topology.env)"
|
||||||
|
else
|
||||||
|
bad "$name: резолвлен $resolved, реальный MAC интерфейса $real_mac"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Таблица 21 — единственное место с MAC следующего перехода; правило члена
|
||||||
|
# должно быть приоритета 100 (заливка hcd), а не 90 (пассивный learn клиента).
|
||||||
|
# Проверка заодно закрывает регрессию force-реапплая: apply.sh уже вызывался
|
||||||
|
# в блоке 14 и стирал весь пайплайн вместе с таблицей 21 — если бы /reapply
|
||||||
|
# не восстанавливал её принудительно, этих правил здесь бы уже не было.
|
||||||
|
t21=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=21 2>/dev/null)
|
||||||
|
for ip in 10.20.0.2 10.30.0.2 10.20.0.3 10.30.0.3; do
|
||||||
|
echo "$t21" | grep -q "priority=100,ip,nw_dst=$ip " \
|
||||||
|
&& ok "таблица 21: приоритет-100 правило для $ip пережило apply.sh" \
|
||||||
|
|| bad "таблица 21: нет приоритет-100 правила для $ip"
|
||||||
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Итог: успешно $pass, провалено $fail"
|
echo "Итог: успешно $pass, провалено $fail"
|
||||||
[ "$fail" -eq 0 ]
|
[ "$fail" -eq 0 ]
|
||||||
Reference in new issue
Block a user