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>
220 lines
20 KiB
Markdown
220 lines
20 KiB
Markdown
# Шаг 4 — оценка: публичная балансировка без conntrack
|
||
|
||
**Дата:** 2026-08-18
|
||
**Статус:** оценка осуществимости (не план внедрения и не реализация)
|
||
**Область:** только публичный листенер `192.168.5.21:80`
|
||
**Источник:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md), `lb/pipeline.sh`
|
||
(таблицы 12, 15, 16)
|
||
**Смежные документы:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md),
|
||
[MAGLEV_SLOT_TABLE.md](MAGLEV_SLOT_TABLE.md)
|
||
|
||
## Задача
|
||
|
||
Оценить переход публичного пути с текущей stateful-схемы (`ct(commit,nat)` на
|
||
прямом пути, `ct(nat)` на обратном) на схему без conntrack. Главное условие,
|
||
заданное на входе: **бэкенд обязан видеть реальный IP клиента** — адрес, с
|
||
которым клиент обратился на балансировщик, до всех преобразований.
|
||
|
||
Уточнение по ходу обсуждения: клиент **всегда** обращается на адрес
|
||
балансировщика (VIP), не на бэкенд напрямую. Он должен получить ответ, который
|
||
его TCP-стек и приложение примут как продолжение своего же соединения — то есть
|
||
`src` ответа обязан остаться `VIP:80`.
|
||
|
||
## Вывод
|
||
|
||
Технически осуществимо для нового трафика через публичный листенер, при двух
|
||
явных условиях периметра и одном принимаемом изменении поведения:
|
||
|
||
1. бэкенды не должны быть достижимы клиентами напрямую, в обход VIP (условие
|
||
периметра, не техническое ограничение датапаса);
|
||
2. правило обратной трансляции обязано быть сужено по адресу назначения,
|
||
иначе оно ломает трафик между бэкендами;
|
||
3. установленные сессии перестают быть защищены от смены состава пула — это
|
||
принимаемый компромисс, а не дефект.
|
||
|
||
## 1. Что меняется в датапасе
|
||
|
||
| Таблица | Сейчас | Предлагается |
|
||
|---|---|---|
|
||
| 12 (прямой DNAT) | `ct(commit, zone=1, nat(dst=member:8080))` | `mod_nw_dst:<member_ip>, mod_tp_dst:<member_port>` — без `ct` вообще |
|
||
| 15 (обратный путь) | одно правило `tcp actions=ct(table=16,zone=1,nat)` на весь трафик | по одному правилу на каждого члена: `tcp,nw_src=<member_ip>,tp_src=<member_port>,nw_dst=$PUB_NET actions=mod_nw_src:$VIP,mod_tp_src:$VIP_PORT,goto_table:16` |
|
||
| 16 | пост-`ct` — счётчики | не нужна для этого пути; сохраняется для транзита, где `ct` ещё используется |
|
||
|
||
`src` клиента в т.12 не трогается ни сейчас, ни в новой схеме — сохранение IP
|
||
клиента никогда не зависело от `ct`, поэтому по этой части поведение не
|
||
меняется вообще.
|
||
|
||
Обоснование замены в т.15 — §2.2 дизайна v1: DNAT переписывает адрес назначения
|
||
в фиксированный `member_ip:member_port`, а не в случайный порт, как SNAT.
|
||
Поэтому пара `(member_ip, member_port, proto)` однозначно определяет
|
||
`(VIP, listener_port)`, и обратную трансляцию можно сделать статическим
|
||
правилом без какого-либо состояния.
|
||
|
||
## 2. Условие периметра: прямой доступ к бэкенду должен быть закрыт
|
||
|
||
Таблица 1 отправляет в т.15 весь трафик с `dl_dst = MAC шлюза сегмента`, не
|
||
различая, был ли пакет частью балансируемой сессии. Сегодняшний стенд умеет и
|
||
поддерживает прямой доступ клиента к бэкенду в обход VIP (`table=10,
|
||
priority=100,ip actions=goto_table:20`, проверяется в `verify.sh` секция 5 и
|
||
`MANUAL_TEST_PLAN` блок C.2) — ответ на такое обращение и ответ на сессию через
|
||
VIP дают на выходе бэкенда идентичные заголовки (`src=member_ip:8080,
|
||
dst=client_ip:client_port`), различить их без состояния невозможно.
|
||
|
||
При заданном требовании — клиент всегда обращается на VIP — эта коллизия не
|
||
относится к реальному клиентскому трафику. Она становится значимой только если
|
||
прямой доступ к порту бэкенда остаётся разрешён одновременно со stateless-схемой.
|
||
Вывод: **предпосылка корректности схемы — бэкенды недостижимы клиентами
|
||
напрямую**, что и так является типовой практикой для облачных LB (изоляция
|
||
периметра, security groups), а не новым ограничением, придуманным ради этого
|
||
перехода.
|
||
|
||
## 3. Связность между бэкендами
|
||
|
||
Уточняющий вопрос по ходу оценки: не потеряют ли бэкенды способность общаться
|
||
друг с другом. Ответ разделяется по топологии.
|
||
|
||
**Бэкенды в разных сегментах.** Ответ `be1 → be2:8080` (транзит между
|
||
сегментами) идёт через ту же таблицу 15, что и обратный трафик клиентских
|
||
сессий, потому что оба используют один и тот же MAC шлюза сегмента как
|
||
классификатор. Правило `nw_dst=$PUB_NET` в предложенном un-DNAT решает это:
|
||
адресные пространства клиентов (`192.168.5.0/24`) и бэкендов (приватные
|
||
сегменты) не пересекаются, поэтому транзитный трафик просто не попадает под
|
||
условие правила и проходит немодифицированным. **Связность между сегментами
|
||
сохраняется**, если это условие включено в правило.
|
||
|
||
**Бэкенды в одном сегменте.** Это территория приватного пути (таблица 24,
|
||
шаг 3), не публичного, и в этой оценке не менялась — она по-прежнему на `ct`.
|
||
Если в будущем этот путь тоже переводить на stateless-схему, префиксный
|
||
критерий не сработает: в шаге 3 клиент и бэкенды намеренно живут в одной
|
||
подсети, поэтому ответ `be3 → be1` и ответ `be3 → реальный клиент cli2`
|
||
неотличимы по заголовкам. Решение для этого случая другое — не диапазон
|
||
адресов, а явный список исключений (для каждого члена сегмента: «если
|
||
назначение — адрес другого известного члена этого же сегмента, не
|
||
транслировать»), которые должны иметь приоритет выше общего правила un-DNAT.
|
||
Список членов конечен и известен заранее, в отличие от адресов произвольных
|
||
клиентов, поэтому это решаемо, но требует отдельного набора правил и не входит
|
||
в объём этой оценки (она ограничена публичным путём).
|
||
|
||
## 4. Изменение поведения: установленные сессии больше не защищены
|
||
|
||
Сейчас `ct(commit)` закрепляет привязку за соединением на весь срок его жизни
|
||
независимо от таблицы слотов — это подтверждено в `STEP2_SUMMARY.md`: сессия,
|
||
обслуживаемая выведенным из пула членом, не рвётся (`verify.sh` секция 13,
|
||
`MANUAL_TEST_PLAN` блок I).
|
||
|
||
Без `ct` трансляция каждого пакета определяется исключительно текущим
|
||
состоянием таблицы слотов. При выводе члена из пула следующий пакет уже
|
||
установленной к нему сессии уйдёт на другого члена — без контекста
|
||
TCP-соединения, и сессия оборвётся. Минимальное возмущение Maglev ограничивает
|
||
**долю** пострадавших сессий (переезжают только слоты выбывшего члена, у
|
||
остальных ничего не меняется), но не защищает сами эти сессии.
|
||
|
||
Это ровно тот компромисс, который был предсказан заранее в `STEP2_SUMMARY.md`:
|
||
«ценность таблицы слотов раскроется при переходе на stateless-датапас, где
|
||
единственной защитой от переезда сессий останется минимальное возмущение
|
||
раскладки». Переход на публичном пути — первое место, где это предсказание
|
||
становится проверяемым поведением, а не наблюдением.
|
||
|
||
Практическое следствие: тест «сессия переживает дренаж» (`verify.sh` секция 13)
|
||
для публичного пути перестаёт быть верным и требует пересмотра или явного
|
||
исключения из проверок при внедрении.
|
||
|
||
## 5. Узкое остаточное ограничение (из дизайна v1)
|
||
|
||
Если сам бэкенд когда-либо инициирует исходящее TCP-соединение с src-портом,
|
||
равным его собственному порту прослушивания (8080), и адресует его в
|
||
клиентскую сеть — такой пакет попадёт под правило un-DNAT и будет неверно
|
||
переписан. Маловероятный случай (обычно исходящие соединения используют
|
||
эфемерный порт источника), уже документирован в дизайне v1 (§3.2, «остаточное
|
||
ограничение», §15.4). Переход на stateless ничего не меняет в этом отношении —
|
||
ограничение существовало бы в любой чисто заголовочной схеме un-DNAT.
|
||
|
||
## 6. Что выигрывается
|
||
|
||
- `ct` и зависимость от `CT_ZONE` уходят из публичного пути целиком — оба
|
||
направления становятся чистым `match + rewrite`.
|
||
- Меньше recirculation в датапасе: на момент оценки в датапасе стенда
|
||
зафиксировано 6 потоков с `ct(...)`, каждый вызывает `recirc`; на публичном
|
||
пути они исчезают.
|
||
- Снимается ограничение, зафиксированное в `PUBLIC_LB_DATAFLOW.md` (раздел 9):
|
||
«прямой и обратный трафик сессии обязаны проходить через один узел». Это
|
||
предпосылка, без которой Active/Active ECMP между несколькими узлами LB не
|
||
имеет смысла обсуждать — устраняется она именно этим шагом.
|
||
|
||
## 7. Что не меняется
|
||
|
||
Маршрутизация (таблицы 20/21), таблица слотов (таблица 11), алгоритм Maglev,
|
||
детерминизм `multipath` — не затрагиваются вообще. Ограничения по ICMP-ошибкам
|
||
и фрагментации сохраняются в прежнем виде — они не были связаны с `ct` и
|
||
раньше.
|
||
|
||
## 8. Отдельный вопрос: доставка до физического сервера в EVPN-фабрике
|
||
|
||
Поднят по ходу обсуждения, зафиксирован здесь как связанный, но не
|
||
пересекающийся с темой `ct`.
|
||
|
||
Вопрос: если нода HPNN подключена к EVPN-фабрике, и MAC сетевого порта
|
||
физического сервера-бэкенда доходит до HPNN через фабрику — будет ли трафик
|
||
доставлен до сервера?
|
||
|
||
**Ответ: это не зависит от наличия или отсутствия `ct`.** Трансляция адреса
|
||
(таблицы 12/15) и доставка кадра до правильного MAC (таблица 21, adjacency) —
|
||
независимые механизмы. `ct` в доставке кадра не участвует ни сейчас, ни в
|
||
предложенной схеме.
|
||
|
||
Реальная зависимость — в существующей модели adjacency: MAC членов пула сейчас
|
||
**статически** прописаны в `topology.env` и заливаются в таблицу 21 константами
|
||
(`mod_dl_dst:$BE1_MAC`). Это уже задокументированное ограничение —
|
||
`STEP1_SUMMARY.md`: «MAC бэкендов прописаны статически. ARP-резолвер next-hop
|
||
отсутствует». Дизайн v1 предполагал агента, резолвящего MAC next-hop через
|
||
packet-in/packet-out по своей neigh-таблице (§4.3, §8.2), но это не
|
||
реализовано ни в стенде, ни где-либо ещё.
|
||
|
||
То, что MAC физического порта доходит до HPNN по EVPN-фабрике, — свойство самой
|
||
фабрики (leaf-коммутаторы разучивают MAC локально, публикуют по EVPN Type-2
|
||
остальным VTEP); HPNN в этом процессе не участвует, для него это просто кадры
|
||
на порту аплинка. Вопрос не в том, дойдёт ли MAC до HPNN (дойдёт), а в том,
|
||
знает ли пайплайн HPNN, какой MAC подставить в `mod_dl_dst`, когда решает
|
||
отправить пакет этому серверу. При текущей статической модели — знает только
|
||
то, что вручную прописано заранее. Если реальный MAC карты сервера не совпадает
|
||
с прописанным (замена карты, переключение LACP/бондинга на другой физический
|
||
линк, сервер ещё не был известен на момент конфигурации) — таблица 21 либо
|
||
отправит кадр не туда, либо, если совпадения нет вовсе, попадёт под
|
||
`priority=0 actions=drop`: трафик до сервера не дойдёт, несмотря на то что
|
||
фабрика прекрасно знает его настоящий MAC.
|
||
|
||
Это существующий пробел архитектуры, не имеющий отношения к переходу на схему
|
||
без conntrack — он не блокирует и не усложняет этот шаг. Требует отдельной
|
||
оценки: динамический ARP-резолвер next-hop (как и предполагал дизайн v1) либо
|
||
интеграция с тем, как EVPN-фабрика публикует MAC-привязки, чтобы control plane
|
||
HPNN получал их не вручную, а из фабрики.
|
||
|
||
> **Обновление (шаг 5, [STEP5_SUMMARY.md](STEP5_SUMMARY.md)):** первая часть
|
||
> этого пробела закрыта — `hcd` резолвит MAC членов пула динамически через
|
||
> обычный ARP ядра, статических `BE*_MAC` в таблице 21 больше нет. Это не
|
||
> решает вопрос EVPN-фабрики целиком: `hcd` резолвит MAC в пределах L2-домена,
|
||
> видимого узлу напрямую (те же hcif-порты, что и для health-проб), а не через
|
||
> интеграцию с EVPN Type-2. Если фабрика доставляет сегмент до узла как один
|
||
> L2-домен (что и предполагает текущая архитектура), тот же ARP-путь работает
|
||
> без изменений; отдельная интеграция с control plane EVPN остаётся вне рамок.
|
||
|
||
## 9. Итоговая сводка условий и рисков
|
||
|
||
| # | Пункт | Тип | Статус |
|
||
|---|---|---|---|
|
||
| 1 | Бэкенды недостижимы клиентами напрямую, только через VIP | предпосылка периметра | требует явного решения при внедрении |
|
||
| 2 | Правило un-DNAT сужено `nw_dst=$PUB_NET` | техническое условие | зафиксировано в предложенной схеме |
|
||
| 3 | Связность между сегментами (be↔be, разные сегменты) | проверено | сохраняется при условии (2) |
|
||
| 4 | Связность внутри сегмента (be↔be, один сегмент) | вне объёма этой оценки | не тронуто (путь на `ct`); при переносе на stateless требует отдельного набора исключений |
|
||
| 5 | Установленные сессии при смене состава пула | изменение поведения | принимаемый компромисс, требует пересмотра теста `verify.sh` §13 |
|
||
| 6 | Исходящий трафик бэкенда с src-портом = 8080 в клиентскую сеть | остаточное ограничение | узкое, уже документировано в дизайне v1 |
|
||
| 7 | Доставка до физического сервера по MAC (EVPN) | отдельный вопрос | не связан с `ct`; существующий пробел модели adjacency |
|
||
|
||
## 10. Что дальше
|
||
|
||
Это оценка осуществимости, не план внедрения. Следующий шаг, если решение
|
||
принято, — оформить отдельный план и итоги по конвенции репозитория
|
||
(`STEP4_IMPLEMENTATION_PLAN.md` / `STEP4_SUMMARY.md`), с явной фиксацией
|
||
решений по пунктам 1 и 5 таблицы выше как условий приёмки, и отдельной оценкой
|
||
пункта 7 при планировании физического окружения.
|