Files
hpnn-proto/docs/MANUAL_TEST_PLAN.md
T

37 KiB
Raw Blame History

План ручного тестирования стенда hpnn_v2

Пошаговая проверка обеих подсистем — маршрутизации и балансировки — и подсистемы health-check. Рассчитан на прохождение целиком примерно за 25–30 минут.

Автоматический аналог большей части проверок — make verify. Этот документ нужен, чтобы увидеть поведение стенда своими глазами и понять, что означает каждый результат.

Обозначения

Метка Где выполнять
[К] на клиентской машине в подсети 192.168.5.0/24
[Х] на хосте стенда (192.168.5.9), из каталога /opt/lvraid/claude/hpnn_v2

Команды [Х] требуют root или членства в группе docker.

Подготовка

Шаг 0.1 [Х] — поднять стенд:

cd /opt/lvraid/claude/hpnn_v2
make up

Ожидается: пять контейнеров в состоянии healthy и таблица состояния пула, где все четыре члена up и у каждого по 256 слотов.

Шаг 0.2 [Х] — убедиться, что всё запустилось:

docker compose ps

Ожидается:

hpnn-be1    Up ... (healthy)
hpnn-be2    Up ... (healthy)
hpnn-be3    Up ... (healthy)
hpnn-be4    Up ... (healthy)
hpnn-lb     Up ... (healthy)

Если hpnn-lb в состоянии Restarting — смотрите docker compose logs lb-router и раздел «Диагностика» в конце документа.

Шаг 0.3 [К] — проверить, что клиент в нужной подсети:

ip -br addr | grep 192.168.5

Адрес клиента понадобится дальше; обозначим его <CLIENT_IP>.

Если проверять хотите с самого хоста стенда, а не с отдельной машины, выполните [Х] make shim — macvlan-интерфейс контейнера и физический интерфейс хоста напрямую друг друга не видят, это ограничение macvlan. Тогда все шаги [К] выполняются на хосте, а <CLIENT_IP> = 192.168.5.13.


Блок A. Базовая доступность узла

Проверяем, что OVS отвечает за адреса, которых нет ни на одном интерфейсе: ARP-респондер и ICMP-респондер живут в правилах OpenFlow.

Шаг A.1 [К] — доступность адреса узла:

ping -c3 192.168.5.20

Ожидается: три ответа. Отвечает не сетевой стек, а правило таблицы 6.

Шаг A.2 [К] — доступность VIP:

ping -c3 192.168.5.21

Ожидается: три ответа.

Шаг A.3 [К] — какой MAC отдаёт ARP-респондер:

ip neigh flush 192.168.5.20 2>/dev/null; ping -c1 192.168.5.20 >/dev/null
ip neigh show | grep -E '192.168.5.2[01]'

Ожидается: оба адреса, .20 и .21, разрешаются в один и тот же MAC 02:42:c0:a8:05:14 — это MAC macvlan-порта балансировщика.

Если ответа нет ни на один ping — переходите сразу к разделу «Диагностика», дальнейшие блоки бессмысленны.


Блок B. Балансировка нагрузки

Шаг B.1 [К] — один запрос на VIP:

curl http://192.168.5.21/

Ожидается строка вида:

backend=be1    time=2026-08-16 20:18:49 MSK client=192.168.5.13:59660    served=10.20.0.2:8080        req=1

Шаг B.2 [К] — распределение по бэкендам:

for i in $(seq 24); do curl -s http://192.168.5.21/; done | sort | uniq -c -w 12

Ожидается: встречаются все четыре имени — be1…be4 — примерно поровну, по 6 ± 3 на 24 запроса. Заметный перекос на такой выборке нормален, это статистика, а не дефект; равномерность раскладки проверяется по слотам в шаге B.5, а не по числу запросов.

Что это означает: слот выбирается хэшем от (ip_src, tcp_src), а порт источника у каждого нового соединения свой.

Шаг B.3 [К] — сохранение IP клиента (ключевая проверка DNAT-без-SNAT):

curl -s http://192.168.5.21/ | grep -o 'client=[0-9.]*'

