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 }