Добавлена локальная локальная балансировка

This commit is contained in:
ayurishchev committed 2026-08-17 16:59:16 +03:00
1 parent afd4f057be
commit d80a6c44b0
17 files changed
+1628 -369

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
# Шаг 3 — итоги: листенер в приватном сегменте
**Дата:** 2026-08-17
**Статус:** выполнено, проверено после холодного перезапуска (86 из 86 проверок)
**План:** [STEP3_IMPLEMENTATION_PLAN.md](STEP3_IMPLEMENTATION_PLAN.md)
**Предыдущий шаг:** [STEP2_SUMMARY.md](STEP2_SUMMARY.md)
## Что сделано
Балансировщик получил листенеры в приватных сегментах — `10.20.0.100:80` и
`10.30.0.100:80` — и научился обслуживать клиента, который стоит **в одном
сегменте с бэкендами**. Исходный IP клиента сохраняется, порт транслируется
80 → 8080, в ОС виртуальных машин не настраивается ничего.
- **`br-lb` стал коммутатором приватного сегмента.** Каждая ВМ подключена
отдельным портом (`scripts/attach-segment.sh`), порты `p2`/`p3` остались
аплинками сегмента к шлюзу Docker. Появились таблицы 0 (сегментация и
обучение), 25 (FDB, broadcast и unknown flood) и 1 (классификация).
- **Обратная трансляция коммутируемого трафика — таблица 24.** Ответ бэкенда
клиенту-соседу уходит по L2 напрямую, но проходит через мост, где `ct(nat)`
возвращает источник к `VIP:80`.
- **Выдача на члена своего сегмента — без маршрутизации.** Таблица 12 при
совпадении сегмента клиента и члена меняет только MAC назначения и отдаёт кадр
в коммутацию: `dec_ttl` не выполняется, клиент и бэкенд остаются L2-соседями.
- **Пул общий с публичным листенером** — та же таблица слотов, тот же `pool_id`,
тот же дайджест `0a16713c5a9eb94d`. Демон `hcd` не изменён ни строкой.
- **Переключатель `PRIV_LISTENERS`** в `topology.env`: пусто — приватных
листенеров нет, `"p2"` — один сегмент, `"p2 p3"` — оба.
- **Клиенты сегментов** `cli2` (`10.20.0.10`) и `cli3` (`10.30.0.10`) — ВМ без
единого маршрута в таблице маршрутизации.
## Почему именно так
Клиент и бэкенд — L2-соседи, поэтому ответ бэкенда уходит клиенту напрямую.
Снять DNAT можно только там, где этот кадр виден. Остальные пути отпадали по
условиям задачи:
| Вариант | Почему отвергнут |
|---|---|
| SNAT | уничтожает сохранение IP клиента |
| DSR, VIP на `lo` бэкенда | требует настройки ОС ВМ, не транслирует порт |
| Изоляция портов сегмента | блокирует внутрисегментный трафик, неприменима к ВМ |
| Дробление подсети маршрутами `/25` | вмешательство в ОС ВМ и в адресацию заказчика |
Осталась единственная предпосылка, совпадающая с зоной влияния SDN: **порты ВМ
— порты OVS**. В целевой среде это выполняется по построению; стенд приведён к
той же модели.
## Результаты проверок
| Проверка | Результат |
|---|---|
| ВМ сегментов подключены портами OVS | 6 портов: be1, be3, cli2, be2, be4, cli3 |
| `ping 10.20.0.100` из `cli2` | отвечают ARP- и ICMP-респондеры OVS |
| Балансировка из приватного клиента | 24 запроса, задействованы все четыре члена |
| **IP клиента на бэкенде** | `client=10.20.0.10` во всех ответах |
| **Трансляция порта** | `served=10.20.0.2:8080` при листенере на 80 |
| Член в сегменте клиента | обслуживает запросы, ответ транслируется таблицей 24 |
| **TTL ответа от VIP** | 64 для членов своего сегмента, 63 — для членов другого |
| В ОС клиента нет маршрутов | `ip route` содержит только connected-запись |
| Внутрисегментная связность | `cli2 → be1` напрямую, `be1 → be3`, шлюз Docker — всё работает |
| Отказ члена под нагрузкой из `cli2` | слоты разошлись 341/342/341, 2 ошибки в окне обнаружения |
| Профиль A | egress бэкенда адресован шлюзу Docker, не маршрутизатору сегмента |
| `PRIV_LISTENERS="p2"` | листенер в priv3 выключен (счётчик drop растёт), связность цела |
| Регрессия шагов 1–2 | публичный листенер, транзит, health-check, fail-close, дренаж — без изменений |
## Главное измерение: TTL как признак пути
Ответы от `be1`/`be3` (сегмент клиента) приходят с **TTL 64**, от `be2`/`be4`
(другой сегмент) — с **TTL 63**. Это прямое доказательство того, что для соседей
по сегменту балансировщик не стал L3-хопом: он подменил MAC назначения,
оттранслировал адрес и отдал кадр коммутацией. С точки зрения ВМ ничего не
изменилось — она общается с соседом по подсети, как и раньше.
Для членов из другого сегмента работает прежний маршрутизируемый путь с
`dec_ttl`. Оба пути живут на одном пуле и одной таблице слотов, и клиент их не
различает.
## Отклонения от плана
1. **`load:0->NXM_OF_IN_PORT[]` не понадобился.** План предполагал hairpin —
выдачу в порт входа — и сброс `in_port`, чтобы OVS не подавил вывод. После
перевода каждой ВМ на собственный порт hairpin исчез: клиент и бэкенд входят
и выходят через разные порты. Риск снят самой архитектурой.
2. **Отдельная сеть для egress не потребовалась.** План предусматривал служебную
docker-сеть под default-маршрут. Оказалось достаточно оставить `p2`/`p3`
аплинками сегмента: бэкенд адресует кадр MAC-у шлюза Docker, мост его
коммутирует, и профиль A сохраняется в прежнем виде.
3. **Появилась таблица 1.** В плане классификация оставалась в таблице 0. Но
таблица 0 занята определением сегмента и обучением (два действия `learn` на
каждый порт), поэтому классификация вынесена отдельно — иначе правила
пришлось бы дублировать на каждый порт сегмента.
4. **`make` больше не требует `sudo`.** Под root его в системе может не быть;
цели подготовки хоста теперь подставляют `sudo` только при запуске не от root.
5. **Проверки 7 и 9 в `verify.sh` переформулированы.** Обе слушали аплинк `p2`,
через который health-пробы и egress больше не проходят: пробы идут
коммутацией на порт ВМ, а признаком профиля A стал MAC назначения (шлюз
Docker, а не маршрутизатор сегмента), а не отсутствие трафика на `p2`.
## Известные ограничения
- **Предпосылка архитектуры:** порты ВМ должны быть портами OVS. Если между
клиентом и бэкендом окажется коммутатор, который SDN не контролирует, кейс не
решается ничем, кроме SNAT.
- **Хрупкость стенда:** veth, созданные `attach-segment.sh`, исчезают при
пересоздании контейнера — нужен `make attach`. В целевой среде порты создаёт
гипервизор.
- **Весь внутрисегментный трафик проходит через `br-lb`** — пропускная
способность узла становится пропускной способностью сегмента. Замер k6 не
выполнен: в системе нет клиента k6; цель для сравнения —
`http://10.20.0.100/` против `http://192.168.5.21/`.
- **Обратная трансляция опирается на `ct`** — как и прежде. Stateless-вариант
(`nw_src=member, tp_src=8080 → VIP:80`) остаётся заделом: в сегменте он
потребует аккуратного исключения трафика, который бэкенд сам инициирует с
порта 8080.
- **Unicast ARP внутри сегмента рассылается флудом**, если MAC ещё не выучен —
обычное поведение обучающегося коммутатора, но без таймера старения записей
короче 300 с.
- **Приватный листенер только TCP и только IPv4**, как и публичный. UDP- и
L3-листенеров нет.
- Кворум, control plane в OVSDB, ICMP-ошибки и фрагменты — по-прежнему вне рамок.
## Задел на шаг 4
1. **Stateless-датапас** для обоих путей: явный un-DNAT вместо `ct(nat)`.
На коммутируемом пути это снимет последнюю зависимость от состояния.
2. **Control plane:** модель `Load_Balancer → Listener → Pool → Member` в OVSDB
вместо `topology.env`. Сейчас листенер, сегмент и состав пула описаны в трёх
местах, а добавление ВМ требует правки `topology.env`, `hc-config.sh` и
запуска `attach-segment.sh`.
3. **Старение FDB и защита от переполнения** — сейчас записи живут 300 с без
ограничения на число MAC в сегменте.
4. Второй узел LB: кворум наблюдений, генерации пулов, сравнение дайджестов.