负载均衡代码实现-副本均衡

1 副本均衡简介

想象一个场景2,假如集群有那么一个节点,还没有任何访问qps。我们肯定可以把数据放在上面,让它去分担一些数据库的读写压力。怎么去做呢?

如图所示

我们可以删除原有副本,在其他节点上重建副本。

可以看出,副本迁移前,4个节点的qps分别是45,2020, 1030, 1000,负载是不均衡的。

  • 副本均衡后

可以看出副本 range4 - 1 从节点2迁移到了节点4,且lease 由节点2 转移到了节点1; 副本range5 - 2 从节点3迁移到了节点4。 4个节点副本迁移后的qps都是接近1000,实现了负载均衡。由于副本分布发生了变化,所以叫副本均衡。

副本均衡的实现比较复杂一些,涉及到存储的一些逻辑。

2 代码实现细节

2.1问题1: 节点怎么选出需要做副本迁移的副本?

在经过了上一章节中lease 迁移后,如果本节点的qps 还是大于最大阈值,则还需要做副本均衡。

Lease 迁移过程中已经删除了热点range (hottestRanges)中做了lease 迁移的 range,我们对剩余的热点range hottestRanges 做check,看其是否可以做副本均衡。副本均衡的意思是,将该range 的在本节点的副本迁移到其他的节点,即在另一个节点重建一个副本,本节点的副本被删除掉。

2.2问题2: 副本迁移详细步骤

实际上kwbase 选出满足副本迁移条件的副本是在函数 chooseReplicaToRebalance() 中进行的。

满足以下条件的热点副本才能副本 转移。

2.2.1当前 store 上的该range拥有的租约是合法的。

很明显,例如章节1中,Lease 均衡前, 如果 range1 - 1 不是合法的leaseholder,则不能副本迁移,因为迁移本身是为了降低qps,该副本不是leaseholder,迁移对qps均衡不会有影响。

代码见2.2.2:

2.2.2判断副本转移后本地节点 的 QPS 可以在合理阈值以内 (集群角度)。

很明显,例如章节1中, 如果 range1 - 1 副本迁移后,节点1的qps 的很小,假如迁移后由于节点1的qps很小,加剧了整个集群的不均衡,则肯定是不合理的。 这个合理阈上一章节已经介绍过。

代码如下:

// 1 如果要转移的热点副本没有持有lease, 则不转移
// 2 本地节点的qps 去掉要转移的热点副本的qps 后, 剩余的qps 太小了,则不转移
func shouldNotMoveAway(
        ctx context.Context,
        replWithStats replicaWithStats,  // 本地热点副本
        localDesc *roachpb.StoreDescriptor, // 本地节点store
        now hlc.Timestamp,
        minQPS float64,
) bool {
        if !replWithStats.repl.OwnsValidLease(now) { // 如果要转移的热点副本没有持有lease
                log.VEventf(ctx, 3, "store doesn't own the lease for r%d", replWithStats.repl.RangeID)
                return true
        }
        if localDesc.Capacity.QueriesPerSecond-replWithStats.qps < minQPS { // 本地节点的qps 去掉要转移的热点副本的qps 后, 剩余的qps 太小了
                log.VEventf(ctx, 3, "moving r%d's %.2f qps would bring s%d below the min threshold (%.2f)",
                        replWithStats.repl.RangeID, replWithStats.qps, localDesc.StoreID, minQPS)
                return true
        }
        return false
}

2.2.3 根据热点副本的qps在本节点占比,判断是否有价值做副本转移 (本地角度)。

很明显,例如章节1中, 如果 range1 - 1 的qps 在本节点的占比非常小,副本 迁移后,对本节点的qps影响微乎其微,则也没有做副本 迁移的价值。

实际上,Kwbase 是这样的做的,如果 range1 - 1 的qps 在本节点占比小于 0.001,且本节点的lease 数量小于平均值,则不进行副本 迁移。

代码如下:

const minQPSFraction = .001
if replWithStats.qps < localDesc.Capacity.QueriesPerSecond*minQPSFraction &&
        float64(localDesc.Capacity.LeaseCount) <= storeList.candidateLeases.mean {  // 虽然是个热点range, 但是qps 小于节点的 0.001, 且节点的lease 数量小于平均值;
        log.VEventf(ctx, 5, "r%d's %.2f qps is too little to matter relative to s%d's %.2f total qps",
                replWithStats.repl.RangeID, replWithStats.qps, localDesc.StoreID, localDesc.Capacity.QueriesPerSecond)
        continue
}

2.2.4 选取一个节点作为副本迁移的接收者节点

按一些指标对集群中副本接受节点排序,得到一个后选择者节点列表;并从候选者节点列表中选择一个 good节点作为副本转移的接收者节点。

代码如下:

/ 返回一个good 副本接受者节点
func (a *Allocator) allocateTargetFromList(
        ctx context.Context,
        sl StoreList,  // 集群节点列表
        zone *zonepb.ZoneConfig,
        candidateReplicas []roachpb.ReplicaDescriptor,  // range 现有副本列表
        options scorerOptions,
) (*roachpb.StoreDescriptor, string) {
        analyzedConstraints := constraint.AnalyzeConstraints( // 对副本列表分析约束
                ctx, a.storePool.getStoreDescriptor, candidateReplicas, zone)
        candidates := allocateCandidates(  // 1 排序的候选者节点列表
                sl, analyzedConstraints, candidateReplicas, a.storePool.getLocalities(candidateReplicas),
                options,
        )
        log.VEventf(ctx, 3, "allocate candidates: %s", candidates)
        if target := candidates.selectGood(a.randGen); target != nil {   // 2 对候选者节点列表 candidates选择一个good节点作为副本转移的接受者节点
                log.VEventf(ctx, 3, "add target: %s", target)
                details := decisionDetails{Target: target.compactString(options)}
                detailsBytes, err := json.Marshal(details)
                if err != nil {
                        log.Warningf(ctx, "failed to marshal details for choosing allocate target: %+v", err)
                }
                return &target.store, string(detailsBytes)
        }

        return nil, ""
}

2.2.4.1 获得排序的候选者节点列表

