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>
128 lines
11 KiB
Markdown
128 lines
11 KiB
Markdown
# Шаг 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`.
|