44 KiB
План ручного тестирования стенда hpnn_v2
Пошаговая проверка обеих подсистем — маршрутизации и балансировки — и подсистемы health-check. Рассчитан на прохождение целиком примерно за 25–30 минут.
Автоматический аналог большей части проверок — make verify. Этот документ
нужен, чтобы увидеть поведение стенда своими глазами и понять, что означает
каждый результат.
Что происходит с пакетом на каждом шаге — в документах по потокам данных: публичная балансировка для блоков A–C и E–I, приватная балансировка для блока J.
Обозначения
| Метка | Где выполнять |
|---|---|
| [К] | на клиентской машине в подсети 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
Ожидается: семь контейнеров и таблица состояния пула, где все четыре члена
up и у каждого по 256 слотов. make up сам вызывает make attach —
подключение ВМ приватных сегментов к мосту.
Шаг 0.2 [Х] — убедиться, что всё запустилось:
docker compose ps
Ожидается:
hpnn-be1 Up ... (healthy)
hpnn-be2 Up ... (healthy)
hpnn-be3 Up ... (healthy)
hpnn-be4 Up ... (healthy)
hpnn-cli2 Up ...
hpnn-cli3 Up ...
hpnn-lb Up ... (healthy)
Проверьте, что ВМ сегментов подключены к мосту:
make ports
Ожидается 11 портов: pub0, p2, p3, hcif-p2, hcif-p3, be1, be3,
cli2, be2, be4, cli3. Если портов ВМ нет — make attach.
Если 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
Блок J. Листенер в приватном сегменте
Главное шага 3: клиент стоит в одном сегменте с бэкендами, и его исходный адрес
на бэкенде сохраняется. Все шаги выполняются на хосте — клиентом служит
контейнер cli2.
Шаг J.1 [Х] — убедиться, что в ОС клиента ничего не настроено:
docker compose exec cli2 ip route
Ожидается ровно одна строка — connected-маршрут 10.20.0.0/24 dev eth0. Ни
default, ни маршрута на VIP: адрес листенера находится в подсети клиента.
Шаг J.2 [Х] — доступность VIP сегмента:
docker compose exec cli2 ping -c2 10.20.0.100
Шаг J.3 [Х] — запрос к сервису через балансировщик:
docker compose exec cli2 curl -s http://10.20.0.100/
Ожидается строка вида:
backend=be1 time=... client=10.20.0.10:46064 served=10.20.0.2:8080 req=1
Здесь два ключевых поля: client — исходный адрес клиента (SNAT не
выполняется), served — адрес и порт бэкенда, то есть листенер принял на 80 и
оттранслировал на 8080.
Шаг J.4 [Х] — распределение по пулу:
docker compose exec cli2 sh -c 'for i in $(seq 24); do curl -s http://10.20.0.100/; done' \
| sort | uniq -c -w 12
Ожидается: встречаются все четыре бэкенда. be1 и be3 — соседи клиента по
сегменту (коммутируемый путь), be2 и be4 — из другого сегмента
(маршрутизируемый).
Шаг J.5 [Х] — увидеть разницу путей по TTL. В одном терминале:
docker compose exec lb-router tcpdump -ni cli2 -v \
'tcp and src host 10.20.0.100 and tcp[tcpflags] & tcp-syn != 0'
Во втором — десяток запросов из шага J.3.
Ожидается: у ответов ttl 64 и ttl 63. Первые пришли от be1/be3 —
для них балансировщик не был L3-хопом, он лишь подменил MAC и оттранслировал
адрес. Вторые — от be2/be4 через маршрутизацию, с уменьшением TTL.
Шаг J.6 [Х] — путь пакета по таблицам:
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2
Ожидается цепочка 0 → 1 → 10 → 11 → 12, затем после ct(...nat(dst=...)) —
Resuming from table 25 и вывод в порт члена. В Final flow видно, что
nw_src остался клиентским, а nw_ttl не уменьшен.
Шаг J.7 [Х] — внутрисегментный трафик не блокирован:
docker compose exec cli2 curl -s http://10.20.0.2:8080/ # клиент к бэкенду напрямую
docker compose exec be1 curl -s http://10.20.0.3:8080/ # бэкенд к бэкенду
docker compose exec be1 ping -c2 10.20.0.254 # шлюз Docker
Ожидается: все три работают. Балансировщик коммутирует этот трафик, не транслируя его.
Шаг J.8 [Х] — таблица коммутации:
make fdb
Ожидается: по строке на каждую ВМ сегмента с её MAC и портом, счётчики растут.
Шаг J.9 [Х] — выключить листенер в одном сегменте:
sed -i 's/^PRIV_LISTENERS=.*/PRIV_LISTENERS="p2"/' lb/topology.env
docker compose cp lb/topology.env lb-router:/opt/lb/topology.env
docker compose exec lb-router /opt/lb/apply.sh
docker compose exec cli3 curl -s --max-time 4 http://10.30.0.100/ # таймаут
docker compose exec cli3 curl -s http://10.30.0.2:8080/ # связность цела
docker compose exec cli2 curl -s http://10.20.0.100/ # второй сегмент работает
Вернуть обратно:
sed -i 's/^PRIV_LISTENERS=.*/PRIV_LISTENERS="p2 p3"/' lb/topology.env
docker compose cp lb/topology.env lb-router:/opt/lb/topology.env
docker compose exec lb-router /opt/lb/apply.sh
Итоговый чек-лист
| # | Проверка | Результат |
|---|---|---|
| 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 | Установленная сессия переживает дренаж | ☐ |
| J | В ОС клиента сегмента нет ни одного маршрута | ☐ |
| J | Бэкенд видит исходный IP клиента-соседа | ☐ |
| J | Листенер 80 транслирует на порт бэкенда 8080 | ☐ |
| J | Ответ от соседа по сегменту приходит с неуменьшенным TTL | ☐ |
| J | Трафик между ВМ сегмента не блокирован | ☐ |
| J | PRIV_LISTENERS выключает листенер, не трогая связность |
☐ |
Возврат стенда в исходное состояние
# [Х]
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 |
| Приватные листенеры (какие сегменты) | lb/topology.env, PRIV_LISTENERS |
make flows после копирования файла |
| Адрес приватного VIP | lb/topology.env, P2_VIP / P3_VIP |
то же |
«Пересборка образа» — это:
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.
Диагностика
Клиент сегмента не получает ответ от приватного VIP, или ответ приходит не от VIP.
Первая гипотеза — ВМ не подключены к мосту: make ports должен показывать
порты be1, be3, cli2, be2, be4, cli3. Вручную созданные veth
исчезают вместе с контейнером, поэтому после docker compose restart be1 или
пересоздания любого контейнера сегмента нужен make attach.
Если порты на месте — смотрите make fdb (выучен ли MAC клиента) и счётчики
таблицы 24 (docker compose exec lb-router lbctl flows 24): именно она снимает
трансляцию с ответа бэкенда соседу.
Контейнер 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