Ожидается: client=<CLIENT_IP> — реальный адрес вашей машины.

Если бы выполнялся SNAT, здесь стоял бы адрес балансировщика. Бэкенд видит клиента напрямую — это то, ради чего обратный трафик заворачивается через балансировщик маршрутом.

Шаг B.4 [К] — одно соединение не «размазывается» по бэкендам:

curl -s http://192.168.5.21/info

Ожидается: развёрнутая карточка одного бэкенда. Все пакеты одной сессии идут на один член пула — хэш считается от заголовков, одинаковых внутри сессии.

Шаг B.5 [Х] — увидеть распределение со стороны датапаса:

make slots

Ожидается: у всех четырёх членов по 256 слотов, счётчики пакетов растут у всех.


Блок C. Подсистема маршрутизации

Балансировка — не единственная функция узла. Проверяем транзит.

Шаг C.1 [Х] — транзит между приватными сегментами:

docker compose exec be1 curl -s http://10.30.0.2:8080/

Ожидается: ответ backend=be2, в поле client — 10.20.0.2.

Пакет прошёл из сети 2 в сеть 3 через таблицы маршрутизации OpenFlow (20 и 21), без участия ядра контейнера и без всякой трансляции.

Шаг C.2 [К] — маршрутизация из публичного сегмента в приватный. Пропишите на клиенте маршруты в приватные сегменты через узел — команды для Linux и Windows 11 приведены в приложении А. После этого обратитесь к бэкендам напрямую, минуя VIP:

curl http://10.20.0.2:8080/
curl http://10.30.0.2:8080/

Ожидается: ответы от be1 и be2 соответственно, в обоих client=<CLIENT_IP>. Балансировка здесь не участвует — работает только подсистема маршрутизации.

Не выполняйте этот шаг на хосте стенда. Там эти маршруты перекроют connected-маршруты docker-бриджей hpnn-p2/hpnn-p3 и оборвут бэкендам выход наружу. На отдельной клиентской машине проблемы нет.

Шаг C.3 [К] — узел является настоящим L3-хопом:

ping -c1 -t 1 10.20.0.2      # Linux:   потеря, пакет умирает на узле
ping -c1 -t 2 10.20.0.2      # Linux:   проходит
ping -n 1 -i 1 10.20.0.2     # Windows: потеря
ping -n 1 -i 2 10.20.0.2     # Windows: проходит

Ожидается: с TTL=1 ответа нет, с TTL=2 есть. Сообщения TTL expired in transit не будет — узел ICMP-ошибки не генерирует, поэтому и traceroute / tracert через стенд ничего осмысленного не покажет.

Шаг C.4 [Х] — трафик действительно идёт через таблицы маршрутизации:

docker compose exec lb-router lbctl flows 20
docker compose exec lb-router lbctl flows 21

Ожидается: у правил nw_dst=10.20.0.0/24, nw_dst=10.30.0.0/24 и nw_dst=192.168.5.0/24 растут счётчики n_packets, у правила priority=0 actions=drop — нет.

Шаг C.5 [Х] — назначение без маршрута отбрасывается (дефолта у узла нет):

docker compose exec be1 ip route add 10.40.0.0/24 via 10.20.0.1
docker compose exec lb-router lbctl flows 20 | grep priority=0
docker compose exec be1 ping -c2 -W1 10.40.0.7
docker compose exec lb-router lbctl flows 20 | grep priority=0
docker compose exec be1 ip route del 10.40.0.0/24 via 10.20.0.1

Ожидается: ping без ответа, счётчик drop вырос ровно на число пакетов (2).

Шаг C.6 [Х] — ядро контейнера-узла в транзите не участвует:

docker compose exec lb-router ip route

Ожидается: всего два connected-маршрута, на hcif-p2 и hcif-p3, — они нужны только health-пробам. Маршрутов на клиентскую сеть и на бэкенды в ядре нет вовсе, а трафик при этом ходит: вся маршрутизация живёт в OpenFlow.

Шаг C.7 [К] — уберите маршруты после проверки (см. приложение А).

