# Потоки данных: балансировка приватного трафика **Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP) **Область:** путь пакета от клиента внутри приватного сегмента на приватный листенер (`10.20.0.100:80` или `10.30.0.100:80`) и обратно. **Смежный документ:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md) — балансировка публичного трафика. **Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP3_SUMMARY.md](STEP3_SUMMARY.md), [STEP5_SUMMARY.md](STEP5_SUMMARY.md). Отличие от публичного случая: клиент может стоять **в одном сегменте с бэкендами**. Тогда ответ бэкенда уходит клиенту по L2 напрямую, минуя маршрутизацию, и снять с него трансляцию можно только там, где этот кадр виден. Поэтому в приватном сегменте узел работает **коммутатором**: каждая ВМ подключена отдельным портом моста. Предпосылка всей схемы: **порт ВМ — порт OVS**. В целевой среде это выполняется по построению (ВМ подключены к vSwitch), в стенде порты создаёт `scripts/attach-segment.sh` (`make attach`). При этом в ОС виртуальных машин не настраивается ничего, адресация заказчика не меняется, а трафик внутри сегмента не блокируется. ## 1. Участники сегмента ```mermaid flowchart LR subgraph BR["br-lb — OpenFlow datapath"] direction TB subgraph S2["сегмент 2 — 10.20.0.0/24 · reg0 = 2"] P2["p2 · 2
аплинк к шлюзу Docker"] HC2["hcif-p2 · 4
10.20.0.253 в ядре"] B1["be1 · 10"] B3["be3 · 11"] C2["cli2 · 12"] end end CLI2["cli2 10.20.0.10
клиент: настроен только адрес"] BE1["be1 10.20.0.2:8080"] BE3["be3 10.20.0.3:8080"] GW["шлюз Docker 10.20.0.254
egress бэкендов"] OTHER["сегмент 3 — 10.30.0.0/24
be2, be4, cli3"] C2 <--> CLI2 B1 <--> BE1 B3 <--> BE3 P2 <--> GW S2 -.->|"маршрутизация
таблицы 20/21"| OTHER ``` Шлюз сегмента `10.20.0.1` и VIP листенера `10.20.0.100` отвечают **одним MAC** — `02:42:0a:14:00:01`. По нему пайплайн отличает трафик к маршрутизатору от трафика между ВМ. Сегмент 3 устроен симметрично. Какие сегменты имеют листенер, задаёт `PRIV_LISTENERS` в `topology.env`: пусто — ни одного, `"p2"` — один, `"p2 p3"` — оба. ## 2. Конвейер таблиц приватного пути ```mermaid flowchart TD IN(["Кадр с порта ВМ"]) --> T0["0 сегмент по in_port → reg0
learn FDB (eth_src → порт)
learn adjacency (для адресов своей подсети)"] T0 --> T1{"1 классификация
по MAC назначения"} T1 -->|"arp"| T5["5 ARP-респондер
за VIP, шлюз, .253"] T1 -->|"icmp echo на VIP
или шлюз"| T6["6 ICMP-респондер"] T1 -->|"dl_dst = MAC шлюза
+ nw_dst = VIP:80"| T10 T1 -->|"dl_dst = MAC шлюза
прочий IP"| T15["15 обратный путь L3
ct(nat) — для сессий
из другого сегмента"] T1 -->|"прочее —
трафик между ВМ"| T24 T5 -->|"не наш адрес"| T25 T10{"10 листенер
tcp VIP:80"} --> T11["11 таблица слотов
reg1 → reg2 (общая
с публичным листенером)"] T11 -->|"пул пуст"| DROPF(["drop — fail-close"]) T11 --> T12{"12 применение члена
ct(commit,nat)"} T12 -->|"член в сегменте клиента
без dec_ttl, минуя таблицу 20"| T21 T12 -->|"член в другом сегменте"| T20["20 маршрутизация
dec_ttl"] T20 --> T21["21 adjacency
MAC члена + порт —
резолвит hcd через ARP
ядра (шаг 5), единое
место для обоих путей"] T21 --> OUTR(["output: порт члена"]) T15 --> T16["16 пост-ct"] --> T20 T24{"24 обратная трансляция
сегмента"} T24 -->|"nw_src = член, tp_src = 8080
ct(nat): src → VIP:80"| T25 T24 -->|"прочее — без изменений"| T25 T25{"25 коммутация
FDB по eth_dst"} --> OUTL(["output: порт ВМ"]) T25 -->|"broadcast или
неизвестный MAC"| FLOOD(["рассылка по сегменту,
кроме входного порта"]) classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff class DROPF d ``` Таблица 21 — единственное место, где применяется MAC следующего перехода, для обоих путей сразу: и для члена в сегменте клиента (сюда ведёт таблица 12 напрямую, без `dec_ttl`), и для члена в другом сегменте (сюда приходят из таблицы 20, уже после `dec_ttl`). MAC члена не дублируется между таблицами. ## 3. Основной поток: клиент и член пула в одном сегменте Тот самый случай, ради которого сегмент переведён на порты OVS. ```mermaid sequenceDiagram autonumber participant C as cli2 10.20.0.10 participant OF as br-lb participant B as be1 10.20.0.2 C->>OF: ARP who-has 10.20.0.100 Note over OF: t1 → t5: респондер за VIP OF-->>C: is-at 02:42:0a:14:00:01 (MAC шлюза сегмента) C->>OF: SYN 10.20.0.10 → 10.20.0.100:80, TTL 64 Note over OF: t0: reg0=2, обучение FDB и adjacency Note over OF: t1: dl_dst = MAC шлюза, nw_dst = VIP → листенер Note over OF: t10 → t11: слот → член пула Note over OF: t12: ct(commit, nat(dst=10.20.0.2:8080))
→ t21 напрямую, минуя t20 Note over OF: t21: mod_dl_dst = MAC be1 (резолвлен hcd
через ARP ядра, шаг 5), output порт be1
dec_ttl не выполнялся OF->>B: пакет в порт be1 Note over B: client=10.20.0.10 — исходный адрес клиента
served=10.20.0.2:8080 — трансляция 80 → 8080 B->>OF: ответ src=10.20.0.2:8080 → 10.20.0.10,
dst MAC = MAC клиента (сосед по L2) Note over OF: t1: dl_dst ≠ MAC шлюза → t24 Note over OF: t24: ct(nat) → src = 10.20.0.100:80 OF->>C: t25: FDB → порт cli2 Note over C: ответ от 10.20.0.100:80, TTL 64 ``` Обратный кадр не адресован балансировщику вовсе — бэкенд отправляет его соседу напрямую. Через мост он проходит только потому, что порт ВМ — порт OVS. Именно здесь, в таблице 24, снимается трансляция. Для ВМ ничего не изменилось: TTL не уменьшен, значит клиент и бэкенд по-прежнему L2-соседи, а балансировщик не стал для них маршрутным хопом. ## 4. Второй поток: член пула в другом сегменте Тот же листенер, но слот выбрал члена из соседнего сегмента — работает обычная маршрутизация. ```mermaid flowchart LR C["cli2
10.20.0.10"] -->|"dst=10.20.0.100:80"| A["t0/t1: reg0=2,
dl_dst = MAC шлюза"] A --> B["t10 → t11 → t12
reg2 указывает на be2:
DNAT 10.30.0.2:8080"] B --> D["t20: LPM /24 10.30.0.0
dec_ttl, src MAC = p3"] D --> E["t21: dst MAC = be2,
output порт be2"] E --> BE2["be2
client=10.20.0.10"] BE2 -->|"маршрут 10.20.0.0/24
via 10.30.0.1"| F["t1: dl_dst = MAC шлюза → t15
ct(nat): src → 10.20.0.100:80"] F --> G["t20 → t21: adjacency клиента,
выученная в t0"] --> C ``` Клиенту оба пути неотличимы — ответ приходит от `10.20.0.100:80`. Разница видна только по TTL: **64** от соседа по сегменту против **63** через маршрутизацию. ## 5. Остальной трафик сегмента Балансировка не мешает обычной работе сегмента: всё, что не адресовано VIP, коммутируется без трансляции. ```mermaid flowchart TD A["cli2 → be1:8080 напрямую,
минуя VIP"] --> B["t1: dl_dst = MAC be1 → t24"] B --> C["t24: ct без записи —
изменений нет"] --> D["t25: FDB → порт be1"] E["be1 → be3:8080
внутри сегмента"] --> F["t1 → t24 → t25"] F --> G["t25: FDB → порт be3"] H["be1 → 1.1.1.1
default via 10.20.0.254"] --> I["t1 → t24 → t25"] I --> J["t25: FDB → аплинк p2 →
docker-бридж → NAT хоста
профиль A: мимо балансировки"] K["ARP между ВМ"] --> L["t5: не наш адрес → t25"] L --> M["t25: рассылка по портам сегмента"] N["health-проба
10.20.0.253 → be1:8080"] --> O["t0: in_port = hcif-p2
t1 → t24 → t25 → порт be1"] ``` Про пробы: они уходят с уникального адреса узла `.253`, а не с VIP. Если бы источником был VIP, ответ бэкенда попал бы в обратную трансляцию и до пробера не дошёл. Правило таблицы 24 ответ на пробу видит, но `ct` записи для него не находит и пропускает без изменений. ## 6. Таблица решений приватного пути | Таблица | Матч | Действие | Комментарий | |---|---|---|---| | 0 | `in_port` = порт сегмента | `reg0` + `learn` FDB | мост становится обучающимся коммутатором | | 0 | то же, `ip`, `nw_src` из подсети сегмента | + `learn` adjacency | нужно для возврата ответов из другого сегмента; с аплинка не обучаем, иначе в t21 попал бы внешний мир | | 1 | `dl_dst` = MAC шлюза, `nw_dst` = VIP, `tp_dst` = 80 | → t10 | листенер сегмента | | 1 | `dl_dst` = MAC шлюза, `ip`, `nw_dst` = VIP | drop | адрес занят, листенер выключен — видно по счётчику | | 1 | `dl_dst` = MAC шлюза, `ip` | → t15 | транзит и обратный путь L3 | | 1 | прочее | → t24 | трафик между ВМ | | 5 | `arp_tpa` = VIP, шлюз или `.253` | ответ `IN_PORT` | MAC-ом порта сегмента | | 5 | прочий ARP | → t25 | ВМ резолвят друг друга сами | | 10 | `tcp`, `nw_dst` = VIP сегмента | `multipath` → t11 | тот же `pool_id` и хэш, что у публичного | | 12 | `reg0` = сегмент члена | `ct(commit,nat)` → t21 напрямую | **без `dec_ttl`**, минуя t20 | | 12 | прочее | `ct(commit,nat)` → t20 | член из другого сегмента | | 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5) — единое место для обоих путей | | 24 | `nw_src` = член, `tp_src` = 8080 | `ct(nat)` → t25 | снимает DNAT с ответа соседу | | 24 | prio 0 | → t25 | обычный трафик проходит нетронутым | | 25 | `eth_dst` из FDB | `output` | записи `learn`, `idle_timeout` 300 с | | 25 | broadcast или неизвестный MAC | рассылка по сегменту | порт входа OVS исключает сам | ## 7. Где поток обрывается | Точка | Причина | Как увидеть | |---|---|---| | t0 prio 0 | кадр с порта, не отнесённого к сегменту — ВМ не подключена | `make ports`, затем `make attach` | | t1 prio 125 | обращение на VIP при выключенном листенере | `lbctl flows 1` | | t11 prio 0 | пул без живых членов — fail-close | `make slots` | | t20 prio 0 | назначение без маршрута | `lbctl flows 20 \| grep priority=0` | | t21 prio 0 | adjacency клиента неизвестна — запись `learn` истекла | `lbctl flows 21` | | t25 prio 0 | кадр без сегмента в `reg0` | `make fdb` | ## 8. Осмотр ```bash make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # путь сессии по таблицам make fdb # выученные MAC клиентов и счётчики рассылки make neigh # MAC членов пула, резолвленный hcd через ARP make ports # подключены ли ВМ сегментов docker compose exec lb-router lbctl flows 24 # счётчики обратной трансляции docker compose exec cli2 curl -s http://10.20.0.100/ ``` В `Final flow` трассировки коммутируемого пути `nw_src` остаётся клиентским, `nw_ttl` не уменьшен, а вывод происходит из таблицы 25 в порт члена. ## 9. Ограничения приватного пути - **Порты ВМ должны быть портами OVS.** Если между клиентом и бэкендом окажется коммутатор, который SDN не контролирует, ответ бэкенда мост не увидит, и кейс не решается ничем, кроме SNAT — то есть с потерей IP клиента. - **Весь внутрисегментный трафик проходит через узел.** Пропускная способность узла становится пропускной способностью сегмента. - **Обратная трансляция опирается на `ct`.** Stateless-вариант (`nw_src=member, tp_src=8080 → VIP:80`) потребует аккуратного исключения трафика, который бэкенд сам инициирует с порта 8080. - **FDB без старения короче 300 с и без ограничения на число MAC** в сегменте. - **Неизвестный unicast и unicast-ARP рассылаются флудом** — обычное поведение обучающегося коммутатора. - **В стенде порты ВМ исчезают при пересоздании контейнера** — нужен `make attach`. В целевой среде порты создаёт гипервизор. - **Только TCP и только IPv4**; ICMP-ошибки и фрагменты — как и в публичном пути, вне рамок.