180 lines
12 KiB
Markdown
180 lines
12 KiB
Markdown
# Шаг 2 — подсистема health-check и таблица слотов
|
||
|
||
## Контекст
|
||
|
||
Стенд шага 1 работает: OVS маршрутизирует три сегмента и балансирует TCP на
|
||
два бэкенда группой `type=select`. Но состав пула статичен — отказ бэкенда
|
||
балансировщику неизвестен, и половина соединений уходит в никуда.
|
||
|
||
Шаг 2 добавляет подсистему проверки живости: узел сам определяет состояние
|
||
членов пула, выводит мёртвых из балансировки и возвращает восстановившихся.
|
||
Это §7 дизайн-концепции `../hpnn_v1/docs/design.md`, адаптированный к
|
||
одноузловому прототипу (кворум по кластеру и генерации пулов появятся вместе
|
||
с control plane).
|
||
|
||
Вместе с этим по решению заказчика группа `type=select` заменяется на
|
||
**таблицу слотов с Maglev-раскладкой** (§5 дизайна). Причина: при перезаписи
|
||
бакетов группы хэш пересчитывается целиком, и размещение новых соединений
|
||
переезжает даже у нетронутых членов. Таблица слотов даёт минимальное
|
||
возмущение — переезжают только слоты выбывшего члена — и является тем самым
|
||
механизмом, на котором позже строится stateless-датапас.
|
||
|
||
## Ключевые решения
|
||
|
||
**Пробы уходят с уникального адреса узла в сегменте бэкенда** (§7.1 дизайна),
|
||
не с VIP. У чисто-OpenFlow узла шага 1 адресов в ядре нет вовсе, поэтому на
|
||
мосту появляются два internal-порта: `hcif-p2` (`10.20.0.253`) и `hcif-p3`
|
||
(`10.30.0.253`). Это `lbif-bk` из §3.3 — на них позже поселится FRR.
|
||
|
||
**Пробер — демон на Go (`hcd`) внутри контейнера-балансировщика**, как
|
||
`lb-agent` на узле LB в дизайне. Становится основным процессом контейнера;
|
||
`ovsdb-server` и `ovs-vswitchd` работают демонами рядом.
|
||
|
||
**Fail-close при полном отказе пула:** таблица слотов пустеет, остаётся только
|
||
`priority=0 actions=drop` — трафик на VIP отбрасывается со счётчиком.
|
||
|
||
**Дренаж** (§7.3): член выводится из раскладки вручную, пробы при этом
|
||
продолжают идти и состояние остаётся `up`.
|
||
|
||
**Нюанс, который надо зафиксировать честно.** В пайплайне остаётся `ct(nat)`
|
||
для обратного пути, а conntrack закрепляет трансляцию за соединением при его
|
||
создании. Поэтому уже установленные сессии переживают смену раскладки и без
|
||
Maglev. Реальная ценность таблицы слотов здесь — стабильность размещения
|
||
**новых** соединений и подготовка к stateless-датапасу, где никакой ct не
|
||
подстрахует. Это проверяется отдельным тестом (см. верификацию).
|
||
|
||
## Реализация
|
||
|
||
### 1. Датапас: слоты вместо группы (`lb/pipeline.sh`)
|
||
|
||
Таблицы перенумеровываются так, чтобы все переходы шли вперёд (`goto_table`
|
||
назад не умеет):
|
||
|
||
| Таблица | Было | Стало |
|
||
|---|---|---|
|
||
| 10 | листенер → `group:100` | листенер → `multipath(...)` → таблица 11 |
|
||
| 11 | DNAT по `reg2` | **таблица слотов**: `reg1=<slot>` → `reg2=<member>` |
|
||
| 12 | — | DNAT по `reg2` (бывшая 11) |
|
||
|
||
```
|
||
table=10,priority=200,tcp,nw_dst=<VIP>,tp_dst=80 \
|
||
actions=multipath(symmetric_l4,<pool_id>,modulo_n,1024,0,NXM_NX_REG1[]),goto_table:11
|
||
|
||
# правила слотов заливает hcd; pipeline.sh отдаёт только fail-close основание
|
||
table=11,priority=0 actions=drop
|
||
```
|
||
|
||
Группа 100 удаляется (`ovs-ofctl del-groups`), `apply.sh` её больше не
|
||
создаёт. Счётчики правил таблицы 11 дают бесплатную per-member статистику.
|
||
|
||
### 2. Порты источника проб (`lb/entrypoint.sh`)
|
||
|
||
Для каждого приватного сегмента:
|
||
|
||
```
|
||
ovs-vsctl --may-exist add-port br-lb hcif-p2 \
|
||
-- set Interface hcif-p2 type=internal ofport_request=4 mac='"02:42:0a:14:00:fd"'
|
||
ip addr replace 10.20.0.253/24 dev hcif-p2
|
||
ip link set hcif-p2 up
|
||
ip neigh replace 10.20.0.2 lladdr 02:42:0a:14:00:02 dev hcif-p2 nud permanent
|
||
```
|
||
|
||
Статическая ARP-запись избавляет ядро от резолва MAC бэкенда — MAC членов и
|
||
так зафиксированы в `docker-compose.yml`. Обратное направление (бэкенд
|
||
резолвит `10.20.0.253`) закрывает ARP-респондер OVS.
|
||
|
||
Правила для hc-портов в `pipeline.sh`:
|
||
|
||
| Таблица | Правило |
|
||
|---|---|
|
||
| 0 | `in_port=hcif-*` → таблица 20 (без `learn`, без `ct`) |
|
||
| 5 | ARP-респондер для `10.20.0.253` и `10.30.0.253` |
|
||
| 20 | `/32` на адреса hc-портов, приоритет 32 → таблица 21, **без `dec_ttl`** |
|
||
| 21 | adjacency на hc-порты: `mod_dl_dst` + `output` |
|
||
|
||
Ответы бэкендов на пробы приходят на `p2`/`p3`, проходят таблицу 15 (записи в
|
||
`ct` для них нет, трансляция не применяется) и уходят транзитом.
|
||
|
||
### 3. Демон `hc/` (Go, ~350 строк)
|
||
|
||
`hc/maglev.go` — раскладка по §5.2 дизайна:
|
||
|
||
- ключ члена — `"address:port"`, не UUID: пересоздание члена с теми же
|
||
адресом и портом обязано давать ту же раскладку;
|
||
- вход — отсортированный по ключу список живых членов с весами + `pool_id`
|
||
как seed;
|
||
- 1024 слота; `offset = h1 % M`, `skip` приводится к **нечётному** — при
|
||
M = 1024 (степень двойки) чётный шаг не покрывает все слоты и заполнение
|
||
зациклилось бы;
|
||
- `slot_table_digest` — SHA-256 от сериализованной раскладки (§5.3), пишется
|
||
в лог при каждом применении.
|
||
|
||
`hc/main.go` — пробер и применение:
|
||
|
||
- конфиг — JSON, который `lb/hc-config.sh` генерирует из `topology.env`;
|
||
- тикер `interval` (2 с), пробы всех членов параллельно с таймаутом (1 с);
|
||
- тип пробы `http` (GET `/healthz`, ожидается 2xx) или `tcp` (connect);
|
||
источник фиксируется через `net.Dialer.LocalAddr` — это и есть требование
|
||
«пробы с уникального адреса узла»;
|
||
- переход в `down` после `fall` подряд неудач (3), в `up` — после `rise`
|
||
подряд успехов (2); в лог пишутся только переходы, как в §7.1;
|
||
- применение — атомарным бандлом `ovs-ofctl bundle` с директивами
|
||
`delete table=11` + `add ...`, то есть замена таблицы целиком без
|
||
промежуточного состояния; повторно та же раскладка не заливается;
|
||
- HTTP-API на `127.0.0.1:9111`: `/status` (JSON), `/metrics` (Prometheus),
|
||
`/drain?member=`, `/enable?member=`, `/reapply`.
|
||
|
||
`/reapply` дёргает `lb/apply.sh` после `replace-flows`: тот перезаливает весь
|
||
пайплайн и стёр бы слоты, поэтому в конце сообщает демону перезалить их.
|
||
|
||
### 4. Инструментарий и сборка
|
||
|
||
- `lbctl health | slots | drain <m> | enable <m>`; `make health`, `make slots`,
|
||
`make drain M=be1`, `make enable M=be1`.
|
||
- `lbctl slots` — сводка: сколько слотов у каждого члена, счётчики пакетов,
|
||
дайджест.
|
||
- Контекст сборки балансировщика расширяется до корня (`context: .`,
|
||
`dockerfile: lb/Dockerfile`), первая стадия — `golang:1.23-alpine`.
|
||
- В бэкенд добавляется `/slow` (ответ растягивается на N секунд) — нужен для
|
||
проверки выживания установленной сессии.
|
||
|
||
### 5. Файлы
|
||
|
||
```
|
||
hc/{go.mod,main.go,maglev.go} новый демон
|
||
lb/hc-config.sh генерация конфига из topology.env
|
||
lb/topology.env параметры проб, слотов, hc-портов
|
||
lb/pipeline.sh слоты, hc-порты, перенумерация таблиц
|
||
lb/entrypoint.sh internal-порты, статический neigh, запуск hcd
|
||
lb/apply.sh снятие группы, вызов /reapply
|
||
lb/lbctl.sh, Makefile health / slots / drain / enable
|
||
backend/main.go эндпоинт /slow
|
||
docker-compose.yml контекст сборки
|
||
scripts/verify.sh новые проверки
|
||
```
|
||
|
||
## Верификация
|
||
|
||
Добавляется в `scripts/verify.sh`:
|
||
|
||
| Проверка | Ожидание |
|
||
|---|---|
|
||
| Пробы доходят | `lbctl health` — оба члена `up`, счётчик `/healthz` на бэкендах растёт |
|
||
| Источник проб | на бэкенде видно обращение с `10.20.0.253`/`10.30.0.253`, не с VIP |
|
||
| Раскладка слотов | 1024 слота поделены поровну ±1, дайджест стабилен между перезапусками |
|
||
| **Минимальное возмущение** | снимок раскладки → `drain be1` → у be2 **ни один** слот не сменил владельца |
|
||
| Отказ члена | `docker compose pause be1` → `down` за `fall × interval`, его слоты перешли к be2 |
|
||
| Балансировка при отказе | все запросы на VIP обслуживает be2, ошибок нет |
|
||
| Восстановление | `unpause` → `up` за `rise × interval`, раскладка вернулась к исходному дайджесту |
|
||
| Выживание сессии | долгая сессия через `/slow` не рвётся при выводе второго члена |
|
||
| Полный отказ | оба члена недоступны → в таблице 11 только `drop`, трафик на VIP падает со счётчиком |
|
||
| Идемпотентность | `make flows` → слоты возвращаются в актуальном составе, а не в полном |
|
||
|
||
## Артефакты (по требованию CLAUDE.md)
|
||
|
||
1. `docs/STEP2_IMPLEMENTATION_PLAN.md` — план внедрения; черновик уже написан
|
||
под вариант с группой, привести к решению о таблице слотов.
|
||
2. `docs/STEP2_SUMMARY.md` — итоги, отклонения, ограничения.
|
||
3. `README.md` — раздел про health-check и таблицу слотов, схема с
|
||
hc-портами, новые команды; таблица пайплайна перенумерована.
|