遍历集群所有节点,如果节点上有该range 的一个副本,则忽略;通过约束检查;通过容量检查,如果容量超出95%, 则不能接受副本; 计算多样性得分,收敛得分,平衡得分,节点range容量 等参数,构造一个候选者结构candidate; 获得一个候选者列表后,根据根据 fullDisk, 多样性得分,收敛得分,平衡得分,range计数 等参数 进行排序对候选者列表排序。

代码如下:

// 获得一个排序的候选节点列表,排序规则是,根据 fullDisk, 多样性得分,收敛得分,平衡得分,range计数 等参数 进行排序。
func allocateCandidates(
        sl StoreList,   // 集群节点
        constraints constraint.AnalyzedConstraints,
        existing []roachpb.ReplicaDescriptor,    // range的 现有副本列表
        existingNodeLocalities map[roachpb.NodeID]roachpb.Locality,
        options scorerOptions,
) candidateList {
        var candidates candidateList
        for _, s := range sl.stores { // 遍历集群节点
                if nodeHasReplica(s.Node.NodeID, existing) { // 如果在 existing 的副本所在的节点上,则忽略; 因为这些节点已经有该ragne的副本,不能再接受一个
                        continue
                }
                constraintsOK, necessary := allocateConstraintsCheck(s, constraints) // 对该节点约束check
                if !constraintsOK {
                        continue
                }
                if !maxCapacityCheck(s) { // 节点容量校验,如果存储大于或等于容量的95%, 则不能作为副本接受者节点
                        continue
                }
                diversityScore := diversityAllocateScore(s, existingNodeLocalities) // 目标节点的多样性得分
                balanceScore := balanceScore(sl, s.Capacity, options) // 平衡得分
                var convergesScore int
                if options.qpsRebalanceThreshold > 0 {
                        if s.Capacity.QueriesPerSecond < underfullThreshold(sl.candidateQueriesPerSecond.mean, options.qpsRebalanceThreshold) {
                                convergesScore = 1  // 如果qps 小于 under 阀值, 收敛得分为1
                        } else if s.Capacity.QueriesPerSecond < sl.candidateQueriesPerSecond.mean {
                                convergesScore = 0  // qps 小于 平均值, 收敛得分为0
                        } else if s.Capacity.QueriesPerSecond < overfullThreshold(sl.candidateQueriesPerSecond.mean, options.qpsRebalanceThreshold) {
                                convergesScore = -1 // qps 小于 over 阀值, 收敛得分为-1
                        } else {
                                convergesScore = -2 // qps 大于 over 阀值, 收敛得分为 -2
                        }
                }
                candidates = append(candidates, candidate{
                        store:          s,
                        valid:          constraintsOK,
                        necessary:      necessary,  // 必要性
                        diversityScore: diversityScore, // 多样性得分
                        convergesScore: convergesScore, // 收敛得分
                        balanceScore:   balanceScore, // 平衡得分
                        rangeCount:     int(s.Capacity.RangeCount), // range 计数
                })
        }
        if options.deterministic {  // 对候选者做排序
                sort.Sort(sort.Reverse(byScoreAndID(candidates)))
        } else {
                sort.Sort(sort.Reverse(byScore(candidates)))
        }
        return candidates
}

2.2.4.1.1 候选者排序算法

根据 valid, fullDisk, necessary, diversityScore, convergesScore, balanceScore, rangeCount 参数对候选者列表排序

即节点 有效的,磁盘不满的,必要的,多样性得分高的,收敛得分高的,平衡得分高的,rangeCount 少的节点在排在前面。

代码如下:

func (c byScoreAndID) Less(i, j int) bool {
        if scoresAlmostEqual(c[i].diversityScore, c[j].diversityScore) &&
                c[i].convergesScore == c[j].convergesScore &&
                scoresAlmostEqual(c[i].balanceScore.totalScore(), c[j].balanceScore.totalScore()) &&
                c[i].rangeCount == c[j].rangeCount &&
                c[i].necessary == c[j].necessary &&
                c[i].fullDisk == c[j].fullDisk &&
                c[i].valid == c[j].valid {
                return c[i].store.StoreID < c[j].store.StoreID // 如果前面都相等则按storeId 大小排序
        }
        return c[i].less(c[j])  // 否则按 byScoreAndID 从小到大排序
}

func (c candidate) compare(o candidate) float64 {
        if !o.valid { // o 无效, c大
                return 6
        }
        if !c.valid { // c 无效, o大
                return -6
        }
        if o.fullDisk { // o 满了, c大
                return 5
        }
        if c.fullDisk { // c 满了, o大
                return -5
        }
        if c.necessary != o.necessary {
                if c.necessary { // c 必要, c 大
                        return 4
                }               // o 必要, o大
                return -4
        }
        if !scoresAlmostEqual(c.diversityScore, o.diversityScore) {
                if c.diversityScore > o.diversityScore { // c 多样性得分大, c 大
                        return 3
                }                                      // o 的多样性得分大, o大
                return -3
        }
        if c.convergesScore != o.convergesScore {
                if c.convergesScore > o.convergesScore { // c 的收敛得分大,c 大
                        return 2 + float64(c.convergesScore-o.convergesScore)/10.0
                }                                       // o 的收敛得分大,o大
                return -(2 + float64(o.convergesScore-c.convergesScore)/10.0)
        }
        if !scoresAlmostEqual(c.balanceScore.totalScore(), o.balanceScore.totalScore()) {
                if c.balanceScore.totalScore() > o.balanceScore.totalScore() { // c 的平衡得分大, c 大
                        return 1 + (c.balanceScore.totalScore()-o.balanceScore.totalScore())/10.0
                }             // o 的平衡得分大, o 大
                return -(1 + (o.balanceScore.totalScore()-c.balanceScore.totalScore())/10.0)
        }
        // Sometimes we compare partially-filled in candidates, e.g. those with
        // diversity scores filled in but not balance scores or range counts. This
        // avoids returning NaN in such cases.
        if c.rangeCount == 0 && o.rangeCount == 0 {
                return 0
        }
        if c.rangeCount < o.rangeCount {  // c 的range 数量多, o 大
                return float64(o.rangeCount-c.rangeCount) / float64(o.rangeCount)
        }                                // o 的range 数量多, c 大
        return -float64(c.rangeCount-o.rangeCount) / float64(c.rangeCount)
}

2.2.4.1.2 某节点的多样性得分算法

获得本节点到集群所有节点的多样性得分的平均值。

