# Шаг 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`.