codis rebalance 算法的理解

本文详细解析了Codis集群中slot的rebalance算法,旨在均匀分配1024个slot,减少迁移次数,以实现负载均衡。通过实例展示,阐述了如何在新增集群时调整slot分配,以降低单一集群的压力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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)
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值