2026-08-17 16:59:16 +03:00
|
|
|
|
# Потоки данных: балансировка публичного трафика
|
|
|
|
|
|
|
2026-08-19 12:33:48 +03:00
|
|
|
|
**Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP)
|
2026-08-17 16:59:16 +03:00
|
|
|
|
**Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер
|
|
|
|
|
|
`192.168.5.21:80` и обратно.
|
|
|
|
|
|
**Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) —
|
|
|
|
|
|
балансировка внутри приватного сегмента.
|
2026-08-19 12:33:48 +03:00
|
|
|
|
**Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP5_SUMMARY.md](STEP5_SUMMARY.md).
|
2026-08-17 16:59:16 +03:00
|
|
|
|
|
|
|
|
|
|
Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в
|
|
|
|
|
|
разных сегментах, узел выступает 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 через приоритет правила |
|
2026-08-19 12:33:48 +03:00
|
|
|
|
| 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5), не `topology.env` |
|
2026-08-17 16:59:16 +03:00
|
|
|
|
| 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 # путь сессии по таблицам
|
2026-08-19 12:33:48 +03:00
|
|
|
|
make neigh # MAC членов, резолвленный hcd через ARP
|
2026-08-17 16:59:16 +03:00
|
|
|
|
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 секунд** — после простоя первая же новая сессия
|
|
|
|
|
|
создаёт запись заново.
|