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>
11 KiB
Шаг 5 — итоги: динамическое изучение MAC/ARP для бэкендов
Дата: 2026-08-18 Статус: выполнено, проверено после холодного перезапуска (94 из 94 проверок) План: STEP5_IMPLEMENTATION_PLAN.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.
Отклонения от плана
nud permanentдля members убраны полностью, без опционального ручногоmac-переопределения в JSON — как и намечалось («не делать этого на первой итерации»). Подтверждено достаточным: cold-start окно закрывается первой же успешной пробой.- Обнаружен и исправлен баг с
force, не предусмотренный явно в плане (план предполагал его как риск в общих чертах — «зависимость отip -json neigh», но не called out force-реапплай конкретно). Добавлена регрессионная проверка (verify.sh, блок 17) именно на этот случай. - Проверка 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) поддерживает флаг; при смене базового образа стоит переподтвердить.
Задел на следующие шаги
- Тот же приём (демон резолвит адрес через ядро и заливает точечным бандлом) применим к обнаружению самих хостов, если понадобится снять ограничение «только уже известные адреса» — потребует ARP-сканирования сегмента и решения, как новый хост попадает в конфигурацию пула.
- Control plane (
Load_Balancer → Listener → Pool → Memberв OVSDB,MULTITENANCY.md) естественно принимает полеifaceкак часть моделиMember, а не спецификуhc-config.sh.