K8s源码1-LabelSelector-Selector

打算做一个k8s源码阅读系列。k8s在云原生领域一骑绝尘,也有很多专业人士发表了k8s源码剖析相关的文章,所以此系列文章仅仅记录个人学习与收获。

起因是最近做的项目中,自己的工程能力以及架构能力进入了一个瓶颈期,因此打算通过阅读优秀的项目代码,来拓展一下视野。

官方文档介绍:

标签(Labels) 是附加到 Kubernetes 对象(比如 Pod)上的键值对。 标签旨在用于指定对用户有意义且相关的对象的标识属性,但不直接对核心系统有语义含义。 标签可以用于组织和选择对象的子集。标签可以在创建时附加到对象,随后可以随时添加和修改。 每个对象都可以定义一组键/值标签。每个键对于给定对象必须是唯一的。

"labels": {
   "key1" : "value1",
    "key2" : "value2"
   }

接下来将简单分析一下k8s如何进行label match,以下代码基于releases v1.26.0

label selector位于k8s.io/apimachinery/label中,apimachinery包中对模式,类型,解码,编码和转换的一些工具进行了封装。

image-20230114173035272

使用方法

label selector使用方法如下

// test文件中调用方法,给一个Set x:y和现有的标签"x=y"进行匹配,显然可以看出,匹配成功
expectMatch(t, "x=y", Set{"x": "y"})


// ls Set是匹配目标,selector是现有标签
func expectMatch(t *testing.T, selector string, ls Set) {
  // 这里将selector x=y解析成Selector,解析Parse的实现在下一篇文章讲,Selector会在后文讲。
	lq, err := Parse(selector)
	if err != nil {
		t.Errorf("Unable to parse %v as a selector\n", selector)
		return
	}
  // Selector有一个方法matches,进行匹配
	if !lq.Matches(ls) {
		t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls)
	}
}

源码分析

labels.go文件,这里定义了一个labels接口和Set map。Set实现了Labels接口。比较简单,之后selector会用到这个结构。

// labels.go

// Labels allows you to present labels independently from their storage.
type Labels interface {
	Has(label string) (exists bool)

	Get(label string) (value string)
}

type Set map[string]string

// .. 省略Set的接口实现

selector.go文件,定义了Selector接口,Selector用来进行label的匹配。其中比较重要的是Matches方法。

// selector.go
// Selector represents a label selector.
type Selector interface {
	Matches(Labels) bool

	Empty() bool

	String() string

	Add(r ...Requirement) Selector

	// ... 省略了一些方法
}

接下来是重点,internalSelector 实现了Selector接口,是一个Requirement数组。

// selector.go

// Requirement是对标签匹配的一个抽象。在internalSelector进行匹配的时候,会遍历所有的Requirement,将目标label和每一个Requirement进行Match匹配。
type Requirement struct {
  // 键
	key      string
  
 	// == != exists等运算符
	operator selection.Operator
  // 值
	strValues []string
}

// internalSelector是默认的Selector,实现and运算,即要满足每一个Reqirement的匹配。有一个匹配失败,则整体匹配失败。
type internalSelector []Requirement

func (s internalSelector) Matches(l Labels) bool {
	for ix := range s {
		if matches := s[ix].Matches(l); !matches {
			return false
		}
	}
	return true
}

func NewSelector() Selector {
	return internalSelector(nil)
}

internalSelector的Matches方法调用了每一个Requirement的Matches,将给定的key value对和现有的key value(Requirement)进行匹配。

那么Requirement如何进行Match的呢?这里也比较简单

// 这里根据每一种运算类型,分别进行匹配,调用了Labels的has方法,Labels接口在上文有提到。
func (r *Requirement) Matches(ls Labels) bool {
	switch r.operator {
	case selection.In, selection.Equals, selection.DoubleEquals:
		if !ls.Has(r.key) {
			return false
		}
		return r.hasValue(ls.Get(r.key))
	case selection.NotIn, selection.NotEquals:
		if !ls.Has(r.key) {
			return true
		}
		return !r.hasValue(ls.Get(r.key))
	case selection.Exists:
		return ls.Has(r.key)

  // ... 省略一些case
    
	default:
		return false
	}
}

现在进行匹配的逻辑已经清楚了,接下来要解决的是如何将给定的Label解析成Requirement,“喂”给Selector,下一篇文章进行分析。

总结

结合实际总结一下,在应用中,每个Pod等资源都有自己的LabelSelector,当通过Api Server进行筛选标签查询时,Selector的Requirement就是Pod已有的Labels,通过Selector.Matches()方法将Selector的每一个Labels(抽象为Requirement)和给定的Labels进行匹配。

在代码编写的时候,当有一些业务由许多个同类项组成时,可以将每一个同类项进行抽象封装,分而治之,由“不稳定”的输入,到“稳定”的输出。

k8s早期源码中selector的实现采用了类递归的方式,不方便理解,但大体思路上也是如此。

这个系列的帖子