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包中对模式,类型,解码,编码和转换的一些工具进行了封装。
使用方法
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的实现采用了类递归的方式,不方便理解,但大体思路上也是如此。