rebalance
其实就是n个组一起瓜分1024个slot的问题.
一个组就是一个 redis 集群, codis 所有的key 都被hash到 1024 个 slot 上
集群分到的slot 越多, 保管的key就越多, 负载就越重
rebalance 尽量让每个集群的负载均衡
如果是重新分配的话,尽可能减少迁移的slot
举例
刚开始运行的时候,开了个2 redis组, 组1 {0,1,...511} 组2 分到了 {512, 513 ... 1023 }
后来压力大了,内存扛不住了, 新加2个 组 来分担压力
怎么分呢? 理想情况下 每人分 1024/4 = 256个 slot
组1 组2 各迁移 256 个slot到 组3 组4
迁移slot是个耗时的工作,得把该slot所有的 key 从原先的分组 复制到 新的redis组
因此rebalance 算法要尽可能的减少 迁移slot的数量
总结
1. 尽可能均匀分配slot
2. 尽量减少迁移的slot数量
迁移中的slot就不参与本次分配了
rebalance的时候,有些slot正在迁移(从一个组迁移到另一个组) 那么这些slot就不参与本次分配
如果让它们也参与分配,分配算法没啥问题,但是我这个迁移到一半的slot该怎么再去新的redis组呢?这会涉及到3个redis组,想想都复杂,那就禁止这么干吧
rebalance源码
func (s *Topom) SlotsRebalance(confirm bool) (map[int]int, error) {
s.mu.Lock()
defer s.mu.Unlock()
slot 是田, group是人,分田多的是地主,分的少的是穷人
rebalance 就是要打土豪分田地
现在普查下有哪些人,有哪些田
slot迁移就是说 有田 正在从 某人 划归到另一个人 名下
ctx, err := s.newContext()
if err != nil {
return nil, err
}
// 人的编号 从1开始 ,全局唯一, groupIds存了当前所有的人
var groupIds []int
for _, g := range ctx.group {
if len(g.Servers) != 0 {
groupIds = append(groupIds, g.Id)
}
}
sort.Ints(groupIds) // 排个序 这样返回的 迁移计划 targetGroupID 就是从小到大的
// 没人分个毛线
if len(groupIds) == 0 {
return nil, errors.Errorf("no valid group could be found")
}
var (
每个人都有3个属性 : assigned[key] pendings[key] moveout[key]
对于编号为key 的这个人。
assigned[key] 已经确定分给你多少块田了
moveout[key] 你就是地主吧,霸占那么多田,我要拿moveout[key]个分给穷人们
pendings[key] 地主家的余田都存这里了。
assigned = make(map[int]int)
pendings = make(map[int][]int)
moveout = make(map[int]int)
docking []int
)
对于id为gid的人 当前你会分配到多少块田。
len(pendings[gid]) 你的田,你自己先分,分完基本的后,你还有余,余下的多少块田?
moveout[gid] 分多少块田给别人
分田这个过程是一个持续的过程,我会多次反复分配,groupSize 是统计下你当前分到了多少块田
var groupSize = func(gid int) int {
return assigned[gid] + len(pendings[gid]) - moveout[gid]
}
// 转移中的田,不参与本次统计,转给谁就归谁,就算你是地主也归你
大家先统计转移中的田
// don't migrate slot if it's being migrated
for _, m := range ctx.slots {
if m.Action.State != models.ActionNothing {
assigned[m.Action.TargetId]++
}
}
// 我算算 大家能分到多少块田 均分
var lowerBound = MaxSlotNum / len(groupIds)
分田了,先分自己原先的田
先拿够 lowerBound 块田
多出来的田放自己的 pendings 中
没的多的 pendings 肯定是空的
地主的pengdings 肯定是多多的
// don't migrate slot if groupSize < lowerBound
for _, m := range ctx.slots {
if m.Action.State != models.ActionNothing {
continue
}
if m.GroupId != 0 {
if groupSize(m.GroupId) < lowerBound {
assigned[m.GroupId]++
} else {
pendings[m.GroupId] = append(pendings[m.GroupId], m.Id)
}
}
}
红黑树,比较均衡的二叉排序树,key 是 各组的 id
乡亲们。排队了,排队了,按田多少排队,穷人的站前面,地主站最后
var tree = rbtree.NewWith(func(x, y interface{}) int {
var gid1 = x.(int)
var gid2 = y.(int)
if gid1 != gid2 {
if d := groupSize(gid1) - groupSize(gid2); d != 0 {
return d
}
return gid1 - gid2
}
return 0
})
for _, gid := range groupIds {
tree.Put(gid, nil)
}
我看看还有没田是无人认领的。 恩 系统刚构建时,所有的田都是无主的。
这是共产主义分配啊,每一块田都分给当前最穷的人。
啥?地主你说你没分到过? 谁让你之前拿那么多的。
// assign offline slots to the smallest group
for _, m := range ctx.slots {
if m.Action.State != models.ActionNothing {
continue
}
if m.GroupId != 0 {
continue
}
dest := tree.Left().Key.(int)
tree.Remove(dest)
docking = append(docking, m.Id)
moveout[dest]--
tree.Put(dest, nil)
}
我算算 最多该拿多少田,我一轮轮的分田总得找个出口, upperBound 就是为出口设计的
其实再+1也行的吧,不是最优分配,有出口就成 这个值一定是作者细细琢磨出来的。
// upperBound = 1 + ((MaxSlotNum - 1) / len(groupIds))
var upperBound = (MaxSlotNum + len(groupIds) - 1) / len(groupIds)
2个以上的人才需要不断的重新分配,一个人分啥,田都归你好了
// rebalance between different server groups
for tree.Size() >= 2 {
地主你站出来
from := tree.Right().Key.(int)
tree.Remove(from)
什么? 地主家没余田了
那就站出来,别回队伍了,其余的人继续分
if len(pendings[from]) == moveout[from] {
continue // 注意 from 没有再回队伍了~~~~~~~~
}
最穷的你也站出来
dest := tree.Left().Key.(int)
tree.Remove(dest)
var (
fromSize = groupSize(from)
destSize = groupSize(dest)
)
// 得了 得了 虽然是地主, 你现在的田 比基本的都少了 lowerBound 咱就不分了
// 迁移的田数量很多,地主被踢出队伍后,剩下的 人 最多的田也不够基本数量, 不分了
if fromSize <= lowerBound {
break
}
// 最穷的人 分到的田 也到达了 作者细细琢磨出来的田上限数, 不分了
if destSize >= upperBound {
break
}
// 当前队伍中最穷的和最富的 都一样多了 说明整个队伍中人的田都一样了 不需要分了
if d := fromSize - destSize; d <= 1 {
break
}
最富的,给块田给最穷的吧 给完回队伍继续按田数排队
moveout[from]++
moveout[dest]--
tree.Put(from, nil)
tree.Put(dest, nil)
}
pengding 余田 需要迁移的全部统计到 docking 中去
每个人看看自己的 moveout 值 和 pengding 数量
moveout 为0 表示 pengding 里的田现在归你了, 那就不要加到docking中
moveout < 0 表示公家还会补多少块田给你
moveout > 0 地主吧你,把 pengding 中 划拨 moveout 块 到 docking 中去
for gid, n := range moveout {
if n < 0 {
continue
}
if n > 0 {
sids := pendings[gid]
sort.Sort(sort.Reverse(sort.IntSlice(sids)))
docking = append(docking, sids[0:n]...)
pendings[gid] = sids[n:]
}
delete(moveout, gid)
}
sort.Ints(docking)
var plans = make(map[int]int)
好了 docking 就是所有可以分给穷人们的田了
看看穷人们的没人需要补给多少块田吧, 按各自的moveout 数量来
moveout 负多少,就补你多少块田
plan 最终就是一份 田地迁移计划 {田编号,分给谁} {田编号,分给谁} {田编号,分给谁} {田编号,分给谁}
for _, gid := range groupIds {
var in = -moveout[gid]
for i := 0; i < in && len(docking) != 0; i++ {
plans[docking[0]] = gid
docking = docking[1:]
}
}
提交分田计划 给领导审批
if !confirm {
return plans, nil
}
confirm过了,咱开始执行 分田计划更新到 zookeeper 或 etcd 中去
var slotIds []int
for sid, _ := range plans {
slotIds = append(slotIds, sid)
}
sort.Ints(slotIds)
for _, sid := range slotIds {
m, err := ctx.getSlotMapping(sid)
if err != nil {
return nil, err
}
defer s.dirtySlotsCache(m.Id)
m.Action.State = models.ActionPending
m.Action.Index = ctx.maxSlotActionIndex() + 1
m.Action.TargetId = plans[sid]
if err := s.storeUpdateSlotMapping(m); err != nil {
return nil, err
}
}
return plans, nil
}
我抽取了rebalance代码,方便在自己的程序中调试。
忽略了其中 涉及到 迁移 的部分
package main
import (
"sort"
"fmt"
rbtree "github.com/emirpasic/gods/trees/redblacktree"
)
type Slot struct {
Id int
GroupId int
}
var MaxSlotNum = 100
func SlotsRebalance(groupIds []int, slots []Slot) (map[int]int, error) {
sort.Ints(groupIds)
var (
assigned = make(map[int]int)
pendings = make(map[int][]int)
moveout = make(map[int]int)
docking []int
)
var groupSize = func(gid int) int {
return assigned[gid] + len(pendings[gid]) - moveout[gid]
}
var lowerBound = MaxSlotNum / len(groupIds)
// don't migrate slot if groupSize < lowerBound
for _, m := range slots {
if m.GroupId != 0 {
if groupSize(m.GroupId) < lowerBound {
assigned[m.GroupId]++
} else {
pendings[m.GroupId] = append(pendings[m.GroupId], m.Id)
}
}
}
var tree = rbtree.NewWith(func(x, y interface{}) int {
var gid1 = x.(int)
var gid2 = y.(int)
if gid1 != gid2 {
if d := groupSize(gid1) - groupSize(gid2); d != 0 {
return d
}
return gid1 - gid2
}
return 0
})
for _, gid := range groupIds {
tree.Put(gid, nil)
}
// assign offline slots to the smallest group
for _, m := range slots {
if m.GroupId != 0 {
continue
}
dest := tree.Left().Key.(int)
tree.Remove(dest)
docking = append(docking, m.Id)
moveout[dest]--
tree.Put(dest, nil)
}
var upperBound = (MaxSlotNum + len(groupIds) - 1) / len(groupIds)
// rebalance between different server groups
for tree.Size() >= 2 {
from := tree.Right().Key.(int)
tree.Remove(from)
if len(pendings[from]) == moveout[from] {
continue
}
dest := tree.Left().Key.(int)
tree.Remove(dest)
var (
fromSize = groupSize(from)
destSize = groupSize(dest)
)
if fromSize <= lowerBound {
break
}
if destSize >= upperBound {
break
}
if d := fromSize - destSize; d <= 1 {
break
}
moveout[from]++
moveout[dest]--
tree.Put(from, nil)
tree.Put(dest, nil)
}
for gid, n := range moveout {
if n < 0 {
continue
}
if n > 0 {
sids := pendings[gid]
sort.Sort(sort.Reverse(sort.IntSlice(sids)))
docking = append(docking, sids[0:n]...)
pendings[gid] = sids[n:]
}
delete(moveout, gid)
}
sort.Ints(docking)
var plans = make(map[int]int)
for _, gid := range groupIds {
var in = -moveout[gid]
for i := 0; i < in && len(docking) != 0; i++ {
plans[docking[0]] = gid
docking = docking[1:]
}
}
return plans, nil
}
func main() {
groupIds := []int{2,3, 1}
slots := make([]Slot,MaxSlotNum)
for i,_ := range slots {
slots[i].Id = i
}
fmt.Println(groupIds)
fmt.Println(slots)
plans,_ := SlotsRebalance(groupIds, slots[:10])
fmt.Println(plans)
for k,v := range plans {
slots[k].GroupId = v
}
groupIds = append(groupIds, 4)
plans,_ = SlotsRebalance(groupIds, slots)
fmt.Println(plans)
}