Files
hpnn-proto/hc/maglev.go
T

168 lines
6.0 KiB
Go
Raw Normal View History

package main
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"hash/fnv"
"sort"
)
// Раскладка слотов по членам пула — §5 дизайн-концепции hpnn_v1.
//
// Свойства, ради которых взят именно Maglev-подобный алгоритм:
// - детерминированность: одинаковый вход даёт одинаковую раскладку на любом
// узле и между перезапусками;
// - равномерность в пределах ±1 слота от идеальной доли;
// - минимальное возмущение: при выбытии члена переезжают только его слоты,
// чужие остаются на месте.
//
// Ключ члена — "адрес:порт", а не идентификатор объекта: удаление и повторное
// создание члена с теми же адресом и портом обязано давать ту же раскладку
// (§5.1).
// hashKey — детерминированный хэш ключа члена с солью seed.
func hashKey(key string, seed uint32, salt uint32) uint32 {
h := fnv.New32a()
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], seed)
_, _ = h.Write(buf[:])
binary.BigEndian.PutUint32(buf[:], salt)
_, _ = h.Write(buf[:])
_, _ = h.Write([]byte(key))
return h.Sum32()
}
// BuildSlotTable возвращает срез длиной slots, где каждый элемент — ID члена
// пула, обслуживающего этот слот. Пустой список членов даёт nil: вызывающая
// сторона трактует это как fail-close.
func BuildSlotTable(poolID uint32, members []*Member, slots int) []int {
live := make([]*Member, 0, len(members))
for _, m := range members {
if m.Active() {
live = append(live, m)
}
}
if len(live) == 0 || slots <= 0 {
return nil
}
sort.Slice(live, func(i, j int) bool { return live[i].Key() < live[j].Key() })
// Перестановка каждого члена: последовательность (offset + j*skip) mod M.
// Она обойдёт все M слотов, только если skip взаимно прост с M. При M —
// степени двойки (по умолчанию 1024) это означает «skip нечётный»: чётный
// шаг покрыл бы лишь половину слотов и заполнение зациклилось бы.
perm := make([][]int, len(live))
for i, m := range live {
offset := int(hashKey(m.Key(), poolID, 1) % uint32(slots))
skip := int(hashKey(m.Key(), poolID, 2)%uint32(slots-1)) + 1
if skip%2 == 0 {
skip++
}
p := make([]int, slots)
for j := 0; j < slots; j++ {
p[j] = (offset + j*skip) % slots
}
perm[i] = p
}
// Квоты по весам: минимальный вес получает не менее одного слота.
total := 0
for _, m := range live {
total += m.WeightOrDefault()
}
quota := make([]int, len(live))
assigned := 0
for i, m := range live {
quota[i] = slots * m.WeightOrDefault() / total
if quota[i] == 0 {
quota[i] = 1
}
assigned += quota[i]
}
// Остаток раздаём по кругу, чтобы сумма квот совпала с числом слотов.
for i := 0; assigned < slots; i = (i + 1) % len(live) {
quota[i]++
assigned++
}
table := make([]int, slots)
for i := range table {
table[i] = -1
}
next := make([]int, len(live))
filled := 0
for filled < slots {
progress := false
for i := range live {
if quota[i] == 0 {
continue
}
for next[i] < slots {
c := perm[i][next[i]]
next[i]++
if table[c] == -1 {
table[c] = live[i].ID
quota[i]--
filled++
progress = true
break
}
}
if filled == slots {
break
}
}
if !progress {
// Недостижимо при нечётном skip, но лучше выйти, чем зациклиться.
break
}
}
// Страховка: не покрытые слоты отдаём первому живому члену.
for i := range table {
if table[i] == -1 {
table[i] = live[0].ID
}
}
return table
}
// Digest — SHA-256 от сериализованной раскладки (§5.3 дизайна). Служит для
// сравнения раскладок между узлами и между перезапусками.
func Digest(table []int) string {
h := sha256.New()
var buf [4]byte
for _, id := range table {
binary.BigEndian.PutUint32(buf[:], uint32(id))
_, _ = h.Write(buf[:])
}
return hex.EncodeToString(h.Sum(nil))[:16]
}
// SlotCounts возвращает распределение слотов по членам пула.
func SlotCounts(table []int) map[int]int {
out := map[int]int{}
for _, id := range table {
out[id]++
}
return out
}
// FlowBundle рендерит директивы для ovs-ofctl bundle: замена таблицы слотов
// целиком одной атомарной транзакцией. Первая строка сносит прежнее
// содержимое таблицы, остальные заливают новое — датапас не проходит через
// состояние с полупустой раскладкой.
//
// Формат файла бандла: каждая строка начинается с типа сообщения ("flow") и
// команды ("add" / "delete" / "modify").
func FlowBundle(table []int, slotTable int, nextTable int) string {
out := fmt.Sprintf("flow delete table=%d\n", slotTable)
// Основание fail-close: пока слотов нет, трафик на VIP отбрасывается.
out += fmt.Sprintf("flow add table=%d,priority=0 actions=drop\n", slotTable)
for slot, id := range table {
out += fmt.Sprintf("flow add table=%d,priority=100,ip,reg1=0x%x actions=load:0x%x->NXM_NX_REG2[],goto_table:%d\n",
slotTable, slot, id, nextTable)
}
return out
}