Files

179 lines
12 KiB
Markdown
Raw Permalink Normal View History

# Шаг 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-портами, новые команды; таблица пайплайна перенумерована.