Шаг 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
@@ -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 | Трафик между ВМ сегмента не блокирован | ☐ |
|
||||
| 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`) и обратно.
|
||||
**Смежный документ:** [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 напрямую, минуя
|
||||
@@ -70,11 +70,12 @@ flowchart TD
|
||||
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> применение члена"}
|
||||
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 -->|"член в другом сегменте<br/>ct(commit,nat)"| T20["<b>20/21</b> маршрутизация<br/>dec_ttl, adjacency"]
|
||||
T20 --> OUTR(["output: порт члена<br/>в другом сегменте"])
|
||||
T12 -->|"<b>член в сегменте клиента</b><br/><b>без dec_ttl</b>, минуя таблицу 20"| T21
|
||||
T12 -->|"член в другом сегменте"| T20["<b>20</b> маршрутизация<br/>dec_ttl"]
|
||||
T20 --> T21["<b>21</b> adjacency<br/>MAC члена + порт —<br/>резолвит hcd через ARP<br/>ядра (шаг 5), единое<br/>место для обоих путей"]
|
||||
T21 --> OUTR(["output: порт члена"])
|
||||
|
||||
T15 --> T16["<b>16</b> пост-ct"] --> T20
|
||||
T24{"<b>24</b> обратная трансляция<br/>сегмента"}
|
||||
@@ -87,6 +88,11 @@ flowchart TD
|
||||
class DROPF d
|
||||
```
|
||||
|
||||
Таблица 21 — единственное место, где применяется MAC следующего перехода, для
|
||||
обоих путей сразу: и для члена в сегменте клиента (сюда ведёт таблица 12
|
||||
напрямую, без `dec_ttl`), и для члена в другом сегменте (сюда приходят из
|
||||
таблицы 20, уже после `dec_ttl`). MAC члена не дублируется между таблицами.
|
||||
|
||||
## 3. Основной поток: клиент и член пула в одном сегменте
|
||||
|
||||
Тот самый случай, ради которого сегмент переведён на порты OVS.
|
||||
@@ -105,8 +111,9 @@ sequenceDiagram
|
||||
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 OF: t12: ct(commit, nat(dst=10.20.0.2:8080))<br/>→ t21 напрямую, минуя t20
|
||||
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
|
||||
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
|
||||
@@ -181,8 +188,9 @@ flowchart TD
|
||||
| 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 | `reg0` = сегмент члена | `ct(commit,nat)` → t21 напрямую | **без `dec_ttl`**, минуя 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 | prio 0 | → t25 | обычный трафик проходит нетронутым |
|
||||
| 25 | `eth_dst` из FDB | `output` | записи `learn`, `idle_timeout` 300 с |
|
||||
@@ -203,7 +211,8 @@ flowchart TD
|
||||
|
||||
```bash
|
||||
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # путь сессии по таблицам
|
||||
make fdb # выученные MAC и счётчики рассылки
|
||||
make fdb # выученные MAC клиентов и счётчики рассылки
|
||||
make neigh # MAC членов пула, резолвленный hcd через ARP
|
||||
make ports # подключены ли ВМ сегментов
|
||||
docker compose exec lb-router lbctl flows 24 # счётчики обратной трансляции
|
||||
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.21:80` и обратно.
|
||||
**Смежный документ:** [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 уменьшается на
|
||||
@@ -156,7 +156,7 @@ flowchart TD
|
||||
| 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 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5), не `topology.env` |
|
||||
| 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с |
|
||||
|
||||
## 7. Где поток обрывается
|
||||
@@ -173,6 +173,7 @@ flowchart TD
|
||||
|
||||
```bash
|
||||
make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам
|
||||
make neigh # MAC членов, резолвленный hcd через ARP
|
||||
make slots # раскладка и per-member счётчики
|
||||
make conns # записи ct
|
||||
docker compose exec lb-router lbctl flows 21
|
||||
|
||||
@@ -83,8 +83,9 @@
|
||||
выводит его из группы. У приложения есть `/healthz` как задел.
|
||||
- **Пул и листенер статические** — заданы в `topology.env`, control plane и
|
||||
OVSDB-схемы из §6 дизайна v1 нет.
|
||||
- **MAC бэкендов прописаны статически.** ARP-резолвер next-hop отсутствует;
|
||||
MAC зафиксированы в `docker-compose.yml`.
|
||||
- ~~MAC бэкендов прописаны статически. ARP-резолвер next-hop отсутствует~~ —
|
||||
снято на шаге 5: `hcd` резолвит MAC членов пула через обычный ARP ядра, см.
|
||||
[STEP5_SUMMARY.md](STEP5_SUMMARY.md).
|
||||
- **Только TCP и только IPv4.** UDP- и L3-листенеров нет.
|
||||
|
||||
## Задел на шаг 2
|
||||
|
||||
@@ -189,6 +189,15 @@ packet-in/packet-out по своей neigh-таблице (§4.3, §8.2), но
|
||||
интеграция с тем, как EVPN-фабрика публикует MAC-привязки, чтобы control plane
|
||||
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. Итоговая сводка условий и рисков
|
||||
|
||||
| # | Пункт | Тип | Статус |
|
||||
|
||||
@@ -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`.
|
||||
Reference in new issue
Block a user