12 KiB
Шаг 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)
docs/STEP2_IMPLEMENTATION_PLAN.md— план внедрения; черновик уже написан под вариант с группой, привести к решению о таблице слотов.docs/STEP2_SUMMARY.md— итоги, отклонения, ограничения.README.md— раздел про health-check и таблицу слотов, схема с hc-портами, новые команды; таблица пайплайна перенумерована.