Files
hpnn-proto/docs/MANUAL_TEST_PLAN.md
T

879 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План ручного тестирования стенда 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
```
Адрес клиента понадобится дальше; обозначим его `<CLIENT_IP>`.
> Если проверять хотите с самого хоста стенда, а не с отдельной машины,
> выполните **[Х]** `make shim` — macvlan-интерфейс контейнера и физический
> интерфейс хоста напрямую друг друга не видят, это ограничение macvlan.
> Тогда все шаги **[К]** выполняются на хосте, а `<CLIENT_IP>` = 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=<CLIENT_IP>` — реальный адрес вашей машины.
Если бы выполнялся 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=<CLIENT_IP>`.
Балансировка здесь не участвует — работает только подсистема маршрутизации.
> **Не выполняйте этот шаг на хосте стенда.** Там эти маршруты перекроют
> 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
```