计算两个位置的多样性的得分:

1 如果长度较小的串不是较长串的前缀,则两个串的相同前缀越小,多样性得分越高。

2 如果长度较小的串正好是较长串的前缀;则长度较小的串长度越小,多样性得分越高

//diversityAssignScore返回一个介于0和1之间的值,该值基于将副本添加到该节点存储中的期望程度。分数越高,意味着这个节点越适合。
// 获得该节点的多样性得分
func diversityAllocateScore(
        store roachpb.StoreDescriptor, existingNodeLocalities map[roachpb.NodeID]roachpb.Locality,
) float64 {
        var sumScore float64
        var numSamples int
        // We don't need to calculate the overall diversityScore for the range, just
        // how well the new store would fit, because for any store that we might
        // consider adding the pairwise average diversity of the existing replicas
        // is the same.
        for _, locality := range existingNodeLocalities { // 遍历聚集群所有节点
                newScore := store.Node.Locality.DiversityScore(locality) // 获得本节点store 到其他某个节点位置的多样性得分
                sumScore += newScore   // 累加和
                numSamples++
        }
        // If the range has no replicas, any node would be a perfect fit.
        if numSamples == 0 {
                return roachpb.MaxDiversityScore
        }
        return sumScore / float64(numSamples) // 平均值
}

// 计算 l 位置, 和 other 位置的多样性得分
func (l Locality) DiversityScore(other Locality) float64 {
        length := len(l.Tiers)
        if len(other.Tiers) < length {
                length = len(other.Tiers)
        }
        //1 如果长度较小的串不是较长串的前缀,则两个串的相同前缀越少,多样性得分越高。
        for i := 0; i < length; i++ { // 从0开始比较,如果 i 不同了; 则 len-i/ len 是多样性值
                if l.Tiers[i].Value != other.Tiers[i].Value { // 例如 12345  12399 -  多样性值是2/5; 17890 16789-- 多想行得分是4/5
                        return float64(length-i) / float64(length)  // 很明显,相同前缀越少,多样性得分越高
                }
        }
        if len(l.Tiers) != len(other.Tiers) { // 2 如果长度较小的串正好是较长串的前缀;很明显长度较小的串长度越小,多样性得分越高
                return MaxDiversityScore / float64(length+1) // 例如 123  12345- 多样性值是 1/4 ; 12345 1234567 -- 多样性得分是 1/8
        }                                              // 很明显长度较小的串长度越小,多样性得分越高
        return 0  // 如果都位置信息都相等,则多样性值是0, 例如 123  123,  多样性值是0, 即无多样性
}

2.2.4.2 对候选者节点列表 candidates选择一个good节点作为副本转移的接受者节点
/*
节点0  节点2  节点1  节点3  节点4  候选节点列表
[2, 4, 1, 0, 3]                  random 数组
allocatorRandomCount = 2
则查看[2] [4] , 即节点1, 节点4, 找到一个最好的的候选者
*/
func (cl candidateList) selectGood(randGen allocatorRand) *candidate {
        cl = cl.best()  // 1 获得一个有效,且分数最高的节点集合
        if len(cl) == 0 {
                return nil
        }
        if len(cl) == 1 {
                return &cl[0]
        }
        randGen.Lock()
        order := randGen.Perm(len(cl)) // 2 获得一个随机顺序 order, 例如共5个节点, 获得随机数列表为 [2, 4, 1, 4, 3]
        randGen.Unlock()
        best := &cl[order[0]] // 获取一个
        for i := 1; i < allocatorRandomCount; i++ { // 3 根据order 的索引,分数最高且有效的节点集合中 找到best 的一个候选者节点
                if best.less(cl[order[i]]) { // 如果 i 更大,则best 更新为 i
                        best = &cl[order[i]]
                }
        }
        return best
}

//best返回排序(按分数反转)候选列表中共享最高约束分数且有效的所有节点。
func (cl candidateList) best() candidateList {
        cl = cl.onlyValidAndNotFull()
        if len(cl) <= 1 {
                return cl
        }
        for i := 1; i < len(cl); i++ { // 找到所有的必要,且多样性得分和[0]节点是几乎相等的,且平衡得分和[0]节点是相等的 节点集合
                if cl[i].necessary == cl[0].necessary &&  // 值得注意的是[0] 是根据参数算出的最优节点
                        scoresAlmostEqual(cl[i].diversityScore, cl[0].diversityScore) &&
                        cl[i].convergesScore == cl[0].convergesScore {
                        continue
                }
                return cl[:i]  // 返回匼条件的节点集合
        }
        return cl
}

2.2.5 副本 接收节点接受副本 后qps 不大于最大值

我们试想一下,如果节点4接受了副本 range4-1 后,节点4 的qps 就会增加,如果副本range4-1 的qps很大,导致节点4的qps 增大太多,也会导致负载不均衡。

Kwbase 是这样做的

1 如果副本 接受节点,例如节点4,如果它副本 转移前的qps 小于最小阈值,副本转移后的qps大于最大阈值,则不能转移副本迁移。

2 如果副本 接受节点, 例如节点4,如果它的副本 转移前的qps 大于最小阈值,副本 转移后的qps 大于集群的qps平均值,则不能转移lease。

代码实现如下:


func shouldNotMoveTo(
        ctx context.Context,
        storeMap map[roachpb.StoreID]*roachpb.StoreDescriptor,
        replWithStats replicaWithStats,  // 原lease 的副本
        candidateStore roachpb.StoreID,  // 接受lease 的store
        meanQPS float64,  // 集群平均qps
        minQPS float64,  // qps 最小阈值
        maxQPS float64,  // qps 最大阈值
) bool {
        storeDesc, ok := storeMap[candidateStore]
        if !ok {
                log.VEventf(ctx, 3, "missing store descriptor for s%d", candidateStore)
                return true
        }

        newCandidateQPS := storeDesc.Capacity.QueriesPerSecond + replWithStats.qps  // dest store 接受新lease 后 的qps
        if storeDesc.Capacity.QueriesPerSecond < minQPS { // dest 的qps 小于最小值
                if newCandidateQPS > maxQPS {  // 新的qps 大于最大值, 则不能转移lease
                        log.VEventf(ctx, 3,
                                "r%d's %.2f qps would push s%d over the max threshold (%.2f) with %.2f qps afterwards",
                                replWithStats.repl.RangeID, replWithStats.qps, candidateStore, maxQPS, newCandidateQPS)
                        return true
                }
        } else if newCandidateQPS > meanQPS { // 如果新的qps 大于平均值,也不能转移lease
                log.VEventf(ctx, 3,
                        "r%d's %.2f qps would push s%d over the mean (%.2f) with %.2f qps afterwards",
                        replWithStats.repl.RangeID, replWithStats.qps, candidateStore, meanQPS, newCandidateQPS)
                return true
        }

        return false
}

