Files
hpnn-proto/docs/STEP2_SUMMARY.md
T

114 lines
9.3 KiB
Markdown
Raw Normal View History

# Шаг 2 — итоги: health-check и таблица слотов
**Дата:** 2026-08-16
**Статус:** выполнено, проверено после холодного перезапуска (50 из 50 проверок)
**План:** [STEP2_IMPLEMENTATION_PLAN.md](STEP2_IMPLEMENTATION_PLAN.md)
**Предыдущий шаг:** [STEP1_SUMMARY.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.