Files
hpnn-proto/docs/STEP2_SUMMARY.md
T

9.3 KiB
Raw Blame History

Шаг 2 — итоги: health-check и таблица слотов

Дата: 2026-08-16 Статус: выполнено, проверено после холодного перезапуска (50 из 50 проверок) План: STEP2_IMPLEMENTATION_PLAN.md Предыдущий шаг: STEP1_SUMMARY.md

Что сделано

  • Демон hcd (Go, hc/) внутри контейнера-балансировщика: проверяет живость членов пула, ведёт их состояние с гистерезисом (rise=2, fall=3), пересчитывает раскладку слотов и заливает её в OVS. Стал основным процессом контейнера.
  • Пробы с уникального адреса узла (§7.1 дизайна v1): на мосту появились internal-порты hcif-p2 (10.20.0.253) и hcif-p3 (10.30.0.253) — это единственные адреса, которые узел держит в ядре. Источник соединения фиксируется через net.Dialer.LocalAddr.
  • Таблица слотов вместо группы type=select: 1024 слота, номер слота даёт multipath(symmetric_l4, basis=pool_id, modulo_n), раскладка слотов по членам считается алгоритмом Maglev (§5). Группа удалена.
  • Fail-close: пока живых членов нет, таблица слотов пуста и трафик на VIP отбрасывается со счётчиком.
  • Дренаж (§7.3): lbctl drain <член> / enable <член> — член исключается из раскладки, пробы при этом продолжают идти.
  • Наблюдаемость: lbctl health (состояние пула), lbctl slots (раскладка и счётчики), /metrics в формате Prometheus, дайджест раскладки SHA-256 (§5.3), в лог пишутся только переходы состояния.

Результаты проверок

Проверка Результат
Пробы идут оба члена up через 4 с после старта, задержка 1–3 мс
Источник проб tcpdump на p2: SYN с 10.20.0.253, не с VIP
Раскладка 1024 слота, 512/512, дайджест 2f0ca39d5f33efa6
Детерминизм после docker compose down/up дайджест тот же
Минимальное возмущение при выводе be1 переехали 512 его слотов, у be2 — ни одного
Отказ члена pause be1 → down за ~6 с, все 1024 слота у be2, трафик без ошибок
Восстановление unpause → up за ~4 с, дайджест вернулся к исходному
Выживание сессии длинная сессия через /slow не порвалась при выводе её члена из пула (14/14 тиков)
Fail-close оба члена мертвы → таблица пуста, клиент получает таймаут, счётчик drop растёт
Идемпотентность make flows → раскладка восстановлена в актуальном составе

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

  1. Формат bundle-файла. Планировались директивы delete / add; OVS 3.1 принимает их только с указанием типа сообщения: flow delete / flow add. Первый вариант отвергался с Unsupported bundle message type.

  2. Шаг перестановки Maglev приводится к нечётному. При числе слотов 1024 (степень двойки) шаг, выбранный как h2 % (M-1) + 1, может оказаться чётным — тогда последовательность (offset + j*skip) mod M покрывает лишь половину слотов и заполнение зацикливается. Классический алгоритм рассчитан на простое M; здесь сохранено значение 1024 из §5.1 дизайна, а взаимная простота обеспечена принудительной нечётностью шага.

  3. Форматирование lbctl health вынесено в демон (/status.txt) — иначе в образ балансировщика пришлось бы тянуть jq или python3.

  4. Проверка выживания сессии добавлена в verify.sh вместе с эндпоинтом /slow у бэкенда — в плане она была только описана.

Подтверждённое наблюдение о ct и таблице слотов

План фиксировал предположение, что установленные сессии переживают смену раскладки за счёт conntrack. Проверка подтвердила: сессия, обслуживаемая be2, не порвалась при выводе be2 из пула — все 14 тиков пришли с того же бэкенда. Причина в том, что ct(commit, nat) закрепляет трансляцию за соединением при его создании, и последующие пакеты следуют существующей привязке независимо от того, какого члена выбрала таблица слотов.

Практический вывод: в текущем stateful-датапасе Maglev защищает размещение новых соединений, а не живые сессии — их и без него защищает conntrack. Ценность таблицы слотов раскроется при переходе на stateless-датапас, где никакого ct не будет и единственной защитой от переезда сессий останется именно минимальное возмущение раскладки. Заодно это ровно то поведение, которое §7.3 дизайна описывает как «дренаж означает прекратить приём, а не мягко завершить».

Известные ограничения

Часть перешла с шага 1, часть появилась вместе с health-check:

  • Датапас по-прежнему stateful — обратную трансляцию делает ct(nat). Active/Active ECMP в такой схеме не работает: прямой и обратный трафик обязаны проходить через один узел.
  • Кворума нет. Вердикт о живости принимает единственный узел; §7.2 (наблюдения в Member_Observation, лидер через OVSDB lock, генерации пулов и сходимость по дайджестам) появится вместе с control plane.
  • Только HTTP и TCP-пробы. UDP-проб и ICMP echo из §7.1 нет.
  • Пул статичен — состав задан в topology.env, менять его на лету можно только дренажом. Модели Load_Balancer → Listener → Pool → Member в OVSDB ещё нет.
  • Веса поддержаны в алгоритме, но не в конфигурации — у всех членов вес 1.
  • hash_algo_version не реализована. Дайджест раскладки считается и логируется, но сравнивать его не с кем: узел один.
  • Пул без живых членов не снимает анонс VIP — альтернатива fail-close из §7.3 не реализована, BGP на узле нет.
  • ICMP-ошибки, фрагменты, IPv6, UDP- и L3-листенеры — как и на шаге 1, вне рамок.

Задел на шаг 3

  1. Stateless-датапас: заменить ct(nat) на явный un-DNAT (nw_src=member_ip, tp_src=member_port → src = VIP). Таблица слотов уже на месте, symmetric_l4 даёт одинаковый слот для обоих направлений — это и есть предпосылка, ради которой шаг 2 сделан именно так.
  2. Проверить детерминизм выбора по корпусу синтетических 5-tuple через ofproto/trace (спайк S0 из §12 дизайна).
  3. Control plane: схема OVSDB вместо topology.env, REST API и lbctl поверх неё, валидации §8.4.
  4. Второй узел LB: кворум наблюдений, генерации пулов, сравнение дайджестов.
  5. ICMP-транслятор в userspace для PMTUD и корректных ICMP-ошибок от имени VIP.