9.3 KiB
Шаг 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 → раскладка восстановлена в актуальном составе |
Отклонения от плана
-
Формат bundle-файла. Планировались директивы
delete/add; OVS 3.1 принимает их только с указанием типа сообщения:flow delete/flow add. Первый вариант отвергался сUnsupported bundle message type. -
Шаг перестановки Maglev приводится к нечётному. При числе слотов 1024 (степень двойки) шаг, выбранный как
h2 % (M-1) + 1, может оказаться чётным — тогда последовательность(offset + j*skip) mod Mпокрывает лишь половину слотов и заполнение зацикливается. Классический алгоритм рассчитан на простое M; здесь сохранено значение 1024 из §5.1 дизайна, а взаимная простота обеспечена принудительной нечётностью шага. -
Форматирование
lbctl healthвынесено в демон (/status.txt) — иначе в образ балансировщика пришлось бы тянутьjqилиpython3. -
Проверка выживания сессии добавлена в
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
- Stateless-датапас: заменить
ct(nat)на явный un-DNAT (nw_src=member_ip, tp_src=member_port→src = VIP). Таблица слотов уже на месте,symmetric_l4даёт одинаковый слот для обоих направлений — это и есть предпосылка, ради которой шаг 2 сделан именно так. - Проверить детерминизм выбора по корпусу синтетических 5-tuple через
ofproto/trace(спайк S0 из §12 дизайна). - Control plane: схема OVSDB вместо
topology.env, REST API иlbctlповерх неё, валидации §8.4. - Второй узел LB: кворум наблюдений, генерации пулов, сравнение дайджестов.
- ICMP-транслятор в userspace для PMTUD и корректных ICMP-ошибок от имени VIP.