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

被折叠的 条评论
为什么被折叠?



