137 lines
8.6 KiB
Markdown
137 lines
8.6 KiB
Markdown
# Раскладка слотов 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` — это и
|
||
есть проверка детерминизма.
|