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