Шаг C.8 [Х] — профиль A: собственный трафик бэкенда идёт мимо балансировщика. В одном терминале:

docker compose exec lb-router tcpdump -ni p2 'host 1.1.1.1'

Во втором:

docker compose exec be1 curl -s -o /dev/null -w '%{http_code}\n' http://1.1.1.1/

Ожидается: код 301 во втором терминале и ни одного пакета в tcpdump. Через балансировщик у бэкенда маршрутизируются только клиентские префиксы, а default остаётся на шлюзе Docker.


Блок D. Health-check: наблюдение

Шаг D.1 [Х] — состояние пула:

make health

Ожидается таблица, где у всех четырёх членов СОСТОЯНИЕ=up, ADMIN=enabled, В ПУЛЕ=да, по 256 слотов, счётчик ПРОБ растёт при повторных вызовах, НЕУДАЧ не растёт.

Обратите внимание на колонку ИСТОЧНИК ПРОБ: 10.20.0.253 и 10.30.0.253 — уникальные адреса узла в сегментах бэкендов, а не VIP.

Шаг D.2 [Х] — убедиться, что пробы действительно уходят с этих адресов:

docker compose exec lb-router timeout 6 tcpdump -ni p2 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0'

Ожидается: примерно раз в 2 секунды SYN вида IP 10.20.0.253.xxxxx > 10.20.0.2.8080.

Почему это важно: если бы пробы уходили с VIP, ответ бэкенда попал бы в логику обратной трансляции и до пробера не дошёл.

Шаг D.3 [Х] — метрики:

make metrics

Ожидается: hpnn_member_up{member="be1",...} 1, аналогично для be2, hpnn_member_slots по 512, счётчики hpnn_probes_total растут.


Блок E. Отказ и восстановление бэкенда

Шаг E.1 [К] — запустите фоновую нагрузку, чтобы видеть поведение под трафиком (оставьте работать до конца блока):

while true; do curl -s --max-time 3 http://192.168.5.21/ || echo "ОШИБКА $(date +%T)"; sleep 0.5; done

Шаг E.2 [Х] — «уроните» первый бэкенд:

docker compose pause be1

Шаг E.3 [Х] — через 6–8 секунд посмотрите состояние:

make health

Ожидается: be1 в состоянии down, 0 слотов, в строке ниже — причина (context deadline exceeded); его 256 слотов разошлись между be2, be3 и be4 (примерно по 341).

Порог перехода: fall=3 неудачных пробы при интервале 2 с, то есть до 6 с.

Шаг E.4 [К] — посмотрите на окно с нагрузкой.

Ожидается: после короткого промежутка ответы приходят только от живых членов, строк ОШИБКА нет либо их единицы — те запросы, что успели уйти на be1 до обнаружения отказа. Это и есть цена интервала проб: чем он меньше, тем короче окно, но тем выше нагрузка проб на бэкенды.

Шаг E.5 [Х] — верните бэкенд:

docker compose unpause be1

Шаг E.6 [Х] — через 4–6 секунд:

make health

Ожидается: be1 снова up, слоты вернулись к 256 у каждого, и дайджест раскладки совпадает с тем, что был до отказа — раскладка детерминирована.

Шаг E.7 [К] — остановите фоновую нагрузку (Ctrl+C).


Блок F. Таблица слотов: минимальное возмущение

Самая содержательная проверка шага 2. Убеждаемся, что вывод одного члена не перекладывает слоты другого.

Шаг F.1 [Х] — снимок раскладки до изменения:

snap() { docker compose exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=11 \
  | sed -n 's/.*reg1=\(0x[0-9a-f]*\|[0-9]\+\).*set_field:\(0x[0-9a-f]*\)->reg2.*/\1 \2/p' | sort; }
snap > /tmp/slots_before.txt
wc -l < /tmp/slots_before.txt

Ожидается: 1024.

Шаг F.2 [Х] — вывести be1 из балансировки (не останавливая его):

make drain M=be1
make health

