Files
hpnn-proto/docs/STEP2_IMPLEMENTATION_PLAN.md

12 KiB
Raw Permalink Blame History

Шаг 2 — подсистема health-check и таблица слотов

Контекст

Стенд шага 1 работает: OVS маршрутизирует три сегмента и балансирует TCP на два бэкенда группой type=select. Но состав пула статичен — отказ бэкенда балансировщику неизвестен, и половина соединений уходит в никуда.

Шаг 2 добавляет подсистему проверки живости: узел сам определяет состояние членов пула, выводит мёртвых из балансировки и возвращает восстановившихся. Это §7 дизайн-концепции ../hpnn_v1/docs/design.md, адаптированный к одноузловому прототипу (кворум по кластеру и генерации пулов появятся вместе с control plane).

Вместе с этим по решению заказчика группа type=select заменяется на таблицу слотов с Maglev-раскладкой (§5 дизайна). Причина: при перезаписи бакетов группы хэш пересчитывается целиком, и размещение новых соединений переезжает даже у нетронутых членов. Таблица слотов даёт минимальное возмущение — переезжают только слоты выбывшего члена — и является тем самым механизмом, на котором позже строится stateless-датапас.

Ключевые решения

Пробы уходят с уникального адреса узла в сегменте бэкенда (§7.1 дизайна), не с VIP. У чисто-OpenFlow узла шага 1 адресов в ядре нет вовсе, поэтому на мосту появляются два internal-порта: hcif-p2 (10.20.0.253) и hcif-p3 (10.30.0.253). Это lbif-bk из §3.3 — на них позже поселится FRR.

Пробер — демон на Go (hcd) внутри контейнера-балансировщика, как lb-agent на узле LB в дизайне. Становится основным процессом контейнера; ovsdb-server и ovs-vswitchd работают демонами рядом.

Fail-close при полном отказе пула: таблица слотов пустеет, остаётся только priority=0 actions=drop — трафик на VIP отбрасывается со счётчиком.

Дренаж (§7.3): член выводится из раскладки вручную, пробы при этом продолжают идти и состояние остаётся up.

Нюанс, который надо зафиксировать честно. В пайплайне остаётся ct(nat) для обратного пути, а conntrack закрепляет трансляцию за соединением при его создании. Поэтому уже установленные сессии переживают смену раскладки и без Maglev. Реальная ценность таблицы слотов здесь — стабильность размещения новых соединений и подготовка к stateless-датапасу, где никакой ct не подстрахует. Это проверяется отдельным тестом (см. верификацию).

Реализация

1. Датапас: слоты вместо группы (lb/pipeline.sh)

Таблицы перенумеровываются так, чтобы все переходы шли вперёд (goto_table назад не умеет):

Таблица Было Стало
10 листенер → group:100 листенер → multipath(...) → таблица 11
11 DNAT по reg2 таблица слотов: reg1=<slot> → reg2=<member>
12 — DNAT по reg2 (бывшая 11)
table=10,priority=200,tcp,nw_dst=<VIP>,tp_dst=80 \
    actions=multipath(symmetric_l4,<pool_id>,modulo_n,1024,0,NXM_NX_REG1[]),goto_table:11

# правила слотов заливает hcd; pipeline.sh отдаёт только fail-close основание
table=11,priority=0 actions=drop

Группа 100 удаляется (ovs-ofctl del-groups), apply.sh её больше не создаёт. Счётчики правил таблицы 11 дают бесплатную per-member статистику.

2. Порты источника проб (lb/entrypoint.sh)

Для каждого приватного сегмента:

ovs-vsctl --may-exist add-port br-lb hcif-p2 \
    -- set Interface hcif-p2 type=internal ofport_request=4 mac='"02:42:0a:14:00:fd"'
ip addr replace 10.20.0.253/24 dev hcif-p2
ip link set hcif-p2 up
ip neigh replace 10.20.0.2 lladdr 02:42:0a:14:00:02 dev hcif-p2 nud permanent

Статическая ARP-запись избавляет ядро от резолва MAC бэкенда — MAC членов и так зафиксированы в docker-compose.yml. Обратное направление (бэкенд резолвит 10.20.0.253) закрывает ARP-респондер OVS.

Правила для hc-портов в pipeline.sh:

Таблица Правило
0 in_port=hcif-* → таблица 20 (без learn, без ct)
5 ARP-респондер для 10.20.0.253 и 10.30.0.253
20 /32 на адреса hc-портов, приоритет 32 → таблица 21, без dec_ttl
21 adjacency на hc-порты: mod_dl_dst + output

