Files
hpnn-proto/docs/MAGLEV_SLOT_TABLE.md

137 lines
8.6 KiB
Markdown
Raw Permalink 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.
# Раскладка слотов Maglev — как это работает
**Источник:** `hc/maglev.go`
**Назначение:** справка для разработки: что гарантирует алгоритм, что нет и где
границы применимости.
## Задача
Отобразить 1024 слота на членов пула так, чтобы:
- раскладка была **детерминированной** — одинаковый вход даёт одинаковый
результат на любом узле и после любого перезапуска;
- слоты делились **равномерно** (с точностью до веса);
- при изменении состава пула переезжало **минимальное** число слотов.
Номер слота вычисляет датапас: `multipath(symmetric_l4, pool_id, modulo_n,
1024, 0, reg1)`. Демон отвечает только за содержимое таблицы `слот → член`.
## Вход
| Что | Откуда | Роль |
|---|---|---|
| Живые члены | `m.Active()` — `state=up && admin=enabled` | только они попадают в раскладку |
| Ключ члена | `Key() = "адрес:порт"` | **не** ID объекта: пересоздание члена с теми же адресом и портом даёт ту же раскладку |
| Порядок | `sort.Slice` по `Key()` | убирает зависимость от порядка в конфигурации |
| `poolID` | `topology.env` | соль хэша: разные пулы дают независимые раскладки |
| Вес | `WeightOrDefault()`, по умолчанию 1 | пропорция слотов |
Пустой список живых членов → `nil` → таблица слотов пуста → **fail-close**.
## Алгоритм — четыре шага
### 1. Перестановка на каждого члена
Для члена вычисляются два независимых хэша (FNV-32a от `poolID ‖ salt ‖ key`):
```
offset = hash(key, poolID, 1) mod M
skip = hash(key, poolID, 2) mod (M-1) + 1 // затем принудительно нечётный
p[j] = (offset + j*skip) mod M // j = 0..M-1
```
Это личный порядок обхода слотов данным членом — его «список предпочтений».
**Почему skip делается нечётным.** Последовательность обойдёт все `M` слотов,
только если `skip` взаимно прост с `M`. Классический Maglev рассчитан на простое
`M`; здесь `M = 1024` — степень двойки, поэтому достаточно нечётности. Чётный
шаг покрыл бы половину слотов, и заполнение зациклилось бы.
### 2. Квоты по весам
```
quota[i] = M * weight[i] / totalWeight (минимум 1)
остаток раздаётся по кругу до суммы M
```
Квота — верхняя граница числа слотов члена. Именно она, а не длина
перестановки, определяет долю.
### 3. Заполнение
Раунды по членам в отсортированном порядке. В каждом раунде член берёт из своей
перестановки **первый ещё свободный** слот, занимает его и уменьшает квоту.
Раунды идут, пока не заполнены все `M` слотов.
```
for filled < M:
for i in members:
if quota[i] == 0: continue
c = следующий свободный слот из perm[i]
table[c] = live[i].ID; quota[i]--; filled++
```
Конкуренция за слот разрешается по принципу «кто раньше дошёл»: слот достаётся
члену, у которого он раньше встретился в перестановке при равных условиях
раунда.
### 4. Страховка
Слоты, оставшиеся `-1`, отдаются первому живому члену. При нечётном `skip` это
недостижимо; ветка оставлена, чтобы дефект хэша не оставил дыру в таблице.
## Что получается на выходе
- `table []int` — 1024 элемента, ID члена в каждом.
- `Digest(table)` — SHA-256 от раскладки, первые 16 hex-символов. Служит для
сравнения раскладок между узлами и между перезапусками. Эталон текущего
стенда на четырёх членах — `0a16713c5a9eb94d`.
- `FlowBundle(...)` — директивы `ovs-ofctl bundle`: одна строка `flow delete
table=11`, затем `priority=0 actions=drop` (основание fail-close) и 1024
правила `reg1=<слот> → load reg2=<член>, goto_table:12`. Весь файл
применяется **одной транзакцией**, поэтому датапас не проходит через
состояние с полузаполненной таблицей.
## Гарантии и их цена
| Свойство | Как достигается | Измерено на стенде |
|---|---|---|
| Детерминизм | хэш от `(poolID, key)`, сортировка по ключу, без seed датапаса | дайджест совпадает после `down`/`up` |
| Равномерность | квоты по весам | 256 / 256 / 256 / 256 на четырёх членах |
| Минимальное возмущение | у оставшихся членов перестановки не изменились, переназначаются только освободившиеся слоты | при выводе одного из четырёх: 256 его слотов переехали обязательно, у остальных сменили владельца **8 из 768 — 1 %** |
Возмущение **малое, но не нулевое**: это свойство классического Maglev, а не
дефект. Проверка в `verify.sh` сформулирована как порог (не более 20 слотов), а
не как равенство нулю.
Для сравнения: группа `type=select` при изменении числа бакетов переложила бы
все соединения.
## Границы применимости
- **Живые сессии алгоритм не защищает** — в текущем stateful-датапасе их держит
`ct(commit, nat)`: привязка закрепляется при создании соединения. Ценность
минимального возмущения раскроется при переходе на stateless-датапас.
- **Веса поддержаны в коде, но в конфигурации у всех членов вес 1.**
Гранулярность веса — `1/1024`.
- **`hash_algo_version` не реализована.** Дайджест считается и логируется, но
сравнивать его пока не с кем: узел один.
- **`M` фиксировано в двух местах** — в правиле `multipath` и в конфигурации
демона (`SLOTS` в `topology.env`). Рассинхронизация сделает часть слотов
недостижимой.
- Пересчёт выполняется только при **смене состояния** члена (`up`/`down`,
`drain`/`enable`) или по `/reapply`; если дайджест не изменился, заливка не
выполняется.
## Как проверить
```bash
make slots # раскладка и per-member счётчики
make health | grep дайджест # текущий дайджест
make drain M=be1 && make slots # возмущение при выводе члена
make trace SRC=10.20.0.10 SPORT=40000 SEG=p2 # какой слот и член даёт 5-tuple
```
Повтор `trace` с теми же аргументами обязан дать тот же `reg1` и `reg2` — это и
есть проверка детерминизма.