Шаг 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:
ayurishchevandClaude Opus 5 committed 2026-08-19 12:33:48 +03:00
1 parent f34d2edefa
commit fa389f119b
17 files changed
+678 -61

No files matched your search

+76
View File
@@ -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
View File
@@ -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/
+4 -3
View File
@@ -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
+3 -2
View File
@@ -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. Итоговая сводка условий и рисков
| # | Пункт | Тип | Статус |
+147
View File
@@ -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 пути** — требует правки ожидаемых
трасс в документации.
+127
View File
@@ -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`.