# План ручного тестирования стенда 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 [Х]** — поднять стенд: ```bash cd /opt/lvraid/claude/hpnn_v2 make up ``` Ожидается: пять контейнеров в состоянии `healthy` и таблица состояния пула, где все четыре члена `up` и у каждого по 256 слотов. **Шаг 0.2 [Х]** — убедиться, что всё запустилось: ```bash 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 [К]** — проверить, что клиент в нужной подсети: ```bash ip -br addr | grep 192.168.5 ``` Адрес клиента понадобится дальше; обозначим его ``. > Если проверять хотите с самого хоста стенда, а не с отдельной машины, > выполните **[Х]** `make shim` — macvlan-интерфейс контейнера и физический > интерфейс хоста напрямую друг друга не видят, это ограничение macvlan. > Тогда все шаги **[К]** выполняются на хосте, а `` = 192.168.5.13. --- ## Блок A. Базовая доступность узла Проверяем, что OVS отвечает за адреса, которых нет ни на одном интерфейсе: ARP-респондер и ICMP-респондер живут в правилах OpenFlow. **Шаг A.1 [К]** — доступность адреса узла: ```bash ping -c3 192.168.5.20 ``` Ожидается: три ответа. Отвечает не сетевой стек, а правило таблицы 6. **Шаг A.2 [К]** — доступность VIP: ```bash ping -c3 192.168.5.21 ``` Ожидается: три ответа. **Шаг A.3 [К]** — какой MAC отдаёт ARP-респондер: ```bash 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: ```bash 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 [К]** — распределение по бэкендам: ```bash 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): ```bash curl -s http://192.168.5.21/ | grep -o 'client=[0-9.]*' ``` Ожидается: `client=` — реальный адрес вашей машины. Если бы выполнялся SNAT, здесь стоял бы адрес балансировщика. Бэкенд видит клиента напрямую — это то, ради чего обратный трафик заворачивается через балансировщик маршрутом. **Шаг B.4 [К]** — одно соединение не «размазывается» по бэкендам: ```bash curl -s http://192.168.5.21/info ``` Ожидается: развёрнутая карточка одного бэкенда. Все пакеты одной сессии идут на один член пула — хэш считается от заголовков, одинаковых внутри сессии. **Шаг B.5 [Х]** — увидеть распределение со стороны датапаса: ```bash make slots ``` Ожидается: у всех четырёх членов по 256 слотов, счётчики пакетов растут у всех. --- ## Блок C. Подсистема маршрутизации Балансировка — не единственная функция узла. Проверяем транзит. **Шаг C.1 [Х]** — транзит между приватными сегментами: ```bash 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: ```bash curl http://10.20.0.2:8080/ curl http://10.30.0.2:8080/ ``` Ожидается: ответы от be1 и be2 соответственно, в обоих `client=`. Балансировка здесь не участвует — работает только подсистема маршрутизации. > **Не выполняйте этот шаг на хосте стенда.** Там эти маршруты перекроют > connected-маршруты docker-бриджей `hpnn-p2`/`hpnn-p3` и оборвут бэкендам > выход наружу. На отдельной клиентской машине проблемы нет. **Шаг C.3 [К]** — узел является настоящим L3-хопом: ```bash ping -c1 -t 1 10.20.0.2 # Linux: потеря, пакет умирает на узле ping -c1 -t 2 10.20.0.2 # Linux: проходит ``` ```powershell 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 [Х]** — трафик действительно идёт через таблицы маршрутизации: ```bash 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 [Х]** — назначение без маршрута отбрасывается (дефолта у узла нет): ```bash 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 [Х]** — ядро контейнера-узла в транзите не участвует: ```bash docker compose exec lb-router ip route ``` Ожидается: всего два connected-маршрута, на `hcif-p2` и `hcif-p3`, — они нужны только health-пробам. Маршрутов на клиентскую сеть и на бэкенды в ядре нет вовсе, а трафик при этом ходит: вся маршрутизация живёт в OpenFlow. **Шаг C.7 [К]** — уберите маршруты после проверки (см. приложение А). **Шаг C.8 [Х]** — профиль A: собственный трафик бэкенда идёт мимо балансировщика. В одном терминале: ```bash docker compose exec lb-router tcpdump -ni p2 'host 1.1.1.1' ``` Во втором: ```bash 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 [Х]** — состояние пула: ```bash make health ``` Ожидается таблица, где у всех четырёх членов `СОСТОЯНИЕ=up`, `ADMIN=enabled`, `В ПУЛЕ=да`, по 256 слотов, счётчик `ПРОБ` растёт при повторных вызовах, `НЕУДАЧ` не растёт. Обратите внимание на колонку `ИСТОЧНИК ПРОБ`: `10.20.0.253` и `10.30.0.253` — уникальные адреса узла в сегментах бэкендов, а не VIP. **Шаг D.2 [Х]** — убедиться, что пробы действительно уходят с этих адресов: ```bash 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 [Х]** — метрики: ```bash make metrics ``` Ожидается: `hpnn_member_up{member="be1",...} 1`, аналогично для be2, `hpnn_member_slots` по 512, счётчики `hpnn_probes_total` растут. --- ## Блок E. Отказ и восстановление бэкенда **Шаг E.1 [К]** — запустите фоновую нагрузку, чтобы видеть поведение под трафиком (оставьте работать до конца блока): ```bash while true; do curl -s --max-time 3 http://192.168.5.21/ || echo "ОШИБКА $(date +%T)"; sleep 0.5; done ``` **Шаг E.2 [Х]** — «уроните» первый бэкенд: ```bash docker compose pause be1 ``` **Шаг E.3 [Х]** — через 6–8 секунд посмотрите состояние: ```bash make health ``` Ожидается: `be1` в состоянии `down`, 0 слотов, в строке ниже — причина (`context deadline exceeded`); его 256 слотов разошлись между be2, be3 и be4 (примерно по 341). Порог перехода: `fall=3` неудачных пробы при интервале 2 с, то есть до 6 с. **Шаг E.4 [К]** — посмотрите на окно с нагрузкой. Ожидается: после короткого промежутка ответы приходят только от живых членов, строк `ОШИБКА` нет либо их единицы — те запросы, что успели уйти на be1 до обнаружения отказа. Это и есть цена интервала проб: чем он меньше, тем короче окно, но тем выше нагрузка проб на бэкенды. **Шаг E.5 [Х]** — верните бэкенд: ```bash docker compose unpause be1 ``` **Шаг E.6 [Х]** — через 4–6 секунд: ```bash make health ``` Ожидается: `be1` снова `up`, слоты вернулись к 256 у каждого, и **дайджест раскладки совпадает с тем, что был до отказа** — раскладка детерминирована. **Шаг E.7 [К]** — остановите фоновую нагрузку (Ctrl+C). --- ## Блок F. Таблица слотов: минимальное возмущение Самая содержательная проверка шага 2. Убеждаемся, что вывод одного члена не перекладывает слоты другого. **Шаг F.1 [Х]** — снимок раскладки до изменения: ```bash 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 из балансировки (не останавливая его): ```bash make drain M=be1 make health ``` Ожидается: у be1 `ADMIN=drain`, `В ПУЛЕ=нет`, 0 слотов — но `СОСТОЯНИЕ` осталось `up` и счётчик проб продолжает расти. Дренаж означает «прекратить приём новых сессий», а не «остановить проверку». **Шаг F.3 [Х]** — сравнить раскладки: ```bash 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: ```bash make enable M=be1 make health ``` Ожидается: по 256 слотов у каждого и прежний дайджест. **Шаг F.5 [Х]** — проследить путь конкретной сессии по таблицам: ```bash 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 [Х]** — «уронить» оба бэкенда: ```bash docker compose pause be1 be2 be3 be4 ``` **Шаг G.2 [Х]** — через 8 секунд: ```bash make health make slots ``` Ожидается: оба члена `down`, 0 слотов у каждого; в `make slots` строк с членами нет вовсе, есть только счётчик правила fail-close. **Шаг G.3 [К]** — обратиться на VIP: ```bash time curl --max-time 5 http://192.168.5.21/ ``` Ожидается: таймаут, а не ответ и не мгновенный отказ. Балансировщик молча отбрасывает трафик: отдавать соединения на заведомо мёртвый бэкенд хуже, чем не отдавать вовсе. **Шаг G.4 [Х]** — убедиться, что пакеты именно отброшены правилом, а не потерялись где-то ещё: ```bash make slots ``` Ожидается: счётчик `отброшено правилом fail-close` вырос на число попыток. **Шаг G.5 [Х]** — восстановить пул: ```bash docker compose unpause be1 be2 be3 be4 sleep 6 && make health ``` Ожидается: все четыре `up`, по 256 слотов, прежний дайджест. --- ## Блок H. Устойчивость конфигурации **Шаг H.1 [Х]** — перезаливка пайплайна не теряет раскладку: ```bash make health | grep дайджест # запомните значение make flows make health | grep дайджест # значение то же make slots # по 256 слотов у каждого ``` Смысл: `make flows` перезаливает все правила целиком и стирает таблицу слотов, после чего демон восстанавливает её в **актуальном** составе пула, а не в полном. **Шаг H.2 [Х]** — состояние переживает перезапуск балансировщика: ```bash make health | grep дайджест # запомните значение docker compose restart lb-router sleep 20 && make health ``` Ожидается: после перезапуска все четыре члена снова `up` (через `rise=2` пробы) и дайджест раскладки **тот же самый**. Раскладка не хранится нигде — она вычисляется заново и совпадает, потому что алгоритм детерминирован. **Шаг H.3 [К]** — стенд обслуживает трафик после перезапуска: ```bash for i in $(seq 6); do curl -s http://192.168.5.21/; done ``` --- ## Блок I. Дополнительно: выживание установленной сессии **Шаг I.1 [К]** — запустите длинную сессию (ответ отдаётся по строке в секунду) и запомните, какой бэкенд её обслуживает: ```bash curl -N "http://192.168.5.21/slow?seconds=20" ``` **Шаг I.2 [Х]** — пока сессия идёт, выведите обслуживающий её член из пула (подставьте имя из вывода на клиенте): ```bash make drain M=be2 ``` **Шаг I.3 [К]** — наблюдайте за выводом. Ожидается: сессия **не рвётся**, все 20 тиков приходят от того же бэкенда. Причина — трансляция закрепляется за соединением при его создании (`ct(commit, nat)`), и последующие пакеты следуют существующей привязке независимо от того, что показывает таблица слотов. Практический вывод: дренаж прекращает приём новых сессий, но не завершает активные. Для graceful shutdown приложение обязано само дождаться завершения запросов после исключения из пула. **Шаг I.4 [Х]** — вернуть член: ```bash 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 | Установленная сессия переживает дренаж | ☐ | --- ## Возврат стенда в исходное состояние ```bash # [Х] 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](#б5-возврат-к-исходной-конфигурации). Полная остановка стенда: **[Х]** `make down`. Снять macvlan-shim, если поднимали: `make shim-down`. --- ## Приложение А. Маршруты на клиенте Нужны только для проверки подсистемы маршрутизации (блок C) — обращение к бэкендам напрямую, минуя VIP. Для проверки балансировки маршруты не нужны: VIP находится в той же подсети, что и клиент. Шлюз во всех командах — адрес узла `192.168.5.20`. ### Linux ```bash 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 нужно запустить **от имени администратора**. Определить индекс сетевого интерфейса: ```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-адаптер) — возьмите нужный индекс вручную. Добавить маршруты: ```powershell 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`, и они переживут ребут; для теста это лишнее. Проверить и обратиться к бэкендам: ```powershell 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`. Удалить после проверки: ```powershell Remove-NetRoute -DestinationPrefix 10.20.0.0/24 -Confirm:$false Remove-NetRoute -DestinationPrefix 10.30.0.0/24 -Confirm:$false ``` Вариант через классический `route` (без `-p` тоже временный): ```powershell 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` | «Пересборка образа» — это: ```bash docker compose up -d --build lb-router ``` Файлы вшиты в образ на этапе сборки, поэтому правка на хосте без пересборки ни на что не влияет. Конфигурация демона генерируется при старте контейнера, так что `make flows` для параметров health-check не поможет — нужен именно перезапуск. ### Б.1. Параметры health-check ```bash vi lb/topology.env ``` ```bash HC_PROBE=http # http | tcp HC_HTTP_PATH=/healthz HC_INTERVAL=2s # период опроса HC_TIMEOUT=1s # таймаут одной пробы HC_RISE=2 # успехов подряд для перевода в up HC_FALL=3 # неудач подряд для перевода в down ``` ```bash 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`, остальные рассчитаны на выбор из небольшого числа каналов. Применение — без перезапуска: ```bash 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` все запросы одного клиента должны попадать на один бэкенд: ```bash 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): ```json { "id": 1, "name": "be1", "address": "10.20.0.2", "port": 8080, "weight": 3, "source": "10.20.0.253" } ``` ```bash 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`: ```bash 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` перезапускается.** ```bash 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 — таймаут.** ```bash make health # есть ли живые члены пула make slots # растёт ли счётчик fail-close ``` Пустой пул — ожидаемое поведение fail-close, проверьте бэкенды: `docker compose ps`, `docker compose logs be1`. **Оба члена `down`, хотя бэкенды работают.** ```bash 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, а не через проброс портов. **Полный автоматический прогон для сравнения:** ```bash make verify ```