Files
hpnn-proto/docs/PUBLIC_LB_DATAFLOW.md
T

194 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Потоки данных: балансировка публичного трафика
**Дата:** 2026-08-17
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
`192.168.5.21:80` и обратно.
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
балансировка внутри приватного сегмента.
**Источник:** `lb/pipeline.sh`, `lb/topology.env`.
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
разных сегментах, узел выступает 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 из `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 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 секунд** — после простоя первая же новая сессия
создаёт запись заново.