Files
ayurishchevandClaude Opus 5 fa389f119b Шаг 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>
2026-08-19 12:33:48 +03:00

128 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Шаг 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`.