MVP Single Node with Manual Config in Docker
This commit is contained in:
commit
8212699f23
27 files changed
+3818
No files matched your search
@@ -0,0 +1,878 @@
|
||||
# План ручного тестирования стенда 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
|
||||
```
|
||||
Reference in new issue
Block a user