Ответы бэкендов на пробы приходят на p2/p3, проходят таблицу 15 (записи в ct для них нет, трансляция не применяется) и уходят транзитом.

3. Демон hc/ (Go, ~350 строк)

hc/maglev.go — раскладка по §5.2 дизайна:

  • ключ члена — "address:port", не UUID: пересоздание члена с теми же адресом и портом обязано давать ту же раскладку;
  • вход — отсортированный по ключу список живых членов с весами + pool_id как seed;
  • 1024 слота; offset = h1 % M, skip приводится к нечётному — при M = 1024 (степень двойки) чётный шаг не покрывает все слоты и заполнение зациклилось бы;
  • slot_table_digest — SHA-256 от сериализованной раскладки (§5.3), пишется в лог при каждом применении.

hc/main.go — пробер и применение:

  • конфиг — JSON, который lb/hc-config.sh генерирует из topology.env;
  • тикер interval (2 с), пробы всех членов параллельно с таймаутом (1 с);
  • тип пробы http (GET /healthz, ожидается 2xx) или tcp (connect); источник фиксируется через net.Dialer.LocalAddr — это и есть требование «пробы с уникального адреса узла»;
  • переход в down после fall подряд неудач (3), в up — после rise подряд успехов (2); в лог пишутся только переходы, как в §7.1;
  • применение — атомарным бандлом ovs-ofctl bundle с директивами delete table=11 + add ..., то есть замена таблицы целиком без промежуточного состояния; повторно та же раскладка не заливается;
  • HTTP-API на 127.0.0.1:9111: /status (JSON), /metrics (Prometheus), /drain?member=, /enable?member=, /reapply.

/reapply дёргает lb/apply.sh после replace-flows: тот перезаливает весь пайплайн и стёр бы слоты, поэтому в конце сообщает демону перезалить их.

4. Инструментарий и сборка

  • lbctl health | slots | drain <m> | enable <m>; make health, make slots, make drain M=be1, make enable M=be1.
  • lbctl slots — сводка: сколько слотов у каждого члена, счётчики пакетов, дайджест.
  • Контекст сборки балансировщика расширяется до корня (context: ., dockerfile: lb/Dockerfile), первая стадия — golang:1.23-alpine.
  • В бэкенд добавляется /slow (ответ растягивается на N секунд) — нужен для проверки выживания установленной сессии.

5. Файлы

hc/{go.mod,main.go,maglev.go}      новый демон
lb/hc-config.sh                    генерация конфига из topology.env
lb/topology.env                    параметры проб, слотов, hc-портов
lb/pipeline.sh                     слоты, hc-порты, перенумерация таблиц
lb/entrypoint.sh                   internal-порты, статический neigh, запуск hcd
lb/apply.sh                        снятие группы, вызов /reapply
lb/lbctl.sh, Makefile              health / slots / drain / enable
backend/main.go                    эндпоинт /slow
docker-compose.yml                 контекст сборки
scripts/verify.sh                  новые проверки

Верификация

Добавляется в scripts/verify.sh:

Проверка Ожидание
Пробы доходят lbctl health — оба члена up, счётчик /healthz на бэкендах растёт
Источник проб на бэкенде видно обращение с 10.20.0.253/10.30.0.253, не с VIP
Раскладка слотов 1024 слота поделены поровну ±1, дайджест стабилен между перезапусками
Минимальное возмущение снимок раскладки → drain be1 → у be2 ни один слот не сменил владельца
Отказ члена docker compose pause be1 → down за fall × interval, его слоты перешли к be2
Балансировка при отказе все запросы на VIP обслуживает be2, ошибок нет
Восстановление unpause → up за rise × interval, раскладка вернулась к исходному дайджесту
Выживание сессии долгая сессия через /slow не рвётся при выводе второго члена
Полный отказ оба члена недоступны → в таблице 11 только drop, трафик на VIP падает со счётчиком
Идемпотентность make flows → слоты возвращаются в актуальном составе, а не в полном

Артефакты (по требованию CLAUDE.md)

  1. docs/STEP2_IMPLEMENTATION_PLAN.md — план внедрения; черновик уже написан под вариант с группой, привести к решению о таблице слотов.
  2. docs/STEP2_SUMMARY.md — итоги, отклонения, ограничения.
  3. README.md — раздел про health-check и таблицу слотов, схема с hc-портами, новые команды; таблица пайплайна перенумерована.