2.2.6 选定了一个副本接受者节点后,check 副本多样性得分

选定了一个副本接受者节点后,check 副本多样性得分, 如果更小,则不做副本迁移,重新选择节点做副本迁移。

副本分布多样性得分是一个分布分布的评价重要指标,计算方法后续会介绍。

代码如下:

curDiversity := rangeDiversityScore(  // 副本迁移前,根据副本位置获取多样性得分
        sr.rq.allocator.storePool.getLocalities(currentReplicas))
// 假如做了副本迁移,根据新的副本位置获取多样性得分
newDiversity := rangeDiversityScore(sr.rq.allocator.storePool.getLocalities(targetReplicas))
if newDiversity < curDiversity { // 如果迁移后的副本多样性得分更低了,则这个range 的本节点副本不能迁移
        log.VEventf(ctx, 3,
                "new diversity %.2f for r%d worse than current diversity %.2f; not rebalancing",
                newDiversity, desc.RangeID, curDiversity)
        continue
}

获得该range的所有副本的节点集合的多样性得分

代码如下:

                curDiversity := rangeDiversityScore(  // 根据副本位置获取多样性得分
                        sr.rq.allocator.storePool.getLocalities(currentReplicas)) // 获得range的所有副本的节点集合

// 获得副本涉及的所有节点的多样性得分
func rangeDiversityScore(existingNodeLocalities map[roachpb.NodeID]roachpb.Locality) float64 {
        var sumScore float64
        var numSamples int
        for n1, l1 := range existingNodeLocalities { // 双重遍历,获得所有两两组合的节点间多样性得分总和
                for n2, l2 := range existingNodeLocalities {
                        // Only compare pairs of replicas where s2 > s1 to avoid computing the
                        // diversity score between each pair of localities twice.
                        if n2 <= n1 { // 只比较 n1 > n2
                                continue
                        }
                        sumScore += l1.DiversityScore(l2) // 两个节点的多样性得分
                        numSamples++
                }
        }
        if numSamples == 0 {
                return roachpb.MaxDiversityScore
        }
        return sumScore / float64(numSamples) // 多样性得分平均值
}

2.2.7 选择要给副本作为新的leaseholder

因为我们转移的 range4-1, 它是leaseholder副本。所以我们要选择一个副本作为新的leaseholder。可以在节点1,3,4 上选择。如果在节点1,3上,则需要判断, lease 接受者副本的复制日志是否足够新。

这个很好理解,假如我们将lease 从 range4 - 1 转移到 range4 - 2 ,如果range4-2 的日志落后很多,会导致lease 迁移后range4-2 作为新的lease 缺少了一些数据,导致数据丢失。

那么怎么保证lease 接受者副本的数据足够新呢?

Kwbase 是基于raft 机制日志复制的方式做副本同步的。所以只要满足下面两个条件之一,即可认为lease接受者副本的数据是最新的。

1 如果日志接收者副本在raft 组的角色是 lead。 它的日志肯定是最新的。

2 如果日志接受者副本在raft 组的角色是 follower,且其成功复制日志已经超过lead commit 的日志。 则也可以说明日志接受者副本的日志足够新了,可以作为lease 接受者。

代码如下:

func replicaIsBehind(raftStatus *raft.Status, replicaID roachpb.ReplicaID) bool {
        if raftStatus == nil || len(raftStatus.Progress) == 0 {
                return true
        }
        // NB: We use raftStatus.Commit instead of getQuorumIndex() because the
        // latter can return a value that is less than the commit index. This is
        // useful for Raft log truncation which sometimes wishes to keep those
        // earlier indexes, but not appropriate for determining which nodes are
        // behind the actual commit index of the range.
        if progress, ok := raftStatus.Progress[uint64(replicaID)]; ok {
                if uint64(replicaID) == raftStatus.Lead ||        // 如果dest 已经是leader了, 说明dest 的日志是最新的, 则可以转移lease
                        (progress.State == tracker.StateReplicate &&  // 或dest 在复制状态,且复制已经大于了raft 组的提交 index, 则不可以转移lease
                                progress.Match >= raftStatus.Commit) {
                        return false
                }
        }
        return true
}

2.3 副本迁移动作细节

副本迁移包括两个动作,即删除要给一个副本,添加一个副本,也可以说是副本重分布,是在函数 func (s *Store) AdminRelocateRange() 中进行的。

2.3.1 删除Learners 副本

获取学习者副本,事务执行学习者副本删除

// 此函数返回后,所有剩余的副本将为VOTER_FULL类型。
func maybeLeaveAtomicChangeReplicasAndRemoveLearners(
        ctx context.Context, store *Store, desc *roachpb.RangeDescriptor,
) (*roachpb.RangeDescriptor, error) {

        learners := desc.Replicas().Learners()  // 学习者副本
        if len(learners) == 0 {
                return desc, nil
        }
        targets := make([]roachpb.ReplicationTarget, len(learners)) // 学习者副本的节点集合
        for i := range learners {
                targets[i].NodeID = learners[i].NodeID
                targets[i].StoreID = learners[i].StoreID
        }
        log.VEventf(ctx, 2, `removing learner replicas %v from %v`, targets, desc)
        // NB: unroll the removals because at the time of writing, we can't atomically
        // remove multiple learners. This will be fixed in:
        //
        // https://gitee.com/kwbasedb/kwbase/pull/40268
        origDesc := desc
        for _, target := range targets {
                var err error
                desc, err = execChangeReplicasTxn( // 事务中删除学习者副本
                        ctx, store, desc, storagepb.ReasonAbandonedLearner, "",
                        []internalReplicationChange{{target: target, typ: internalChangeTypeRemove}},
                )
                if err != nil {
                        return nil, errors.Wrapf(err, `removing learners from %s`, origDesc)
                }
        }
        return desc, nil
}

