MAC каждого члена пула больше не статическая константа в topology.env, а резолвится демоном hcd через обычный ARP ядра — в реальном окружении MAC бэкенда заранее не известен (сервер ещё не подключён, NIC может замениться), топология не описывается статически, в отличие от стенда. hcif-порты (единственные адреса узла в ядре) уже были настоящими L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh. Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно заливает бандл в таблицу 21 на том же тикере, что и health-пробы), получили резолвер без нового OpenFlow-контроллера. Таблица 21 стала единственным источником MAC для обоих путей — маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21. Исправлен попутно найденный баг: после apply.sh (replace-flows) таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал MAC с памятью демона, а не с датапасом. Добавлен force-режим по аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh. Проверено на живом стенде: MAC всех четырёх членов резолвлен и совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и применена без вмешательства за счёт штатного старения ARP ядра. Регрессия: 94 из 94 проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1103 lines
48 KiB
Markdown
1103 lines
48 KiB
Markdown
# План ручного тестирования стенда hpnn_v2
|
||
|
||
Пошаговая проверка обеих подсистем — маршрутизации и балансировки — и
|
||
подсистемы health-check. Рассчитан на прохождение целиком примерно за 25–30
|
||
минут.
|
||
|
||
Автоматический аналог большей части проверок — `make verify`. Этот документ
|
||
нужен, чтобы увидеть поведение стенда своими глазами и понять, что означает
|
||
каждый результат.
|
||
|
||
Что происходит с пакетом на каждом шаге — в документах по потокам данных:
|
||
[публичная балансировка](PUBLIC_LB_DATAFLOW.md) для блоков A–C и E–I,
|
||
[приватная балансировка](PRIVATE_LB_DATAFLOW.md) для блока J.
|
||
|
||
## Обозначения
|
||
|
||
| Метка | Где выполнять |
|
||
|---|---|
|
||
| **[К]** | на клиентской машине в подсети 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
|
||
```
|
||
|
||
Ожидается: семь контейнеров и таблица состояния пула, где все четыре члена
|
||
`up` и у каждого по 256 слотов. `make up` сам вызывает `make attach` —
|
||
подключение ВМ приватных сегментов к мосту.
|
||
|
||
**Шаг 0.2 [Х]** — убедиться, что всё запустилось:
|
||
|
||
```bash
|
||
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)
|
||
```
|
||
|
||
Проверьте, что ВМ сегментов подключены к мосту:
|
||
|
||
```bash
|
||
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 [К]** — проверить, что клиент в нужной подсети:
|
||
|
||
```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
|
||
```
|
||
|
||
---
|
||
|
||
## Блок J. Листенер в приватном сегменте
|
||
|
||
Главное шага 3: клиент стоит в одном сегменте с бэкендами, и его исходный адрес
|
||
на бэкенде сохраняется. Все шаги выполняются на хосте — клиентом служит
|
||
контейнер `cli2`.
|
||
|
||
**Шаг J.1 [Х]** — убедиться, что в ОС клиента ничего не настроено:
|
||
|
||
```bash
|
||
docker compose exec cli2 ip route
|
||
```
|
||
|
||
Ожидается ровно одна строка — connected-маршрут `10.20.0.0/24 dev eth0`. Ни
|
||
default, ни маршрута на VIP: адрес листенера находится в подсети клиента.
|
||
|
||
**Шаг J.2 [Х]** — доступность VIP сегмента:
|
||
|
||
```bash
|
||
docker compose exec cli2 ping -c2 10.20.0.100
|
||
```
|
||
|
||
**Шаг J.3 [Х]** — запрос к сервису через балансировщик:
|
||
|
||
```bash
|
||
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 [Х]** — распределение по пулу:
|
||
|
||
```bash
|
||
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. В одном терминале:
|
||
|
||
```bash
|
||
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 [Х]** — путь пакета по таблицам:
|
||
|
||
```bash
|
||
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 [Х]** — внутрисегментный трафик не блокирован:
|
||
|
||
```bash
|
||
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 [Х]** — таблица коммутации:
|
||
|
||
```bash
|
||
make fdb
|
||
```
|
||
|
||
Ожидается: по строке на каждую ВМ сегмента с её MAC и портом, счётчики растут.
|
||
|
||
**Шаг J.9 [Х]** — выключить листенер в одном сегменте:
|
||
|
||
```bash
|
||
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/ # второй сегмент работает
|
||
```
|
||
|
||
Вернуть обратно:
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## Блок K. Динамическое изучение MAC/ARP (шаг 5)
|
||
|
||
MAC каждого члена пула больше не статическая константа — его резолвит демон
|
||
`hcd` через обычный ARP ядра. Проверяем, что это действительно так, а не
|
||
осталось прежним поведением под новым названием.
|
||
|
||
**Шаг K.1 [Х]** — MAC членов пула резолвлен и совпадает с реальным:
|
||
|
||
```bash
|
||
make neigh
|
||
docker compose exec be1 ip link show eth0 | grep link/ether
|
||
```
|
||
|
||
Ожидается: `lbctl neigh` показывает у `be1` состояние «да» (резолвлен) и MAC,
|
||
совпадающий с выводом `ip link show` внутри контейнера `be1`.
|
||
|
||
**Шаг K.2 [Х]** — MAC действительно используется в датапасе:
|
||
|
||
```bash
|
||
docker compose exec lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=21 \
|
||
| grep priority=100
|
||
```
|
||
|
||
Ожидается: по правилу `priority=100,ip,nw_dst=<адрес члена>` на каждого
|
||
живого члена, `actions` содержат тот же MAC, что и в `make neigh`.
|
||
|
||
**Шаг K.3 [Х]** — обнаружение смены MAC «железа» без перезапуска стенда:
|
||
|
||
```bash
|
||
docker compose exec be1 ip link set eth0 down
|
||
docker compose exec be1 ip link set eth0 address 02:42:0a:14:00:99
|
||
docker compose exec be1 ip link set eth0 up
|
||
docker compose exec be1 ip route replace 192.168.5.0/24 via 10.20.0.1
|
||
docker compose exec be1 ip route replace 10.30.0.0/24 via 10.20.0.1
|
||
docker compose exec be1 ip route replace default via 10.20.0.254
|
||
```
|
||
|
||
Ожидается: не сразу, а в течение примерно двух минут (обычное старение ARP
|
||
ядра — `hcd` опрашивает быстро, но отражает только то, что уже узнало ядро)
|
||
`make neigh` покажет у `be1` новый MAC `02:42:0a:14:00:99`, и трафик к нему
|
||
продолжит доставляться:
|
||
|
||
```bash
|
||
watch -n5 'docker compose exec -T lb-router lbctl neigh'
|
||
# в отдельном терминале, после смены MAC в neigh:
|
||
docker compose exec cli2 curl -s http://10.20.0.2:8080/
|
||
```
|
||
|
||
Вернуть `be1` в исходное состояние (родной MAC пропишет заново
|
||
`scripts/attach-segment.sh`):
|
||
|
||
```bash
|
||
docker compose restart be1
|
||
sleep 2 && sudo scripts/attach-segment.sh
|
||
sleep 6 && make neigh
|
||
```
|
||
|
||
**Шаг K.4 [Х]** — заливка таблицы 21 переживает перезаливку пайплайна:
|
||
|
||
```bash
|
||
make health | grep -A1 дайджест # или просто make neigh — запомнить MAC
|
||
make flows
|
||
make neigh # те же MAC на месте, без ручного вмешательства
|
||
```
|
||
|
||
Смысл: `make flows` (`apply.sh`) стирает весь пайплайн вместе с таблицей 21;
|
||
`/reapply` у `hcd` обязан восстановить и таблицу слотов, и таблицу adjacency —
|
||
без этого второй `make neigh` показал бы пустые записи до следующего
|
||
естественного изменения MAC (которого может не случиться никогда).
|
||
|
||
---
|
||
|
||
## Итоговый чек-лист
|
||
|
||
| # | Проверка | Результат |
|
||
|---|---|---|
|
||
| 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` выключает листенер, не трогая связность | ☐ |
|
||
| K | MAC членов пула резолвлен и совпадает с реальным MAC интерфейса | ☐ |
|
||
| K | Таблица 21 содержит правило приоритета 100 с этим MAC | ☐ |
|
||
| K | Смена MAC «железа» обнаружена без перезапуска стенда | ☐ |
|
||
| K | Таблица 21 переживает `make flows` | ☐ |
|
||
|
||
---
|
||
|
||
## Возврат стенда в исходное состояние
|
||
|
||
```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` |
|
||
| Приватные листенеры (какие сегменты) | `lb/topology.env`, `PRIV_LISTENERS` | `make flows` после копирования файла |
|
||
| Адрес приватного VIP | `lb/topology.env`, `P2_VIP` / `P3_VIP` | то же |
|
||
|
||
«Пересборка образа» — это:
|
||
|
||
```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`.
|
||
|
||
---
|
||
|
||
## Диагностика
|
||
|
||
**Клиент сегмента не получает ответ от приватного 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` перезапускается.**
|
||
|
||
```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
|
||
```
|