Files
hpnn-proto/docs/PUBLIC_LB_DATAFLOW.md
T

194 lines
11 KiB
Markdown
Raw Normal View History

# Потоки данных: балансировка публичного трафика
**Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP)
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
`192.168.5.21:80` и обратно.
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
балансировка внутри приватного сегмента.
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP5_SUMMARY.md](STEP5_SUMMARY.md).
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
разных сегментах, узел выступает L3-хопом в обе стороны, TTL уменьшается на
единицу на каждом направлении. Сетевой стек ядра контейнера в передаче трафика
не участвует — все решения принимает OpenFlow.
## 1. Участники
```mermaid
flowchart LR
CL["Клиент<br/>192.168.5.13"]
subgraph BR["br-lb — OpenFlow datapath"]
direction TB
PUB["<b>pub0 · ofport 1</b><br/>macvlan @ enp3s0<br/>02:42:c0:a8:05:14<br/><i>адрес узла 192.168.5.20</i><br/><i>VIP 192.168.5.21:80</i>"]
B1["be1 · 10"]
B3["be3 · 11"]
B2["be2 · 20"]
B4["be4 · 21"]
end
BE1["be1 10.20.0.2:8080"]
BE3["be3 10.20.0.3:8080"]
BE2["be2 10.30.0.2:8080"]
BE4["be4 10.30.0.3:8080"]
CL <-->|"L2 клиентской сети"| PUB
B1 <--> BE1
B3 <--> BE3
B2 <--> BE2
B4 <--> BE4
```
Адреса `192.168.5.20` и `192.168.5.21` существуют только в правилах OpenFlow —
на интерфейсе они не настроены, за них отвечает ARP-респондер MAC-адресом порта
`pub0`.
## 2. Конвейер таблиц публичного пути
```mermaid
flowchart TD
IN(["Кадр с pub0"]) --> T0["<b>0</b> сегмент по in_port<br/>reg0 = 1"]
T0 --> T1{"<b>1</b> классификация"}
T1 -->|"arp"| T5["<b>5</b> ARP-респондер<br/>за 192.168.5.20 и .21"]
T1 -->|"icmp echo на<br/>адрес узла или VIP"| T6["<b>6</b> ICMP-респондер"]
T1 -->|"ip на VIP, адрес узла<br/>или приватные сети<br/>+ <b>learn</b> adjacency"| T10
T1 -->|"прочий IP"| DROP1(["drop"])
T10{"<b>10</b> листенер<br/>tcp VIP:80"}
T10 -->|"multipath(symmetric_l4)<br/>→ reg1 = слот"| T11["<b>11</b> таблица слотов<br/>reg1 → reg2<br/><i>ведёт hcd</i>"]
T10 -->|"IP на VIP без<br/>листенера"| DROPV(["drop"])
T10 -->|"прочий IP: клиент<br/>обратился к бэкенду<br/>напрямую"| T20
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
T11 --> T12["<b>12</b> DNAT<br/>ct(commit, nat(dst=member:8080))"]
T12 --> T20
T20{"<b>20</b> маршрутизация<br/>LPM по приоритету"}
T20 -->|"dec_ttl,<br/>mod_dl_src = MAC сегмента"| T21["<b>21</b> adjacency<br/>MAC члена + его порт"]
T20 -->|"нет маршрута"| DROP20(["drop"])
T21 --> OUT(["output: порт члена пула"])
classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff
class DROP1,DROPV,DROPF,DROP20 d
```
Обратный путь идёт по другим таблицам — см. раздел 4.
## 3. Прямой путь: клиент → VIP → бэкенд
```mermaid
sequenceDiagram
autonumber
participant C as Клиент 192.168.5.13
participant P as pub0 · 1
participant OF as Таблицы OpenFlow
participant B as be1 10.20.0.2
C->>P: ARP who-has 192.168.5.21
P->>OF: t1 → t5 ARP-респондер
OF-->>C: is-at 02:42:c0:a8:05:14
C->>P: SYN 192.168.5.13 → 192.168.5.21:80, TTL 64
Note over OF: t0: reg0=1
Note over OF: t1: адресован стенду →<br/><b>learn</b>: adjacency клиента в t21
Note over OF: t10: multipath(symmetric_l4, basis=pool_id)<br/>→ reg1 = номер слота
Note over OF: t11: слот → reg2 = член пула
Note over OF: t12: ct(commit, nat(dst=10.20.0.2:8080))<br/><b>src не меняется</b>
Note over OF: t20: LPM /24 10.20.0.0 → dec_ttl 63,<br/>src MAC = MAC сегмента
Note over OF: t21: dst MAC = MAC be1, output порт be1
OF->>B: пакет
Note over B: client=192.168.5.13 — исходный адрес,<br/>served=10.20.0.2:8080 — после DNAT
```
Ключевое: **SNAT не выполняется**. Транслируется только адрес и порт
назначения, поэтому бэкенд видит реальный адрес клиента.
## 4. Обратный путь: бэкенд → клиент
Ответ обязан пройти через узел — иначе клиент получит пакет от `10.20.0.2:8080`
вместо `192.168.5.21:80` и разорвёт соединение. Это обеспечивается маршрутом на
бэкенде (профиль A, см. раздел 5).
```mermaid
flowchart LR
BE["be1 10.20.0.2:8080"] -->|"src=10.20.0.2 dst=192.168.5.13<br/>через шлюз сегмента 10.20.0.1"| A["порт be1 · 10"]
A --> B["t1: dl_dst = MAC шлюза<br/>сегмента → t15"]
B --> C["<b>t15</b>: ct(zone=1, nat)<br/><b>un-DNAT</b>: src → 192.168.5.21:80"]
C --> D["t16 → t20: LPM /24 192.168.5.0<br/>dec_ttl, src MAC = pub0"]
D --> E["t21 prio 90: MAC клиента<br/>из действия learn"]
E --> CL["Клиент видит ответ<br/>от 192.168.5.21:80"]
```
Трансляция снимается conntrack'ом: `ct(commit, nat)` на прямом пути закрепил
привязку за соединением, `ct(nat)` без commit на обратном её применяет в
обратную сторону. Пакеты, для которых записи нет (транзит между сегментами,
собственные обращения бэкендов), проходят без изменений.
## 5. Условие возврата: профиль A на бэкенде
```mermaid
flowchart TD
A["Таблица маршрутизации бэкенда"] --> B["192.168.5.0/24 via 10.20.0.1<br/><i>клиентский префикс — через узел</i>"]
A --> C["10.30.0.0/24 via 10.20.0.1<br/><i>соседний сегмент — через узел</i>"]
A --> D["default via 10.20.0.254<br/><i>шлюз Docker — мимо узла</i>"]
B --> E["Ответы на балансируемые сессии<br/>возвращаются на узел и<br/>проходят обратную трансляцию"]
D --> F["Собственный egress бэкенда<br/>(обновления, DNS, телеметрия)<br/>узел не маршрутизирует"]
```
Проверить: `docker compose exec be1 ip route`. Признак, что профиль A цел, —
кадр к внешнему адресу адресован MAC-у шлюза Docker, а не MAC-у маршрутизатора
сегмента (`02:42:0a:14:00:01`).
## 6. Таблица решений публичного пути
| Таблица | Матч | Действие | Комментарий |
|---|---|---|---|
| 0 | `in_port=1` | `reg0 = 1` | публичный сегмент; коммутации в нём нет |
| 1 | `arp` | → t5 | штатного ARP-стека у узла нет |
| 1 | `ip`, dst ∈ {VIP, `.20`, `10.20/24`, `10.30/24`} | `learn` + → t10 | обучение сужено, чтобы в t21 не попадал широковещательный шум LAN |
| 1 | прочий IP с `pub0` | drop | |
| 5 | `arp_tpa` = `.20` или `.21` | ответ `IN_PORT` | отвечает MAC-ом `pub0` |
| 10 | `tcp`, `nw_dst=VIP`, `tp_dst=80` | `multipath` → t11 | симметричный L4-хэш, basis = `pool_id` |
| 10 | `ip`, `nw_dst=VIP` (нет листенера) | drop | видно по счётчику |
| 10 | прочий IP | → t20 | прямое обращение клиента к бэкенду |
| 11 | `reg1` = слот | `reg2` = член | 1024 правила, заливает `hcd` |
| 11 | prio 0 | drop | fail-close: живых членов нет |
| 12 | `reg2` = член | `ct(commit, nat(dst=member:8080))` → t20 | без SNAT |
| 15 | `tcp` с порта сегмента, адресован шлюзу | `ct(nat)` → t16 | обратная трансляция |
| 20 | `/24` префиксы | `dec_ttl` + `mod_dl_src` | LPM через приоритет правила |
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5), не `topology.env` |
| 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с |
## 7. Где поток обрывается
| Точка | Причина | Как увидеть |
|---|---|---|
| t1 prio 100 | IP из публичного сегмента адресован не стенду | `lbctl flows 1` |
| t10 prio 150 | обращение на VIP, для которого нет листенера | `lbctl flows 10` |
| t11 prio 0 | пул без живых членов — fail-close | `make slots` |
| t20 prio 0 | назначение без маршрута (дефолта у узла нет) | `lbctl flows 20 \| grep priority=0` |
| t21 prio 0 | MAC клиента неизвестен — запись `learn` истекла | `lbctl flows 21` |
## 8. Осмотр
```bash
make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам
make neigh # MAC членов, резолвленный hcd через ARP
make slots # раскладка и per-member счётчики
make conns # записи ct
docker compose exec lb-router lbctl flows 21
```
## 9. Ограничения публичного пути
- **Обратный трафик обязан идти через узел.** Без маршрута на клиентский
префикс (профиль A) ответ уйдёт мимо и соединение не соберётся.
- **Датапас stateful**: обратную трансляцию делает `ct`. Прямой и обратный
трафик сессии обязаны проходить через один узел — Active/Active ECMP в такой
схеме не работает.
- **ICMP-ошибки не генерируются**: ни `TTL exceeded`, ни `fragmentation
needed`. PMTUD через стенд не работает, `traceroute` узел не покажет.
- **Фрагменты IP не обрабатываются** — правила матчат L4-заголовок.
- **Только TCP и только IPv4**; UDP- и L3-листенеров нет.
- **MAC клиента живёт 300 секунд** — после простоя первая же новая сессия
создаёт запись заново.