про Maglev алгоритм
This commit is contained in:
1 parent
d80a6c44b0
commit
57ed89e700
2 files changed
+139
No files matched your search
@@ -22,6 +22,9 @@
|
||||
| 3 | Листенер в приватном сегменте, коммутация сегментов | [план](docs/STEP3_IMPLEMENTATION_PLAN.md) | [итоги](docs/STEP3_SUMMARY.md) |
|
||||
| — | Разделение документации по потокам данных | [план](docs/CHANGE_SPLIT_DATAFLOW_DOCS_PLAN.md) | [итоги](docs/CHANGE_SPLIT_DATAFLOW_DOCS_SUMMARY.md) |
|
||||
|
||||
Как устроена раскладка слотов — [docs/MAGLEV_SLOT_TABLE.md](docs/MAGLEV_SLOT_TABLE.md)
|
||||
(справка по `hc/maglev.go` для разработки).
|
||||
|
||||
Потоки данных описаны отдельно для каждого вида балансировки:
|
||||
[публичный трафик](docs/PUBLIC_LB_DATAFLOW.md) — клиент из клиентской сети на
|
||||
`192.168.5.21:80`, целиком маршрутизируемый путь;
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Раскладка слотов 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` — это и
|
||||
есть проверка детерминизма.
|
||||
Reference in new issue
Block a user