Placement Rules 原理

【是否原创】是
【首发渠道】TiDB 社区

Placement rule 格式

系统的 placement-Rule 定义包括两个部分:ruleGroup 和 Rule,它们的关系是同一个 ruleGroup 下可以有多个 rule 规则,如下显示 ruleGroup ID 为 pd 的 rules 配置信息,上部分内容是 ruleGroup 信息,下面的 rules 是属于当前 ruleGroup 的 rule 列表。

» config placement-rules rule-bundle get pd
{
  "group_id": "pd",
  "group_index": 0,
  "group_override": false,
  "rules": [
    {
      "group_id": "pd",
      "id": "default",
      "start_key": "",
      "end_key": "",
      "role": "voter",
      "count": 3
    }
  ]
}

ruleGroup 只有三个 json 字段:

id:代表 ruleGroup 的唯一性 id、名称, string 类型,跟包含的 rule 规则下的 group_id 相同
index:各个 ruleGroup 之前的序号,int 类型,默认值为 0。index 越小,匹配的越早;另外就是 index 高的 ruleGroup 根据 override 配置如果为 true, 那么低 index 的 ruleGroup 规则将不会生效。
override:默认值 false,如果设置为 true,那么 index 较低的 ruleGroup 将不会生效

rule 有以下 json 字段:

group_id:ruleGroup 的 id,string 类型
id:同一个 ruleGroup 下面 rule 的唯一性 id,string 类型
index: rule 在当前 ruleGroup 下的序号,int 类型。 index 越小,匹配的越早;高 index 的 rule 根据 override 配置如果为 true, 那么低 index 的 rule 规则将不会生效。
override:布尔类型,如果设置为 true,那么 index 较低的 rule 将不会生效
start_key:rule 匹配的开始 key
end_key:rule 匹配的结束 key
role:string 类型,可以为 voter、leader、 follower、learner
count:目标 peer 数量,比如定义 3 个 tikv 副本,设置为 3.
label_constraints:这是个 json 数组,每个成员一个规则,定义了根据 label 筛选可用 store 的规则
location_labels:是 json string 数组类型,同 pd.replication.location-labels,用户表示 store 的物理标签,例如 ["zone", "rack", "host"]
isolation_level:string 类型,强制性的物理 store 隔离级别,比如 location_labels:["zone", "rack", "host"], 而 isolation_level:"rack",那么 region peer 物理上不能在相同 rack 的 store 上

其中 [group_id, id] 确定了 rule 的唯一性。

label_constraints 是 json 数组,单个 item 具有以下字段:

key: store 的 label 名字,string 类型
Op: string 类型,可以为 "in"、"notIn"、"exists"、"notExists" 
Values: string 字符串
  • in: 满足条件的 store 的 label 值是 values 其中一个
  • notIn: 与 in 相反,label 值不在 values 列表里
  • exists:满足条件的 store 具有这个 label
  • notExists: 与 exists 相反,满足条件的 store 不具有这个 label

rule 生效算法

rule 排序规则

对所有的 rule 会根据 [GroupIndex, GroupID, Index, ID] 进行排序,来决定先后关系。

// Rules are ordered by (GroupID, Index, ID).
// 比较顺序 [GroupIndex, GroupID, Index, ID].
func compareRule(a, b *Rule) int {
	switch {
	case a.groupIndex() < b.groupIndex():
		return -1
	case a.groupIndex() > b.groupIndex():
		return 1
	case a.GroupID < b.GroupID:    // GroupID 和 ID 都是 string 啊。
		return -1
	case a.GroupID > b.GroupID:
		return 1
	case a.Index < b.Index:
		return -1
	case a.Index > b.Index:
		return 1
	case a.ID < b.ID:
		return -1
	case a.ID > b.ID:
		return 1
	default:
		return 0
	}
}

这里注意排序中用到 groupID 和 id 两个字段,但是这两个字段是 string 类型。

rule 配置生效规则

通过 rule 排序函数 compareRule 对所有适用于 [start_key, end_key] 的 rules 进行排序后,对增序的 rule 数组进行生效检查,检查按照以下准则(排序在后的可以理解为优先级高):

  1. ruleGroup 之间,高优先级的 ruleGroup 如果设置了 override 为 true,那么低优先级的 ruleGroup 所有 rule 都不会生效
  2. 同 ruleGroup 内,高优先级的 rule 如果设置了 override 为 true,那么同 ruleGroup 低优先级 rule 都不会生效
  3. 如果 1、2 没有 override 为 ture,那么低优先级的也会生效

举个例子:

