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

11 KiB
Raw Permalink Blame History

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

Отклонения от плана

  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.