K8s源码1-LabelSelector-Selector

标签
类型
技术
创建时间
Jan 13, 2023 22:21
打算做一个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) boolEmpty() boolString() stringAdd(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的实现采用了类递归的方式,不方便理解,但大体思路上也是如此。