ruleA: group_id: 4, id: 2, override: true
ruleB: group_id: 4, id: 1, override: true
ruleC: group_id: 3
ruleD: group_id: 2
RuleGroupA: id:4, override: false
RuleGroupB: id:3, override: true
RuleGroupC: id:2, override: false
最后生效的只有 ruleA 和 ruleC.
  1. RuleGroupA、RuleGroupB、RuleGroupC 都没有设置 index, 那么默认值为 0,index 相同
  2. 由于 index 相同,根据 [GroupIndex, GroupID, Index, ID] 的比较顺序,那么 rule 排序后会是 [ruleD, ruleC, ruleB, ruleA] 的先后顺序
  3. ruleC 的 groupID 为 3, 所属的 RuleGroupB 将 override 设置 为 ture, 那么 ruleC 前面的 ruleD 属于 RuleGroupC 将不会生效
  4. ruleB 的 groupID 为 4,所属的 RuleGroupA override 为 false,那么 ruleC 会生效
  5. 对于同一个 ruleGroupA 内部,由于 ruleA 设置了 override: true,它使得 ruleB 不生效

所以最后将会以 [ruleC, ruleA] 的顺序应用这两个规则。

func prepareRulesForApply(rules []*Rule) []*Rule {
	var res []*Rule
	var i, j int
	for i = 1; i < len(rules); i++ {
		if rules[j].GroupID != rules[i].GroupID {
			if rules[i].group != nil && rules[i].group.Override {
				res = res[:0] // override all previous groups
			} else {
				res = append(res, rules[j:i]...) // save rules belong to previous group
			}
			j = i
		}
		if rules[i].Override {
			j = i // skip all previous rules in the same group
		}
	}
	return append(res, rules[j:]...)
}

Placement rule 配置更新

rule 是以 key-value 形式持久化在 storage 上了,key 为 groupID + ruleId 的编码。

SetRule、DeleteRule:只更新当前 rule,修改、创建、删除等,不会对所属同样 ruleGroup 其他 rule 改变。

SetRules:批量更新多个 rule,同样的不会对所属同样 ruleGroup 其他 rule 改变。

SetGroupBundle:覆盖更新 ruleGroup,把相同 ruleGroup 原来的 rule 删掉,再插入新 rule,同时更新 ruleGroup.

Range 适用规则维护

  1. 基于 rule 的 [start_key, end_key],将 start_key、end_key 作为分隔点,分成连续的 range 段
  2. 计算不同 range 段需要匹配适用的 rule 规则列表
  3. 当有 region 需要检查的时候,根据 region 的 range 范围从维护的 range 规则中查找 rule 规则

Rule fit 优选算法

实质上就是遍历所有的可能性,在每一种可能性的情况下,根据优选算法,选择最优的方案:

// Pick the most suitable peer combination for the rule.
// Index specifies the position of the rule.
// returns true if it replaces `bestFit` with a better alternative.
func (w *fitWorker) fitRule(index int) bool {
	if index >= len(w.rules) {
		return false
	}

	var candidates []*fitPeer
	if checkRule(w.rules[index], w.stores) {
		// Only consider stores:
		// 1. Match label constraints
		// 2. Role match, or can match after transformed.
		// 3. Not selected by other rules.
		for _, p := range w.peers {    // 以这条规则去检查当前的peer.
			if MatchLabelConstraints(p.store, w.rules[index].LabelConstraints) &&
				p.matchRoleLoose(w.rules[index].Role) &&
				!p.selected {
				candidates = append(candidates, p)
			}
		}
	}

	count := w.rules[index].Count
	if len(candidates) < count {    // 这里都没有考虑down情况。
		count = len(candidates)
	}
	return w.enumPeers(candidates, nil, index, count)
}

// Recursively traverses all feasible peer combinations.
// For each combination, call `compareBest` to determine whether it is better
// than the existing option.
// Returns true if it replaces `bestFit` with a better alternative.
func (w *fitWorker) enumPeers(candidates, selected []*fitPeer, index int, count int) bool {
	if len(selected) == count {
		// We collect enough peers. End recursive.
		return w.compareBest(selected, index)
	}

	var better bool
	// 循环递归遍历出所有可能性。
	for i, p := range candidates {    // 递归循环。
		p.selected = true
		better = w.enumPeers(candidates[i+1:], append(selected, p), index, count) || better
		p.selected = false
	}
	return better

}

优选比较算法:

func compareRuleFit(a, b *RuleFit) int {
	switch {
	case len(a.Peers) < len(b.Peers):
		return -1
	case len(a.Peers) > len(b.Peers):
		return 1
	case len(a.PeersWithDifferentRole) > len(b.PeersWithDifferentRole):
		return -1
	case len(a.PeersWithDifferentRole) < len(b.PeersWithDifferentRole):
		return 1
	case a.IsolationScore < b.IsolationScore:
		return -1
	case a.IsolationScore > b.IsolationScore:
		return 1
	default:
		return 0
	}
}

