114 lines
9.3 KiB
Markdown
114 lines
9.3 KiB
Markdown
# Шаг 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.
|