Ожидается: у be1 ADMIN=drain, В ПУЛЕ=нет, 0 слотов — но СОСТОЯНИЕ осталось up и счётчик проб продолжает расти. Дренаж означает «прекратить приём новых сессий», а не «остановить проверку».

Шаг F.3 [Х] — сравнить раскладки:

snap > /tmp/slots_after.txt
join /tmp/slots_before.txt /tmp/slots_after.txt | awk '$2!="0x1" && $2!=$3' | wc -l   # чужие слоты
join /tmp/slots_before.txt /tmp/slots_after.txt | awk '$2!=$3' | wc -l                # всего

Ожидается: первое число — не больше 20 (на этом стенде измерено 8), второе — 264.

Смысл: 256 слотов выбывшего be1 обязаны переехать — это его четверть таблицы. Показательно второе: у трёх оставшихся членов сменили владельца лишь единицы слотов. Классический Maglev гарантирует малое, а не строго нулевое возмущение, поэтому проверка сформулирована как порог, а не как равенство нулю.

Шаг F.4 [Х] — вернуть be1:

make enable M=be1
make health

Ожидается: по 256 слотов у каждого и прежний дайджест.

Шаг F.5 [Х] — проследить путь конкретной сессии по таблицам:

make trace SRC=192.168.5.100 SPORT=41234

Ожидается цепочка 0 → 10 → 11 → 12, затем после ct(...nat(dst=...)) — 20 → 21 и выход в порт бэкенда. В Final flow видно, что nw_src остался клиентским, nw_dst заменён на адрес бэкенда, nw_ttl уменьшен на единицу.

Повторите команду с теми же аргументами — номер слота (reg1) и член пула (reg2) обязаны совпасть. Это проверка детерминизма выбора.


Блок G. Fail-close при полном отказе пула

Шаг G.1 [Х] — «уронить» оба бэкенда:

docker compose pause be1 be2 be3 be4

Шаг G.2 [Х] — через 8 секунд:

make health
make slots

Ожидается: оба члена down, 0 слотов у каждого; в make slots строк с членами нет вовсе, есть только счётчик правила fail-close.

Шаг G.3 [К] — обратиться на VIP:

time curl --max-time 5 http://192.168.5.21/

Ожидается: таймаут, а не ответ и не мгновенный отказ. Балансировщик молча отбрасывает трафик: отдавать соединения на заведомо мёртвый бэкенд хуже, чем не отдавать вовсе.

Шаг G.4 [Х] — убедиться, что пакеты именно отброшены правилом, а не потерялись где-то ещё:

make slots

Ожидается: счётчик отброшено правилом fail-close вырос на число попыток.

Шаг G.5 [Х] — восстановить пул:

docker compose unpause be1 be2 be3 be4
sleep 6 && make health

Ожидается: все четыре up, по 256 слотов, прежний дайджест.


Блок H. Устойчивость конфигурации

Шаг H.1 [Х] — перезаливка пайплайна не теряет раскладку:

make health | grep дайджест      # запомните значение
make flows
make health | grep дайджест      # значение то же
make slots                 # по 256 слотов у каждого

Смысл: make flows перезаливает все правила целиком и стирает таблицу слотов, после чего демон восстанавливает её в актуальном составе пула, а не в полном.

Шаг H.2 [Х] — состояние переживает перезапуск балансировщика:

make health | grep дайджест      # запомните значение
docker compose restart lb-router
sleep 20 && make health

Ожидается: после перезапуска все четыре члена снова up (через rise=2 пробы) и дайджест раскладки тот же самый. Раскладка не хранится нигде — она вычисляется заново и совпадает, потому что алгоритм детерминирован.

Шаг H.3 [К] — стенд обслуживает трафик после перезапуска:

for i in $(seq 6); do curl -s http://192.168.5.21/; done

Блок I. Дополнительно: выживание установленной сессии

Шаг I.1 [К] — запустите длинную сессию (ответ отдаётся по строке в секунду) и запомните, какой бэкенд её обслуживает:

curl -N "http://192.168.5.21/slow?seconds=20"

