169 lines
6.0 KiB
Go
169 lines
6.0 KiB
Go
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
|
|
}
|