231 lines
15 KiB
Markdown
231 lines
15 KiB
Markdown
# Потоки данных: балансировка приватного трафика
|
||||
|
|
|
|||
|
|
**Дата:** 2026-08-17
|
|||
|
|
**Область:** путь пакета от клиента внутри приватного сегмента на приватный
|
|||
|
|
листенер (`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).
|
|||
|
|
|
|||
|
|
Отличие от публичного случая: клиент может стоять **в одном сегменте с
|
|||
|
|
бэкендами**. Тогда ответ бэкенда уходит клиенту по 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<br/>аплинк к шлюзу Docker"]
|
|||
|
|
HC2["hcif-p2 · 4<br/>10.20.0.253 <b>в ядре</b>"]
|
|||
|
|
B1["be1 · 10"]
|
|||
|
|
B3["be3 · 11"]
|
|||
|
|
C2["cli2 · 12"]
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
CLI2["cli2 10.20.0.10<br/><i>клиент: настроен только адрес</i>"]
|
|||
|
|
BE1["be1 10.20.0.2:8080"]
|
|||
|
|
BE3["be3 10.20.0.3:8080"]
|
|||
|
|
GW["шлюз Docker 10.20.0.254<br/><i>egress бэкендов</i>"]
|
|||
|
|
OTHER["сегмент 3 — 10.30.0.0/24<br/>be2, be4, cli3"]
|
|||
|
|
|
|||
|
|
C2 <--> CLI2
|
|||
|
|
B1 <--> BE1
|
|||
|
|
B3 <--> BE3
|
|||
|
|
P2 <--> GW
|
|||
|
|
S2 -.->|"маршрутизация<br/>таблицы 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["<b>0</b> сегмент по in_port → reg0<br/><b>learn</b> FDB (eth_src → порт)<br/><b>learn</b> adjacency (для адресов своей подсети)"]
|
|||
|
|
T0 --> T1{"<b>1</b> классификация<br/>по MAC назначения"}
|
|||
|
|
|
|||
|
|
T1 -->|"arp"| T5["<b>5</b> ARP-респондер<br/>за VIP, шлюз, .253"]
|
|||
|
|
T1 -->|"icmp echo на VIP<br/>или шлюз"| T6["<b>6</b> ICMP-респондер"]
|
|||
|
|
T1 -->|"dl_dst = MAC шлюза<br/>+ nw_dst = VIP:80"| T10
|
|||
|
|
T1 -->|"dl_dst = MAC шлюза<br/>прочий IP"| T15["<b>15</b> обратный путь L3<br/>ct(nat) — для сессий<br/>из другого сегмента"]
|
|||
|
|
T1 -->|"прочее —<br/>трафик между ВМ"| T24
|
|||
|
|
|
|||
|
|
T5 -->|"не наш адрес"| T25
|
|||
|
|
T10{"<b>10</b> листенер<br/>tcp VIP:80"} --> T11["<b>11</b> таблица слотов<br/>reg1 → reg2 (общая<br/>с публичным листенером)"]
|
|||
|
|
T11 -->|"пул пуст"| DROPF(["drop — fail-close"])
|
|||
|
|
T11 --> T12{"<b>12</b> применение члена"}
|
|||
|
|
|
|||
|
|
T12 -->|"<b>член в сегменте клиента</b><br/>mod_dl_dst + ct(commit,nat)<br/><b>без dec_ttl</b>"| T25
|
|||
|
|
T12 -->|"член в другом сегменте<br/>ct(commit,nat)"| T20["<b>20/21</b> маршрутизация<br/>dec_ttl, adjacency"]
|
|||
|
|
T20 --> OUTR(["output: порт члена<br/>в другом сегменте"])
|
|||
|
|
|
|||
|
|
T15 --> T16["<b>16</b> пост-ct"] --> T20
|
|||
|
|
T24{"<b>24</b> обратная трансляция<br/>сегмента"}
|
|||
|
|
T24 -->|"nw_src = член, tp_src = 8080<br/>ct(nat): src → VIP:80"| T25
|
|||
|
|
T24 -->|"прочее — без изменений"| T25
|
|||
|
|
T25{"<b>25</b> коммутация<br/>FDB по eth_dst"} --> OUTL(["output: порт ВМ"])
|
|||
|
|
T25 -->|"broadcast или<br/>неизвестный MAC"| FLOOD(["рассылка по сегменту,<br/>кроме входного порта"])
|
|||
|
|
|
|||
|
|
classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff
|
|||
|
|
class DROPF d
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 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: mod_dl_dst = MAC be1,<br/>ct(commit, nat(dst=10.20.0.2:8080))<br/><b>dec_ttl не выполняется</b>
|
|||
|
|
OF->>B: t25: FDB → порт be1
|
|||
|
|
Note over B: client=10.20.0.10 — исходный адрес клиента<br/>served=10.20.0.2:8080 — трансляция 80 → 8080
|
|||
|
|
B->>OF: ответ src=10.20.0.2:8080 → 10.20.0.10,<br/>dst MAC = MAC клиента (сосед по L2)
|
|||
|
|
Note over OF: t1: dl_dst ≠ MAC шлюза → t24
|
|||
|
|
Note over OF: <b>t24</b>: 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<br/>10.20.0.10"] -->|"dst=10.20.0.100:80"| A["t0/t1: reg0=2,<br/>dl_dst = MAC шлюза"]
|
|||
|
|
A --> B["t10 → t11 → t12<br/>reg2 указывает на be2:<br/>DNAT 10.30.0.2:8080"]
|
|||
|
|
B --> D["t20: LPM /24 10.30.0.0<br/><b>dec_ttl</b>, src MAC = p3"]
|
|||
|
|
D --> E["t21: dst MAC = be2,<br/>output порт be2"]
|
|||
|
|
E --> BE2["be2<br/><i>client=10.20.0.10</i>"]
|
|||
|
|
BE2 -->|"маршрут 10.20.0.0/24<br/>via 10.30.0.1"| F["t1: dl_dst = MAC шлюза → t15<br/>ct(nat): src → 10.20.0.100:80"]
|
|||
|
|
F --> G["t20 → t21: adjacency клиента,<br/>выученная в t0"] --> C
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Клиенту оба пути неотличимы — ответ приходит от `10.20.0.100:80`. Разница видна
|
|||
|
|
только по TTL: **64** от соседа по сегменту против **63** через маршрутизацию.
|
|||
|
|
|
|||
|
|
## 5. Остальной трафик сегмента
|
|||
|
|
|
|||
|
|
Балансировка не мешает обычной работе сегмента: всё, что не адресовано VIP,
|
|||
|
|
коммутируется без трансляции.
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A["cli2 → be1:8080 напрямую,<br/>минуя VIP"] --> B["t1: dl_dst = MAC be1 → t24"]
|
|||
|
|
B --> C["t24: ct без записи —<br/>изменений нет"] --> D["t25: FDB → порт be1"]
|
|||
|
|
|
|||
|
|
E["be1 → be3:8080<br/>внутри сегмента"] --> F["t1 → t24 → t25"]
|
|||
|
|
F --> G["t25: FDB → порт be3"]
|
|||
|
|
|
|||
|
|
H["be1 → 1.1.1.1<br/>default via 10.20.0.254"] --> I["t1 → t24 → t25"]
|
|||
|
|
I --> J["t25: FDB → аплинк p2 →<br/>docker-бридж → NAT хоста<br/><i>профиль A: мимо балансировки</i>"]
|
|||
|
|
|
|||
|
|
K["ARP между ВМ"] --> L["t5: не наш адрес → t25"]
|
|||
|
|
L --> M["t25: рассылка по портам сегмента"]
|
|||
|
|
|
|||
|
|
N["health-проба<br/>10.20.0.253 → be1:8080"] --> O["t0: in_port = hcif-p2<br/>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` = сегмент члена | `mod_dl_dst` + `ct(commit,nat)` → t25 | **без `dec_ttl`** |
|
|||
|
|
| 12 | прочее | `ct(commit,nat)` → t20 | член из другого сегмента |
|
|||
|
|
| 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 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-ошибки и фрагменты — как и в публичном
|
|||
|
|
пути, вне рамок.
|