Шаг I.2 [Х] — пока сессия идёт, выведите обслуживающий её член из пула (подставьте имя из вывода на клиенте):

make drain M=be2

Шаг I.3 [К] — наблюдайте за выводом.

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

Практический вывод: дренаж прекращает приём новых сессий, но не завершает активные. Для graceful shutdown приложение обязано само дождаться завершения запросов после исключения из пула.

Шаг I.4 [Х] — вернуть член:

make enable M=be2

Итоговый чек-лист

# Проверка Результат
A Узел и VIP отвечают на ping, оба адреса — один MAC ☐
B Запросы на VIP распределяются между be1 и be2 ☐
B Бэкенд видит реальный IP клиента ☐
C Транзит be1 → be2 работает ☐
C Клиент попадает в приватные сегменты через узел ☐
C TTL уменьшается: с -t 1 пакет не доходит, с -t 2 доходит ☐
C Счётчики таблиц 20 и 21 растут, drop-правило молчит ☐
C Назначение без маршрута отбрасывается со счётчиком ☐
C В ядре узла нет маршрутов на транзитные сети ☐
C Egress бэкенда идёт мимо балансировщика ☐
D Пробы уходят с адресов .253, не с VIP ☐
E Отказ бэкенда обнаружен за ~6 с, трафик перешёл на живого ☐
E После восстановления раскладка вернулась к прежнему дайджесту ☐
F При выводе члена ни один чужой слот не переехал ☐
F Повторный trace даёт тот же слот и член ☐
G При пустом пуле трафик отбрасывается, счётчик растёт ☐
H make flows и перезапуск не ломают раскладку ☐
I Установленная сессия переживает дренаж ☐

Возврат стенда в исходное состояние

# [Х]
for m in be1 be2 be3 be4; do make enable M=$m; done   # снять дренаж, если остался
docker compose unpause be1 be2 be3 be4 2>/dev/null || true
make health                                          # все up, по 256 слотов
rm -f /tmp/slots_before.txt /tmp/slots_after.txt

Маршруты, добавленные на клиенте в блоке C, снимаются командами из приложения А — они разные для Linux и Windows.

Если меняли параметры стенда — см. Б.5.

Полная остановка стенда: [Х] make down. Снять macvlan-shim, если поднимали: make shim-down.


Приложение А. Маршруты на клиенте

Нужны только для проверки подсистемы маршрутизации (блок C) — обращение к бэкендам напрямую, минуя VIP. Для проверки балансировки маршруты не нужны: VIP находится в той же подсети, что и клиент.

Шлюз во всех командах — адрес узла 192.168.5.20.

Linux

sudo ip route add 10.20.0.0/24 via 192.168.5.20
sudo ip route add 10.30.0.0/24 via 192.168.5.20

ip route get 10.20.0.2                 # проверка выбора маршрута
curl http://10.20.0.2:8080/

sudo ip route del 10.20.0.0/24 via 192.168.5.20
sudo ip route del 10.30.0.0/24 via 192.168.5.20

Маршруты живут до перезагрузки.

Windows 11 (PowerShell)

PowerShell нужно запустить от имени администратора.

Определить индекс сетевого интерфейса:

Get-NetIPAddress -AddressFamily IPv4 |
    Where-Object { $_.IPAddress -like '192.168.5.*' } |
    Select-Object IPAddress, InterfaceIndex, InterfaceAlias

$if = (Get-NetIPAddress -AddressFamily IPv4 |
       Where-Object { $_.IPAddress -like '192.168.5.*' } |
       Select-Object -First 1).InterfaceIndex

Если строк несколько (например, есть VPN-адаптер) — возьмите нужный индекс вручную.

Добавить маршруты:

New-NetRoute -DestinationPrefix 10.20.0.0/24 -NextHop 192.168.5.20 -InterfaceIndex $if -PolicyStore ActiveStore
New-NetRoute -DestinationPrefix 10.30.0.0/24 -NextHop 192.168.5.20 -InterfaceIndex $if -PolicyStore ActiveStore