2.3.2 获取添加的副本和删除的副本集合

副本迁移本质是添加一个副本和删除一个副本,targets []roachpb.ReplicationTarget 数组包含了副本迁移后的所有副本。

例如章节 1 中,在节点4上添加一个 range4-1的副本,在节点2 上删除 range4-1 副本。则 addTargets集合中有 {node =4}, removeTargets 结合中有{node =2}。

代码如下:

func (s *Store) relocateOne(
        ...
        var addTargets []roachpb.ReplicaDescriptor  // add 副本集合
        for _, t := range targets { // 遍历 targets
                found := false
                for _, replicaDesc := range rangeReplicas { // 遍历现有的副本
                        if replicaDesc.StoreID == t.StoreID && replicaDesc.NodeID == t.NodeID {
                                found = true
                                break
                        }
                }
                if !found { // 现有的副本不存在target, 则add 副本
                        addTargets = append(addTargets, roachpb.ReplicaDescriptor{
                                NodeID:  t.NodeID,
                                StoreID: t.StoreID,
                        })
                }
        }

        var removeTargets []roachpb.ReplicaDescriptor   // remove 副本集合
        for _, replicaDesc := range rangeReplicas { // 遍历现有副本
                found := false
                for _, t := range targets { // 遍历targtes
                        if replicaDesc.StoreID == t.StoreID && replicaDesc.NodeID == t.NodeID {
                                found = true
                                break
                        }
                }
                if !found { // 如果targets 中没有这个现有副本,则现有副本要remove
                        removeTargets = append(removeTargets, roachpb.ReplicaDescriptor{
                                NodeID:  replicaDesc.NodeID,
                                StoreID: replicaDesc.StoreID,
                        })
                }
        }

2.3.3 生成添加副本, 删除副本的ops(operate)并可能为迁移leaseholder 找一个接收者副本。

从添加副本集合addTargets 中找到一个最优的节点,生成添加副本ops

代码如下:

func (s *Store) relocateOne(
        ...
        if len(addTargets) > 0 {
                // The storeList's list of stores is used to constrain which stores the
                // allocator considers putting a new replica on. We want it to only
                // consider the stores in candidateTargets.
                // storeList的存储列表用于约束分配器考虑在哪些存储上放置新副本。我们希望它只考虑candidateTargets中的存储。
                candidateDescs := make([]roachpb.StoreDescriptor, 0, len(candidateTargets))
                for _, candidate := range candidateTargets {
                        store, ok := storeMap[candidate.StoreID] // 获取添加副本的节点集合,也可能只有只有一个节点
                        if !ok {
                                return nil, nil, fmt.Errorf("cannot up-replicate to s%d; missing gossiped StoreDescriptor",
                                        candidate.StoreID)
                        }
                        candidateDescs = append(candidateDescs, *store)
                }
                storeList = makeStoreList(candidateDescs) // 节点集合

                targetStore, _ := s.allocator.allocateTargetFromList( // 获取一个 good store
                        ctx,
                        storeList, // 从这里里面选择 一个store, 作为副本接受者节点
                        zone,
                        rangeReplicas,
                        s.allocator.scorerOptions())
                if targetStore == nil {
                        return nil, nil, fmt.Errorf("none of the remaining targets %v are legal additions to %v",
                                addTargets, desc.Replicas())
                }

                target := roachpb.ReplicationTarget{ // target 设置为good store 的节点位置
                        NodeID:  targetStore.Node.NodeID, // 副本添加到这个节点
                        StoreID: targetStore.StoreID,
                }
                ops = append(ops, roachpb.MakeReplicationChanges(roachpb.ADD_REPLICA, target)...)  // ops add 副本
                // Pretend the voter is already there so that the removal logic below will
                // take it into account when deciding which replica to remove.
                rangeReplicas = append(rangeReplicas, roachpb.ReplicaDescriptor{ // add 副本
                        NodeID:    target.NodeID,
                        StoreID:   target.StoreID,
                        ReplicaID: desc.NextReplicaID,
                        Type:      roachpb.ReplicaTypeVoterFull(),
                })
        }
        
        
        

从删除副本集合removeTargets 中选择最差一个节点,生成删除副本ops;生成删除副本ops时,如果被删除的副本是leaseholder,则需要指定一个新的leaseholder,指定哪一个呢?我们首先排除本次的ops 中添加的副本,因为还未添加成功,副本还不存在。然后我们优先从 targets[0].StoreID 节点上选择副本迁移lease holder。如果targets[0].StoreID 是current lease 节点,则给第一个不是current lease 的节点。

代码如下:

        if len(removeTargets) > 0 {
                //选择要删除的复制副本。请注意,rangeReplicas可能已经反映了我们在当前一轮中添加的副本。这是正确的做法。例如,考虑从(s1,s2,s3)重新定位到(s1,s2,s4),其中addTargets将是(s4)而removeTargets是(s3)。
                //在这段代码中,我们希望分配器查看是否可以从(s1,s2,s3,s4)中删除s3,这是一个合理的请求;该副本集被过度复制。如果我们要求它从(s1,s2,s3)中删除s3,由于约束,它可能不想这样做
                //选出删除副本
                targetStore, _, err := s.allocator.RemoveTarget(ctx, zone, removeTargets, rangeReplicas)  // 选择一个bad 节点做 remove 副本
                if err != nil {
                        return nil, nil, errors.Wrapf(err, "unable to select removal target from %v; current replicas %v",
                                removeTargets, rangeReplicas)
                }
                removalTarget := roachpb.ReplicationTarget{ // 删除该副本
                        NodeID:  targetStore.NodeID,
                        StoreID: targetStore.StoreID,
                }
                // We can't remove the leaseholder, which really throws a wrench into
                // atomic replication changes. If we find that we're trying to do just
                // that, we need to first move the lease elsewhere. This is not possible
                // if there is no other replica available at that point, i.e. if the
                // existing descriptor is a single replica that's being replaced.
                // 我们不能取消承租人,这真的会给原子复制变化带来麻烦。如果我们发现我们正试图这样做,我们需要首先将租约转移到其他地方。
                //如果此时没有其他可用的副本,即如果现有的描述符是正在被替换的单个副本,则这是不可能的。
                var b kv.Batch
                liReq := &roachpb.LeaseInfoRequest{}  // 获取 该range 的lease
                liReq.Key = desc.StartKey.AsRawKey()
                b.AddRawRequest(liReq)
                if err := s.DB().Run(ctx, &b); err != nil {
                        return nil, nil, errors.Wrap(err, "looking up lease")
                }
                curLeaseholder := b.RawResponse().Responses[0].GetLeaseInfo().Lease.Replica
                ok := curLeaseholder.StoreID != removalTarget.StoreID  // 要remove 的副本不是lease。
                if !ok { // 如果要删除的是lease 副本, 要要迁移
                        // Pick a replica that we can give the lease to. We sort the first
                        // target to the beginning (if it's there) because that's where the
                        // lease needs to be in the end. We also exclude the last replica if
                        // it was added by the add branch above (in which case it doesn't
                        // exist yet).
                        //选择一个我们可以授予租约的副本。我们将第一个目标排序到开头(如果它在那里),因为这是租约最后需要的地方。
                        //如果最后一个副本是由上面的add分支添加的(在这种情况下,它还不存在),我们也会将其排除在外。
                        sortedTargetReplicas := append([]roachpb.ReplicaDescriptor(nil), rangeReplicas[:len(rangeReplicas)-len(ops)]...)
                        sort.Slice(sortedTargetReplicas, func(i, j int) bool { // 排序, [0]节点的在前面, 尝试将lease 给[0]节点; 但如果[0]节点是curleaseholder,则给第一个不是curleaseholder的节点
                                sl := sortedTargetReplicas
                                // targets[0] goes to the front (if it's present).
                                return sl[i].StoreID == targets[0].StoreID
                        })
                        for _, rDesc := range sortedTargetReplicas {
                                if rDesc.StoreID != curLeaseholder.StoreID {  // 第一个不是 cur lease 所在的节点, 作为lease 的接受节点
                                        transferTarget = &roachpb.ReplicationTarget{
                                                NodeID:  rDesc.NodeID,
                                                StoreID: rDesc.StoreID,
                                        }
                                        ok = true
                                        break
                                }
                        }
                }

                // Carry out the removal only if there was no lease problem above. If
                // there was, we're not going to do a swap in this round but just do the
                // addition. (Note that !ok implies that len(ops) is not empty, or we're
                // trying to remove the last replica left in the descriptor which is
                // illegal).
                if ok { // 不是lease, 则给与一个 remove op
                        ops = append(ops, roachpb.MakeReplicationChanges(  // ops remove 副本
                                roachpb.REMOVE_REPLICA,
                                removalTarget)...)
                }
        }
        
        
        
// 迁移lease 到上面选定的transferTarget 节点
        ops, leaseTarget, err = s.relocateOne(ctx, &rangeDesc, targets) // 分配一个
}
if err != nil {
        return err
}
if leaseTarget != nil {
        // NB: we may need to transfer even if there are no ops, to make
        // sure the attempt is made to make the first target the final
        // leaseholder. 即使没有运营,我们也可能需要转移,以使第一个目标成为最终的承租人。
        if err := transferLease(*leaseTarget); err != nil { // 将lease 迁移到这里
                break
        }
}

2.3.4 发送迁移副本请求

将迁移副本请求发送给该range 的lease holder, 如下所示,key 是该range 的start key。遍历ops 调用副本change接口; 对每个ops 构造副本change 请求; 对每个ops 通过db接口发送请求,发送到指定副本上。

// 遍历ops 调用副本change接口
tartKey := rangeDesc.StartKey.AsRawKey() // range 的key
for _, ops := range opss { // 遍历ops,  add, remove  , 因为lease 已经迁移了,所以直接做 add remove 即可
        newDesc, err := s.DB().AdminChangeReplicas(ctx, startKey, rangeDesc, ops, needTsSnapshotData)
        

// 对每个ops 构造副本change 请求
req := &roachpb.AdminChangeReplicasRequest{  // 发送到分布式节点,执行相应动作, add 副本或remove 副本
        RequestHeader: roachpb.RequestHeader{
                Key: k,
        },
        ExpDesc:            expDesc,
        NeedTsSnapshotData: needTsSnapshotData,
}
req.AddChanges(chgs...)


// 对每个ops 通过db接口发送请求
b.adminChangeReplicas(key, expDesc, chgs, needTsSnapshotData)
if err := getOneErr(db.Run(ctx, b), b); err != nil { // 将 replica 变更请求发给对应的节点; 对应的节点就会做副本add, 副本remove 响应的操作;后续阅读代码
        return nil, err
}

2.3.5 Range 的leaseholder 副本处理change 副本请求(AdminChangeReplicasRequest)

AdminChangeReplicasRequest 发送到Range 的leaseholder 副本,副本上处理逻辑是在函数中 func (r *Replica) executeAdminBatch 中的 case *roachpb.AdminChangeReplicasRequest: 中执行的。

具体来说,如果范围的初始成员是s1/1、s2/2和s3/3,并且原子成员资格更改是添加s4/4和s5/5,同时删除s1/1和s2/2,则以下范围描述符将形成整体转换:

1. s1/1 s2/2 s3/3 (VOTER_FULL is implied) -- 原始副本都是voter 副本

2. s1/1 s2/2 s3/3 s4/4LEARNER -- 添加s4/4 副本为 learner 副本

3. s1/1 s2/2 s3/3 s4/4LEARNER s5/5LEARNER -- 添加s5/5 副本为 learner 副本

4. s1/1VOTER_DEMOTING s2/2VOTER_DEMOTING s3/3 s4/4VOTER_INCOMING s5/5VOTER_INCOMING --s1/1 s2/2 降级,s4/4, s5/5 升级

5. s1/1LEARNER s2/2LEARNER s3/3 s4/4 s5/5 --s1/1 s2/2 变为learner ; s4/4, s5/5 变为voter

6. s2/2LEARNER s3/3 s4/4 s5/5 -- 删除 s1/1 副本

7. s3/3 s4/4 s5/5 -- 删除 s2/2 副本

2.3.5.1 添加leaner 副本

如2.3.5 中的步骤 2,3 添加 s4/4LEARNER s5/5LEARNER 两个learner, 获取添加副本信息adds,事务执行添加learner 副本

adds := chgs.Additions()
desc, err = addLearnerReplicas(ctx, r.store, desc, reason, details, adds) // 添加 leaner
// addLearnerReplicas adds learners to the given replication targets.
func addLearnerReplicas(
        ctx context.Context,
        store *Store,
        desc *roachpb.RangeDescriptor,
        reason storagepb.RangeLogEventReason,
        details string,
        targets []roachpb.ReplicationTarget,
) (*roachpb.RangeDescriptor, error) {
        // TODO(tbg): we could add all learners in one go, but then we'd need to
        // do it as an atomic replication change (raft doesn't know which config
        // to apply the delta to, so we might be demoting more than one voter).
        // This isn't crazy, we just need to transition out of the joint config
        // before returning from this method, and it's unclear that it's worth
        // doing.
        for _, target := range targets {
                iChgs := []internalReplicationChange{{target: target, typ: internalChangeTypeAddLearner}}
                var err error
                desc, err = execChangeReplicasTxn( // 事务执行change 副本
                        ctx, store, desc, reason, details, iChgs,
                )
                if err != nil {
                        return nil, err
                }
        }
        return desc, nil
}

2.3.5.2 新添加的副本以leaner 角色接受快照,补全数据
func (r *Replica) atomicReplicationChange
......
        for _, target := range chgs.Additions() { // 添加的副本
                iChgs = append(iChgs, internalReplicationChange{target: target, typ: internalChangeTypePromoteLearner})
                // All adds must be present as learners right now, and we send them
                // snapshots in anticipation of promoting them to voters. 所有添加的内容现在都必须以学习者的身份出现,我们给他们发快照,以期向选民推广。
                rDesc, ok := desc.GetReplicaDescriptor(target.StoreID) // 获取target节点副本
                if !ok {
                        return nil, errors.Errorf("programming error: replica %v not found in %v", target, desc)
                }

                if rDesc.GetType() != roachpb.LEARNER {
                        return nil, errors.Errorf("programming error: cannot promote replica of type %s", rDesc.Type)
                }

                if fn := r.store.cfg.TestingKnobs.ReplicaSkipLearnerSnapshot; fn != nil && fn() {
                        continue
                }

                if desc.GetRangeType() == roachpb.DEFAULT_RANGE { // 向 target 副本发送快照
                        if err := r.sendSnapshot(ctx, rDesc, SnapshotRequest_LEARNER, priority); err != nil {
                                return nil, err
                        }
                }
        }

2.3.5.2.1 向副本发送数据实现

本地副本会获取本地副本的快照,通过rpc 的方式将快照发给learner副本,

代码如下:

func (r *Replica) sendSnapshot(
        ctx context.Context,
        recipient roachpb.ReplicaDescriptor,   // 接收者副本
        snapType SnapshotRequest_Type,
        priority SnapshotRequest_Priority,
) (retErr error) {
        defer func() {
                // Report the snapshot status to Raft, which expects us to do this once we
                // finish sending the snapshot.
                r.reportSnapshotStatus(ctx, recipient.ReplicaID, retErr)
        }()

        snap, err := r.GetSnapshot(ctx, snapType, recipient.StoreID) // 1 获取快照
        if err != nil {
                return errors.Wrapf(err, "%s: failed to generate %s snapshot", r, snapType)
        }
        defer snap.Close()
        log.Event(ctx, "generated snapshot")

        sender, err := r.GetReplicaDescriptor()  // 2 发送者副本
        ..........
        
        req := SnapshotRequest_Header{ // 3 快照消息头
                State: snap.State,
                // Tell the recipient whether it needs to synthesize the new
                // unreplicated TruncatedState. It could tell by itself by peeking into
                // the data, but it uses a write only batch for performance which
                // doesn't support that; this is easier. Notably, this is true if the
                // snap index itself is the one at which the migration happens.
                //
                // See VersionUnreplicatedRaftTruncatedState.
                UnreplicatedTruncatedState: !usesReplicatedTruncatedState,
                RaftMessageRequest: RaftMessageRequest{
                        RangeID:     r.RangeID,
                        FromReplica: sender,  // 发送者副本
                        ToReplica:   recipient,  // 接受者副本
                        Message: raftpb.Message{ // 快照消息
                                Type:     raftpb.MsgSnap,
                                To:       uint64(recipient.ReplicaID),
                                From:     uint64(sender.ReplicaID),
                                Term:     status.Term,
                                Snapshot: snap.RaftSnap,  // 快照数据
                        },
                },
                RangeSize: r.GetMVCCStats().Total(),
                // Recipients currently cannot choose to decline any snapshots.
                // In 19.2 and earlier versions pre-emptive snapshots could be declined.
                //
                // TODO(ajwerner): Consider removing the CanDecline flag.
                CanDecline: false,
                Priority:   priority,
                Strategy:   SnapshotRequest_KV_BATCH,
                Type:       snapType,
        }
        sent := func() {
                r.store.metrics.RangeSnapshotsGenerated.Inc(1)
        }
        if err := r.store.cfg.Transport.SendSnapshot( // 4 向副本发送数据
                ctx,
                &r.store.cfg.RaftConfig,
                r.store.allocator.storePool,
                req,
                snap,
                r.store.Engine().NewBatch,
                sent,
        ); 

2.3.6 对删除的副本降级操作; 对添加的副本升级操作

对删除的副本(现在是voter)降级操作,降级为learner; 对添加的副本(现在是leaner副本)升级操作,升级为voter, 如2.3.5 中的步骤4,5所示。

代码如下:

for _, target := range chgs.Additions() { // 添加的副本
        iChgs = append(iChgs, internalReplicationChange{target: target, typ: internalChangeTypePromoteLearner})
        
canUseDemotion := r.store.ClusterSettings().Version.IsActive(ctx, clusterversion.VersionChangeReplicasDemotion)
for _, target := range chgs.Removals() { // 删除的副本
        typ := internalChangeTypeRemove
        if rDesc, ok := desc.GetReplicaDescriptor(target.StoreID); ok && rDesc.GetType() == roachpb.VOTER_FULL && canUseDemotion { // 获取删除的副本
                typ = internalChangeTypeDemote
        }
        iChgs = append(iChgs, internalReplicationChange{target: target, typ: typ})
}

    case internalChangeTypePromoteLearner:
            typ := roachpb.VOTER_FULL
            if useJoint {
                    typ = roachpb.VOTER_INCOMING
            }
            rDesc, prevTyp, ok := updatedDesc.SetReplicaType(chg.target.NodeID, chg.target.StoreID, typ) // 升级副本为 VOTER_FULL
            if !ok || prevTyp != roachpb.LEARNER {
                    return nil, errors.Errorf("cannot promote target %v which is missing as Learner", chg.target)
            }
            updatedDesc.SetReplicaTag(chg.target.NodeID, chg.target.StoreID, rangetyp)
            added = append(added, rDesc)
            
            
    case internalChangeTypeDemote:
            // Demotion is similar to removal, except that a demotion
            // cannot apply to a learner, and that the resulting type is
            // different when entering a joint config. 降级类似于删除,除了降级不能应用于学习者,并且在输入联合配置时产生的类型不同。
            rDesc, ok := updatedDesc.GetReplicaDescriptor(chg.target.StoreID) // 获取要删除的副本
            if !ok {
                    return nil, errors.Errorf("target %s not found", chg.target)
            }
            if !useJoint {
                    // NB: this won't fire because cc.useJoint() is always true when
                    // there's a demotion. This is just a sanity check.
                    return nil, errors.Errorf("demotions require joint consensus")
            }
            if prevTyp := rDesc.GetType(); prevTyp != roachpb.VOTER_FULL {
                    return nil, errors.Errorf("cannot transition from %s to VOTER_DEMOTING", prevTyp)
            }
            rDesc, _, _ = updatedDesc.SetReplicaType(chg.target.NodeID, chg.target.StoreID, roachpb.VOTER_DEMOTING)  // 设置副本降级为 VOTER_DEMOTING
            updatedDesc.SetReplicaTag(chg.target.NodeID, chg.target.StoreID, rangetyp)
            removed = append(removed, rDesc)

2.3.7 删除学习者副本

如2.3.5 所示删除学习者副本 s1/1LEARNER s2/2LEARNER, 即2.3.1 中的函数 maybeLeaveAtomicChangeReplicasAndRemoveLearners。

代码如下:

func (r *Replica) atomicReplicationChange(
   ......
    // Leave the joint config if we entered one. Also, remove any learners we
    // might have picked up due to removal-via-demotion.
    return maybeLeaveAtomicChangeReplicasAndRemoveLearners(ctx, r.store, desc)  // 删除学习者副本

2.3.8 Change 副本到底做了什么

比如添加一个副本,其实是事务执行了要给添加副本的逻辑。逻辑如下:

1 本地准备要修改的 range 描述符信息 RangeDescriptor

2 更新range描述符动做写入batch 中

3 事务执行中执行 batch

4 将副本更改记录到范围事件日志中。

5 更新范围描述符寻址记录,即更新meta1信息

6 事务提交

代码如下:

1 本地准备要修改的 range 描述符信息 RangeDescriptor

// Note that we are now using the descriptor from KV, not the one passed
// into this method.  请注意,我们现在使用的是KV中的描述符,而不是传递给此方法的描述符。 1 本地准备要修改的 range 描述符信息 RangeDescriptor
crt, err := prepareChangeReplicasTrigger(ctx, store, desc, chgs)
if err != nil {
        return err
        
///
case internalChangeTypeAddLearner:
        added = append(added,
                updatedDesc.AddReplica(chg.target.NodeID, chg.target.StoreID, roachpb.LEARNER))
        updatedDesc.SetReplicaTag(chg.target.NodeID, chg.target.StoreID, rangetyp)

2 更新range描述符动做写入batch 中

if err := updateRangeDescriptor(b, descKey, dbDescValue, crt.Desc); err != nil { // 2 更新range描述符动做写入batch 中

func updateRangeDescriptor(
        b *kv.Batch, descKey roachpb.Key, oldValue *roachpb.Value, newDesc *roachpb.RangeDescriptor,  // 更新RangeDescriptor
) error {
        // This is subtle: []byte(nil) != interface{}(nil). A []byte(nil) refers to
        // an empty value. An interface{}(nil) refers to a non-existent value. So
        // we're careful to construct interface{}(nil)s when newDesc/oldDesc are nil.
        var newValue interface{}
        if newDesc != nil {
                if err := newDesc.Validate(); err != nil {
                        return errors.Wrapf(err, "validating new descriptor %+v (old descriptor is %+v)",
                                newDesc, oldValue)
                }
                newBytes, err := protoutil.Marshal(newDesc)
                if err != nil {
                        return err
                }
                newValue = newBytes
        }
        b.CPut(descKey, newValue, oldValue)  // batch 中加入put请求
        return nil
}
                 
                        
                        

3 事务执行中执行 batch

if err := txn.Run(ctx, b); err != nil { // 3 事务执行中执行 batch
        return err
}

4 将副本更改记录到范围事件日志中。

// Log replica change into range event log. 4 将副本更改记录到范围事件日志中。
for _, tup := range []struct {
        typ      roachpb.ReplicaChangeType
        repDescs []roachpb.ReplicaDescriptor
}{
        {roachpb.ADD_REPLICA, crt.Added()},
        {roachpb.REMOVE_REPLICA, crt.Removed()},
} {
        for _, repDesc := range tup.repDescs {
                if err := store.logChange( // 记录日志
                        ctx, txn, tup.typ, repDesc, *crt.Desc, reason, details,
                ); err != nil {
                        return err
                }
        }
}

5 更新范围描述符寻址记录,即更新meta1信息

    // End the transaction manually instead of letting RunTransaction
    // loop do it, in order to provide a commit trigger. 手动结束事务,而不是让RunTransaction循环来完成,以提供提交触发器。
    b := txn.NewBatch()

    // Update range descriptor addressing record(s). 5 更新范围描述符寻址记录。
    if err := updateRangeAddressing(b, crt.Desc); err != nil {
            return err
    }

6 事务提交

b.AddRawRequest(&roachpb.EndTxnRequest{
        Commit: true,
        InternalCommitTrigger: &roachpb.InternalCommitTrigger{
                ChangeReplicasTrigger: crt,  // 事务提交的时候,执行
        },
})
if err := txn.Run(ctx, b); err != nil { // 6 事务提交
        log.Event(ctx, err.Error())
        return err
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值