这里算法感觉还是有挺多疑惑的!!!

  • fitRule 函数是从目前 region 的 peer 中,对比选择最优的方案,没有考虑非 peer 的其他 store.
  • fitRule 函数没有判断 peer store 的情况,有可能某个 peer store 有问题,但是计算过程没有考虑.
  • fitRule 函数原来 peer 数为 4 个,配置的副本数 为 3 个,计算过程中只要求 peer count 为 3,那会多一个,被随机的选定,然后被清理吧
  • compareRuleFit 最优算法,由于先后 rule 列表中,每个 rule 选择都不能比以前差,并没法保证全局最优。

Region 调度

region check 函数

// Check checks if the region matches placement rules and returns Operator to
// fix it.
func (c *RuleChecker) Check(region *core.RegionInfo) *operator.Operator {
	...
	fit := c.cluster.FitRegion(region)
	if len(fit.RuleFits) == 0 {
		...
		// If the region matches no rules, the most possible reason is it spans across
		// multiple rules.
		return c.fixRange(region)
	}
	op, err := c.fixOrphanPeers(region, fit)
	if err == nil && op != nil {
		return op
	}
	log.Debug("fail to fix orphan peer", errs.ZapError(err))
	for _, rf := range fit.RuleFits {
		op, err := c.fixRulePeer(region, fit, rf)
		if err != nil {
			log.Debug("fail to fix rule peer", zap.String("rule-group", rf.Rule.GroupID), zap.String("rule-id", rf.Rule.ID), errs.ZapError(err))
			continue
		}
		if op != nil {
			return op
		}
	}
	return nil
}
  1. 基于现在的 region peer,调用 fitRegion 函数,在 peer 之间重新分配角色,选择最优的

  2. 对于 region 没有匹配到 rule 规则,会对 rule 进行分割

  3. 如果所有 rule 规则副本数都满足,那么清理多余的副本(orphan peer),但是检查 rule 是否满足是简单通过 peer 数是否满足副本定义,没有判断 peer 是否正常等,所以感觉这方法不成熟

  4. 根据 rule 排序后的列表,从前向后,依次调度使其满足 rule 规则。

    • 如果 rule 副本数不够,添加副本
    • 对于 down peer、offline peer,替换副本
    • 对于 peer 角色变化的,进行角色转移
      • leaner提升为voter
      • voter 提升为 leader
      • leader 降级为 follower,这里是用 leader 转移的方法实现的,找到一个可以作为 leader 的,通过从旧 leader 转移到新 leader 方法实现,防止长时间 raft 集群没有 leader 吧,但是这里找新 leader 是从现有 peer 判断的,没有从 fitRegion 最优中找
    • 为当前的 rule,从非 peer store 中尝试选择个更好的 peer store,替换已有的 peer.

    region check 函数对于 rule 规则,总是从前向后依次匹配,只要第一个 rule 规则没有适配好,那总是在适配第一个,所以在定义 rule 的时候,这一点必须要明晰。

balance leader 函数

实质就是比较提升 leader 以后,调用 fitRegion,看目标得分,选择最高分的方案,不怎么好用啊。

func (f *ruleLeaderFitFilter) Target(opt *config.PersistOptions, store *core.StoreInfo) bool {
	targetPeer := f.region.GetStorePeer(store.GetID())
	if targetPeer == nil {
		log.Warn("ruleLeaderFitFilter couldn't find peer on target Store", zap.Uint64("target-store", store.GetID()))
		return false
	}
	copyRegion := createRegionForRuleFit(f.region.GetStartKey(), f.region.GetEndKey(),
		f.region.GetPeers(), f.region.GetLeader(),
		core.WithLeader(targetPeer))
	newFit := f.fitter.FitRegion(copyRegion)
	return placement.CompareRegionFit(f.oldFit, newFit) <= 0
}

balance region 函数

执行 region peer 替换后,调用 fitRegion 函数计算得分,判断是否要替换。

func (f *ruleFitFilter) Target(opt *config.PersistOptions, store *core.StoreInfo) bool {
	region := createRegionForRuleFit(f.region.GetStartKey(), f.region.GetEndKey(),
		f.region.GetPeers(), f.region.GetLeader(),
		core.WithReplacePeerStore(f.srcStore, store.GetID()))
	newFit := f.fitter.FitRegion(region)
	return placement.CompareRegionFit(f.oldFit, newFit) <= 0
}

其他

例如 region merger 等其他调度器,没有考虑 rule 信息等。
调度算法有很多场景感觉还没考虑到,不够完善。
使用的话尽量让 rule 规则简单,不要太复杂。
使用体验不错的方式:alter table t set tiflash replica 2 .