-PolicyStore ActiveStore делает маршруты временными — до перезагрузки. Без этого параметра New-NetRoute пишет их в PersistentStore, и они переживут ребут; для теста это лишнее.

Проверить и обратиться к бэкендам:

Get-NetRoute -DestinationPrefix 10.2*.0.0/24 | Select-Object DestinationPrefix, NextHop, InterfaceIndex
Test-NetConnection 10.20.0.2 -Port 8080
curl.exe http://10.20.0.2:8080/
curl.exe http://10.30.0.2:8080/

В PowerShell curl — алиас на Invoke-WebRequest, поэтому пишите именно curl.exe.

Удалить после проверки:

Remove-NetRoute -DestinationPrefix 10.20.0.0/24 -Confirm:$false
Remove-NetRoute -DestinationPrefix 10.30.0.0/24 -Confirm:$false

Вариант через классический route (без -p тоже временный):

route add 10.20.0.0 mask 255.255.255.0 192.168.5.20
route add 10.30.0.0 mask 255.255.255.0 192.168.5.20
route print 10.*
route delete 10.20.0.0
route delete 10.30.0.0

Нагрузочное тестирование маршрутизации

Цель для k6 — http://10.20.0.2:8080/ вместо http://192.168.5.21/. Трафик пойдёт через таблицы маршрутизации 20 и 21, минуя листенер, таблицу слотов и DNAT. Сравнение показателей двух путей даёт цену балансировки в чистом виде.


Приложение Б. Изменение параметров стенда

Что где лежит

Что меняем Файл Как применить
Тип пробы, интервал, таймаут, rise/fall lb/topology.env, блок HC_* пересборка образа
Веса членов пула lb/hc-config.sh, поле weight пересборка образа
Ключ хэша (алгоритм балансировки) lb/pipeline.sh, таблица 10 make flows
Число слотов, basis хэша lb/topology.env: SLOTS, POOL_ID пересборка образа
Состав пула на лету — make drain / make enable

«Пересборка образа» — это:

docker compose up -d --build lb-router

Файлы вшиты в образ на этапе сборки, поэтому правка на хосте без пересборки ни на что не влияет. Конфигурация демона генерируется при старте контейнера, так что make flows для параметров health-check не поможет — нужен именно перезапуск.

Б.1. Параметры health-check

vi lb/topology.env
HC_PROBE=http          # http | tcp
HC_HTTP_PATH=/healthz
HC_INTERVAL=2s         # период опроса
HC_TIMEOUT=1s          # таймаут одной пробы
HC_RISE=2              # успехов подряд для перевода в up
HC_FALL=3              # неудач подряд для перевода в down
docker compose up -d --build lb-router
sleep 15 && make health

Проверить, что новые параметры применились, — в первой строке вывода make health:

пул 1: слотов 1024, проба http каждые 2s (rise=2 fall=3)

Практический смысл: время обнаружения отказа равно HC_FALL × HC_INTERVAL (по умолчанию 6 с), время возврата — HC_RISE × HC_INTERVAL (4 с). Уменьшая интервал, вы сокращаете окно, в котором часть запросов уходит на мёртвый бэкенд, но увеличиваете постоянную нагрузку проб.

Проверьте изменение по блоку E: docker compose pause be1 и засеките, за сколько член уйдёт в down.

Б.2. Алгоритм балансировки: ключ хэша

Строка листенера в lb/pipeline.sh, таблица 10:

actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[])

Первый аргумент — по каким полям пакета считается хэш. Проверено на OVS 3.1, принимаются все значения:

Значение Поведение
symmetric_l4 по умолчанию: адреса и порты, симметрично для обоих направлений
symmetric_l3l4, symmetric_l3l4+udp вариации симметричного хэша
symmetric_l3 только адреса: все сессии между парой хостов на одном бэкенде
nw_src affinity по адресу источника: весь трафик одного клиента на одном бэкенде
nw_dst, eth_src экзотические варианты, для полноты

Третий аргумент — способ отображения хэша в номер слота: modulo_n (по умолчанию), hash_threshold, hrw, iter_hash. Все принимаются; для таблицы слотов осмыслен modulo_n, остальные рассчитаны на выбор из небольшого числа каналов.

Применение — без перезапуска:

vi lb/pipeline.sh
docker compose cp lb/pipeline.sh lb-router:/opt/lb/pipeline.sh
docker compose exec lb-router /opt/lb/apply.sh

Чтобы изменение пережило пересоздание контейнера, потом соберите образ: docker compose up -d --build lb-router.

Проверка эффекта. С nw_src все запросы одного клиента должны попадать на один бэкенд:

for i in $(seq 8); do curl -s http://192.168.5.21/ | grep -o 'backend=[a-z0-9]*'; done | sort | uniq -c

Ожидается, что все 8 запросов уйдут на один бэкенд вместо деления между четырьмя. Так и проверялось на стенде: nw_src дал 8 из 8 на один член, возврат к symmetric_l4 вернул деление.

Б.3. Веса членов пула

В lb/hc-config.sh у каждого члена есть поле weight (по умолчанию 1):

{ "id": 1, "name": "be1", "address": "10.20.0.2", "port": 8080, "weight": 3, "source": "10.20.0.253" }
docker compose up -d --build lb-router
sleep 15 && make health

Ожидается пропорциональное деление слотов: член с весом 3 получит втрое больше слотов, чем член с весом 1. Проверено на пуле из двух членов — weight=3 против weight=1 дало 768 / 256 вместо 512 / 512.

Б.4. Число слотов и basis хэша

lb/topology.env:

POOL_ID=1              # basis хэша: разные пулы дают независимые раскладки
SLOTS=1024             # гранулярность весов

Значение SLOTS используется одновременно в правиле multipath и в раскладке демона, поэтому менять его нужно только здесь и с пересборкой — рассинхронизация этих двух мест приведёт к тому, что часть слотов окажется недостижима.

Гранулярность: минимальная доля, которую можно выдать члену, равна 1 / SLOTS. Для четырёх бэкендов 1024 слота — с большим запасом; смысл появится при десятках членов с разными весами.

Изменение POOL_ID полностью перетасует раскладку — это ожидаемо, он входит в хэш как seed. Дайджест при этом изменится.

Б.5. Возврат к исходной конфигурации

Все параметры стенда лежат в трёх файлах: lb/topology.env, lb/hc-config.sh, lb/pipeline.sh. Если стенд под git — git checkout этих файлов и пересборка. Исходные значения: проба http каждые 2 с, rise=2 fall=3, symmetric_l4, POOL_ID=1, SLOTS=1024, веса по 1, четыре члена по 256 слотов с дайджестом 0a16713c5a9eb94d.


Диагностика

Контейнер hpnn-lb перезапускается.

docker compose logs lb-router --tail=50

Частая причина — не загружен модуль ядра: make prereq (выполняет modprobe openvswitch).

Ping до 192.168.5.20 не проходит.

  • Проверьте, что клиент действительно в 192.168.5.0/24 и не отделён от хоста маршрутизатором с фильтрацией.
  • [Х] make ports — в мосту должны быть pub0, p2, p3, hcif-p2, hcif-p3.
  • [Х] docker compose exec lb-router tcpdump -ni pub0 arp — видно ли ARP-запросы клиента.
  • Если проверяете с самого хоста — нужен make shim.

Ping до узла проходит, а curl на VIP — таймаут.

make health     # есть ли живые члены пула
make slots      # растёт ли счётчик fail-close

Пустой пул — ожидаемое поведение fail-close, проверьте бэкенды: docker compose ps, docker compose logs be1.

Оба члена down, хотя бэкенды работают.

docker compose exec lb-router ip addr show hcif-p2
docker compose exec lb-router ip neigh show
docker compose exec lb-router curl -sv --max-time 3 http://10.20.0.2:8080/healthz

Ответ приходит, но client= содержит не ваш адрес.

Значит трафик пришёл не напрямую, а через промежуточный NAT — проверьте, что обращаетесь с машины из 192.168.5.0/24, а не через проброс портов.

Полный автоматический прогон для сравнения:

make verify