├── README.md ├── apimachinery └── runtime │ ├── RecognizingDecoder.md │ ├── Serializer.md │ └── StreamSerializer.md ├── client-go.md ├── client-go ├── README.md ├── rest │ ├── Client.md │ ├── Config.md │ ├── README.md │ └── Request.md └── tools │ ├── README.md │ └── cache │ ├── Controller.md │ ├── ListerWatcher.md │ ├── README.md │ ├── SharedIndexInformer.md │ ├── SharedIndexInformerFlow.png │ └── controller.jpeg ├── controller ├── ControllerExpectations.md ├── ControllerRefManager.md ├── JobController.md ├── NodeLifecycleController │ ├── NoExecuteTaintManager.md │ └── NoExecuteTaintManger.jpg ├── PodControl.md └── README.md ├── kube-scheduler ├── Cache.md ├── Configurator.md ├── EventHandlers.md ├── Extender.md ├── Framework.md ├── KubeSchedulerConfiguration.md ├── Plug-In.png ├── Plugin.md ├── PodNominator.md ├── README.md ├── ScheduleAlgorithm.md ├── Scheduler.md ├── SchedulingQueue.md ├── SchedulingQueue.png ├── WaitingPods.md ├── kube-scheduler.png └── scheduling-framework-extensions.png └── kubernetes-sigs └── kubespary ├── README.md ├── cluster.jpg ├── cluster.md ├── container-engine ├── README.md ├── containerd │ └── README.md └── runc │ └── README.md ├── docs ├── downloads.md └── offline-environment.md ├── kubernetes └── preinstall │ ├── 0010-swapoff.md │ └── README.md ├── network_plugin ├── README.md └── cni │ └── README.md └── offline-install.md /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 目录 8 | 9 | 1. [kube-scheduler](./kube-scheduler/README.md) 10 | 2. [client-go](./client-go/README.md) 11 | 3. [controller](./controller/README.md) 12 | -------------------------------------------------------------------------------- /apimachinery/runtime/RecognizingDecoder.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 在[Serializer](./Serializer.md)文章中提到了,Serializer虽然抽象了序列化/反序列化的接口,但是反序列化不同格式的数据需要不同的Serializer对象,并不是很通用。有没有一种类型可以反序列化任意格式的数据呢?答案是有的,但是在介绍这个“万能解码器”之前,需要引入RecognizingDecoder接口。 10 | 11 | 本文引用源码为kubernetes的release-1.21分支。 12 | 13 | # RecognizingDecoder 14 | 15 | 如果笔者自己实现“万能解码器”,我抽象一个接口用于判断解码器是否能够解码当前格式的数据。让解码器实现这个接口,因为只有解码器自己才了解序列化数据的"特征"(早年笔者做视频存储产品时就是这么做的)。Kubernetes将这个判断过程称之为Recognize,笔者翻译为辨识。那么实现了辨识接口的解码器就是RecognizingDecoder,源码链接: 16 | 17 | ```go 18 | // RecognizingDecoder是具有辨识能力的解码器。 19 | type RecognizingDecoder interface { 20 | // 继承了解码器,很好理解,因为本身就是解码器。 21 | runtime.Decoder 22 | // 从序列化数据中提取一定量(也可以是全量)的数据(peek),根据这部分数据判断序列化数据是否属于该解码器。 23 | // 举个栗子,如果是json解码器,RecognizesData()就是判断peek是不是json格式数据。 24 | // 如果辨识成功返回ok为true,如果提供的数据不足以做出决定则返回unknown为true。 25 | // 首先需要注意的是,在1.20版本,该接口的定义为RecognizesData(peek io.Reader) (ok, unknown bool, err error)。 26 | // 因为io.Reader.Read()可能会返回错误,导致该接口也可能返回错误,到了1.21版本改成[]byte就不会返回错误了。 27 | // 所以该接口返回的可能性如下: 28 | // 1. ok=false, unknown=true: peek提供的数据不足以做出决定,什么情况会返回unknown后续章节会给出例子 29 | // 2. ok=true, unknown=false: 辨识成功,序列化数据属于该解码器,比如json解码器辨识json数据 30 | // 3. ok&unknown=false: 辨识失败,序列化数据不属于该解码器,比如json解码器辨识yaml或protobuf数据 31 | RecognizesData(peek []byte) (ok, unknown bool, err error) 32 | } 33 | ``` 34 | 35 | 在[Serializer](./Serializer.md#json)的文章中介绍了json.Serializer,它是json和yaml的编解码器(runtime.Serializer),自然它就是json和yaml的解码器。但是[Serializer](./Serializer.md#json)的文章中笔者仅对runtime.Serializer相关的功能做了解析,其实它也实现了RecognizingDecoder。接下来的章节将解析json、yaml、protobuf解码器是如何实现辨识数据接口(RecognizingDecoder.RecognizesData())。 36 | 37 | ## json&yaml 38 | 39 | 在[Serializer](./Serializer.md#json)的文章中已经介绍了json.Serializer是json和yaml的解码器,所以json.Serializer可以辨识json和yaml两种格式的数据,源码链接: 40 | 41 | ```go 42 | // RecognizesData()实现了RecognizingDecoder.RecognizesData()接口 43 | func (s *Serializer) RecognizesData(data []byte) (ok, unknown bool, err error) { 44 | // 如果是yaml选项(即yaml编解码器),直接返回unknown,这个操作可以啊,不打算争取一下了么? 45 | // 其实道理很简单,yaml实在是没有什么明显的特征,所以返回unknown,表示不知道是不是yaml格式。 46 | if s.options.Yaml { 47 | return false, true, nil 48 | } 49 | // 既然无法辨识yaml格式,那json格式总不至于也无法辨识吧,毕竟json有明显的特征"{}"。 50 | // utilyaml.IsJSONBuffer()的函数实现就是找数据中是否以'{'开头(前面的空格除外)。 51 | // 这个原理就很简单,因为API类型都是结构体(不存在数组和空指针),所以json数据都是'{...}'格式。 52 | return utilyaml.IsJSONBuffer(data), false, nil 53 | } 54 | ``` 55 | 56 | 本以为辨识数据格式有多么高深?其实就是很简单的事情,找到数据格式的明显特征就可以了。只要这个特征在所有的数据格式中是独一无二的,越简单越好。如果辨识数据非常复杂,甚至与解码一样的计算量,辨识就没有意义了,毕竟设计辨识就是为了避免无意义的解码。 57 | 58 | ## protobuf 59 | 60 | 因为json和yaml是开放的、标准的数据格式,它们只定义了数据的格式。而protobuf是一种协议专属的数据格式,所以一般会在数据头增加一些关键字,源码链接: 61 | 62 | ```go 63 | // RecognizesData()实现了RecognizingDecoder.RecognizesData()接口. 64 | func (s *Serializer) RecognizesData(data []byte) (bool, bool, error) { 65 | // 只要数据是以protobuf协议关键字开始就是protobuf序列化数据,protobuf关键字是[]byte{0x6b, 0x38, 0x73, 0x00}。 66 | return bytes.HasPrefix(data, s.prefix), false, nil 67 | } 68 | ``` 69 | 70 | # decoder 71 | 72 | 虽然json,yaml,protobuf的解码器都实现了RecognizingDecoder,但是他们依然是一个个分散的解码器,是不是应该有一个类型将这些解码器管理器来,然后可以做到自动辨识并解码的能力?它就是万能解码器decoder,源码链接: 73 | 74 | ```go 75 | // decoder实现了RecognizingDecoder。 76 | type decoder struct { 77 | // 管理了若干解码器,为什么是[]runtime.Decoder,而不是[]RecognizingDecoder。 78 | // json,yaml,protobuf的解码器不都实现了RecognizingDecoder么? 79 | // 笔者猜测这是为扩展考虑的,未来如果增加一种解码器,但是又没有实现辨识接口怎么办? 80 | // 那么问题来,没有实现辨识接口的解码器还有必要被decoder管理么? 81 | // 答案是有必要,在非常极端的情况下decoder会尝试让所有解码器都解码一次,只要有一个成功就行,后面会看到代码实现。 82 | decoders []runtime.Decoder 83 | } 84 | 85 | // decoder毕竟是包内的私有类型,如果需要使用它必须通过包内的公有函数构造。 86 | // NewDecoder()是decoder的构造函数,传入各种数据格式的解码器。 87 | func NewDecoder(decoders ...runtime.Decoder) runtime.Decoder { 88 | return &decoder{ 89 | decoders: decoders, 90 | } 91 | } 92 | ``` 93 | 94 | ## RecognizesData 95 | 96 | 其实decoder没有必要实现RecognizesData()接口,因为NewDecoder()返回的是runtime.Decoder,只要实现Decode()接口即可。既然是在recognizer包内定义的类型,既来之则安之吧,源码链接: 97 | 98 | ```go 99 | // RecognizesData()实现了RecognizingDecoder.RecognizesData()接口。 100 | // 因为decoder结构体中包含了[]runtime.Decoder成员变量(decoders),为了区分decoder和decoder.decoders[i], 101 | // 后续的注释中'decoder'代表的就是decoder结构体,而'解码器'代表的是decoder.decoders[i]。 102 | func (d *decoder) RecognizesData(data []byte) (bool, bool, error) { 103 | var ( 104 | lastErr error 105 | anyUnknown bool 106 | ) 107 | // 遍历所有的解码器 108 | for _, r := range d.decoders { 109 | // 将解码器划分为[]RecognizingDecoder和[]runtime.Decoder两个子集,其中只有[]RecognizingDecoder具备辨识能力。 110 | switch t := r.(type) { 111 | // 只用decoder.decoders中的[]RecognizingDecoder子集辨识数据格式 112 | case RecognizingDecoder: 113 | // 让每个解码器都辨识一下序列化数据 114 | ok, unknown, err := t.RecognizesData(data) 115 | if err != nil { 116 | // 如果解码器辨识出错,则只需要记录最后一个错误就可以了,因为所有解码器报错都应该是一样的。 117 | // 前面已经提到了,因为历史原因,io.Reader读取数据可能返回错误,当前版本已经不会返回错误了。 118 | lastErr = err 119 | continue 120 | } 121 | // 只要有任何解码器返回数据不足以做出决定,那么就要返回unknown,除非有任何解码器辨识成功。 122 | // 这个逻辑应该比较简单:当有一些解码器返回unknown,其他都返回无法辨识的时候,对于decoder而言就是返回unknown。 123 | // 因为有些解码器只是不知道数据格式是不是属于自己,并不等于辨识失败,是存在可能的。 124 | anyUnknown = anyUnknown || unknown 125 | if !ok { 126 | continue 127 | } 128 | // 只要有任何一个解码器返回辨识成功,那么就可以直接返辨识成功了。 129 | return true, false, nil 130 | } 131 | } 132 | // 所有的解码器都没有辨识成功,那么如果有任何解码器返回unknown就返回unknown。 133 | return false, anyUnknown, lastErr 134 | } 135 | ``` 136 | 137 | ## Decode 138 | 139 | Decode()才是decoder的精髓所在,因为decoder是万能解码器,不"挑选"数据格式,源码链接: 140 | 141 | ```go 142 | // Decode()实现了runtime.Decoder.Decode()接口。 143 | func (d *decoder) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { 144 | var ( 145 | lastErr error 146 | skipped []runtime.Decoder 147 | ) 148 | 149 | // 遍历所有的解码器 150 | for _, r := range d.decoders { 151 | switch t := r.(type) { 152 | // 找到[]RecognizingDecoder子集,与RecognizesData()类似。 153 | case RecognizingDecoder: 154 | // 这个过程在decoder.RecognizesData()已经注释过了,功能是一样的。 155 | ok, unknown, err := t.RecognizesData(data) 156 | if err != nil { 157 | lastErr = err 158 | continue 159 | } 160 | // 把返回unknown的解码器都汇总到skipped中,表示这些先略过,因为他们不知道数据格式是否属于自己。 161 | if unknown { 162 | skipped = append(skipped, t) 163 | continue 164 | } 165 | // 解码器明确返回数据不属于解码器,辨识失败,忽略该解码器。需要注意是“忽略”,不是“略过”。 166 | // “略过”的解码器未来可能还会有用,而忽略的解码器是不会再用的。 167 | if !ok { 168 | continue 169 | } 170 | // 如果解码器辨识成功,则用该解码器解码 171 | return r.Decode(data, gvk, into) 172 | // []runtime.Decoder子集也放入skipped中,他们不具备辨识数据的能力,与unknown本质是一样的。 173 | default: 174 | skipped = append(skipped, t) 175 | } 176 | } 177 | 178 | // 如果没有任何解码器辨识数据成功,那就用[]runtime.Decoder子集和返回unknown的[]RecognizingDecoder子集逐一解码。 179 | // 这是一个非常简单暴力的做法,但是也没什么好办法。但是不用过分担心,绝大部分情况是可辨识成功的。 180 | // 也就是说,只有非常极端的情况才会执行这里的代码。 181 | for _, r := range skipped { 182 | out, actual, err := r.Decode(data, gvk, into) 183 | if err != nil { 184 | lastErr = err 185 | continue 186 | } 187 | return out, actual, nil 188 | } 189 | 190 | if lastErr == nil { 191 | lastErr = fmt.Errorf("no serialization format matched the provided data") 192 | } 193 | return nil, nil, lastErr 194 | } 195 | ``` 196 | 197 | 既然decoder的两个接口都需要明确区分`[]RecognizingDecoder`和`[]runtime.Decoder`两个子集,笔者打算提交一个PR,将decoder管理的解码器划分为两个子集,在NewDecoder()构造decoder时将所有解码器划分到两个自己,这样就不用每次在解码的时候再做类型断言了。 198 | 199 | # 总结 200 | 201 | 1. RecognizingDecoder是具有数据辨识能力的解码器,它继承了runtime.Decoder并增加了RecognizesData()接口。 202 | 2. RecognizingDecoder只是比runtime.Decoder增加辨识数据的接口,但是不代表RecognizingDecoder.Decode()会自动辨识数据并解码。举个例子:json.Serializer实现了RecognizingDecoder,但是只能解码json或yaml的数据。 203 | 3. recognizer.decoder能够自动辨识数据格式并用相应的解码器解码。 204 | 4. recognizer.decoder辨识的方法是:如果有解码器辨识成功就用该解码器解码,否则就用辨识失败以外的解码器逐一尝试解码。 205 | 5. 虽然感觉辨识是一个尝试行为,其实是一个非常靠谱的动作,因为Kubernetes只有3中数据格式,json,yaml和protobuf,json和protobuf可以明确的返回是否辨识成功,不存在unkonwn的可能。虽然yaml无法辨识,但是此时已经明确排除了json和protobuf,也就只剩下yaml一种可能了。 206 | 6. recognizer.decoder到底有什么用?[CodecFactory](CodecFactory.md).UniversalDeserializer()返回的解码器就是recognizer.decoder,而这个接口官方注释是:"能够解码所有已知格式的API对象"。与笔者叫的“万能解码器”是一个意思。 207 | -------------------------------------------------------------------------------- /apimachinery/runtime/Serializer.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 序列化和反序列化在很多项目中都有应用,Kubernetes也不例外。Kubernetes中定义了大量的API对象,为此还单独设计了一个[包](https://github.com/kubernetes/api),方便多个模块引用。API对象在不同的模块之间传输(尤其是跨进程)可能会用到序列化与反序列化,不同的场景对于序列化个格式又不同,比如grpc协议用protobuf,用户交互用yaml(因为yaml可读性强),etcd存储用json。Kubernetes反序列化API对象不同于我们常用的json.Unmarshal()函数(需要传入对象指针),Kubernetes需要解析对象的类型(Group/Version/Kind),根据API对象的类型构造API对象,然后再反序列化。因此,Kubernetes定义了Serializer接口,专门用于API对象的序列化和反序列化。 10 | 11 | 本文引用源码为kubernetes的release-1.21分支。 12 | 13 | # Serializer 14 | 15 | 因为Kubernetes需要支持json、yaml、protobuf三种数据格式的序列化和反序列化,有必要抽象序列化和反序列化的统一接口,源码链接: 16 | 17 | ```go 18 | // Serializer是用于序列化和反序列化API对象的核心接口, 19 | type Serializer interface { 20 | // Serializer继承了编码器和解码器,编码器就是用来序列化API对象的,序列化的过程称之为编码;反之,反序列化的过程称之为解码。 21 | // 关于编/解码器的定义下面有注释。 22 | Encoder 23 | Decoder 24 | } 25 | 26 | // 序列化的过程称之为编码,实现编码的对象称之为编码器(Encoder) 27 | type Encoder interface { 28 | // Encode()将对象写入流。可以将Encode()看做为json(yaml).Marshal(),只是输出变为io.Writer。 29 | Encode(obj Object, w io.Writer) error 30 | 31 | // Identifier()返回编码器的标识符,当且仅当两个不同的编码器编码同一个对象的输出是相同的,那么这两个编码器的标识符也应该是相同的。 32 | // 也就是说,编码器都有一个标识符,两个编码器的标识符可能是相同的,判断标准是编码任意API对象时输出都是相同的。 33 | // 标识符有什么用?标识符目标是与CacheableObject.CacheEncode()方法一起使用,CacheableObject又是什么东东?后面有介绍。 34 | Identifier() Identifier 35 | } 36 | 37 | // 标识符就是字符串,可以简单的理解为标签的字符串形式,后面会看到如何生成标识符。 38 | type Identifier string 39 | 40 | // 反序列化的过程称之为解码,实现解码的对象称之为解码器(Decoder) 41 | type Decoder interface { 42 | // Decode()尝试使用Schema中注册的类型或者提供的默认的GVK反序列化API对象。 43 | // 如果'into'非空将被用作目标类型,接口实现可能会选择使用它而不是重新构造一个对象。 44 | // 但是不能保证输出到'into'指向的对象,因为返回的对象不保证匹配'into'。 45 | // 如果提供了默认GVK,将应用默认GVK反序列化,如果未提供默认GVK或仅提供部分,则使用'into'的类型补全。 46 | Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error) 47 | } 48 | ``` 49 | 50 | 我们平时工作中最常用的序列化格式包括json、yaml以及protobuf,非常"巧合",Kubernetes也是用这几种序列化格式。以json为例,编码器和解码器可以等同于json.Marshal()和json.Unmarshal(),定义成interface是对序列化与反序列化的统一抽象。为什么我们平时很少抽象(当然有些读者是有抽象的,我们不能一概而论),是因为我们工作中可能只用到一种序列化格式,所以抽象显得没那么必要。而Kubernetes中,这三种都是需要的,yaml的可视化效果好,比如我们写的各种yaml文件;而API对象存储在etcd中是json格式,在用到grpc的地方则需要protobuf格式。 51 | 52 | 再者,Kubernetes对于Serializer的定义有更高的要求,即根据序列化的数据中的元数据自动识别API对象的类型(GVK),这在Decoder.Decode()接口定义中已经有所了解。而我们平时使用json.Marshal()的时候传入了指定类型的对象指针,相比于Kubernetes对于反序列化的要求,我们使用的相对更"静态"。 53 | 54 | 综上所述,抽象Serializer就有必要了,尤其是[RecognizingDecoder](./RecognizingDecoder.md)可以解码任意格式的API对象就可以充分体现这种抽象的价值。 55 | 56 | ## json 57 | 58 | json.Serializer实现了将API对象序列化成json数据和从json数据反序列化API对象,源码链接: 59 | 60 | ```go 61 | // Serializer实现了runtime.Serializer接口。 62 | type Serializer struct { 63 | // MetaFactory从json数据中提取GVK(Group/Version/Kind),下面有MetaFactory注释。 64 | // MetaFactory很有用,解码时如果不提供默认的GVK和API对象指针,就要靠MetaFactory提取GVK了。 65 | // 当然,即便提供了供默认的GVK和API对象指针,提取的GVK的也是非常有用的,详情参看Decode()接口的实现。 66 | meta MetaFactory 67 | // SerializerOptions是Serializer选项,可以看做是配置,下面有注释。 68 | options SerializerOptions 69 | // runtime.ObjectCreater根据GVK构造API对象,在反序列化时会用到,其实它就是Schema。 70 | // runtime.ObjectCreater的定义读者可以自己查看源码,如果对Schema熟悉的读者这都不是事。 71 | creater runtime.ObjectCreater 72 | // runtime.ObjectTyper根据API对象返回可能的GVK,也是用在反序列化中,其实它也是Schema。 73 | // 这个有什么用?runtime.Serializer.Decode()接口注释说的很清楚,在json数据和默认GVK无法提供的类型元数据需要用输出类型补全。 74 | typer runtime.ObjectTyper 75 | // 标识符,Serializer一旦被创建,标识符就不会变了。 76 | identifier runtime.Identifier 77 | } 78 | 79 | // SerializerOptions定义了Serializer的选项. 80 | type SerializerOptions struct { 81 | // true: 序列化/反序列化yaml;false: 序列化/反序列化json 82 | // 也就是说,json.Serializer既可以序列化/反序列json,也可以序列化/反序列yaml。 83 | Yaml bool 84 | 85 | // Pretty选项仅用于Encode接口,输出易于阅读的json数据。当Yaml选项为true时,Pretty选项被忽略,因为yaml本身就易于阅读。 86 | // 什么是易于阅读的?举个例子就立刻明白了,定义测试类型为: 87 | // type Test struct { 88 | // A int 89 | // B string 90 | // } 91 | // 则关闭和开启Pretty选项的对比如下: 92 | // {"A":1,"B":"2"} 93 | // { 94 | // "A": 1, 95 | // "B": "2" 96 | // } 97 | // 很明显,后者更易于阅读。易于阅读只有人看的时候才有需要,对于机器来说一点价值都没有,所以这个选项使用范围还是比较有限的。 98 | Pretty bool 99 | 100 | // Strict应用于Decode接口,表示严谨的。那什么是严谨的?笔者很难用语言表达,但是以下几种情况是不严谨的: 101 | // 1. 存在重复字段,比如{"value":1,"value":1}; 102 | // 2. 不存在的字段,比如{"unknown": 1},而目标API对象中不存在Unknown属性; 103 | // 3. 未打标签字段,比如{"Other":"test"},虽然目标API对象中有Other字段,但是没有打`json:"Other"`标签 104 | // Strict选项可以理解为增加了很多校验,请注意,启用此选项的性能下降非常严重,因此不应在性能敏感的场景中使用。 105 | // 那什么场景需要用到Strict选项?比如Kubernetes各个服务的配置API,对性能要求不高,但需要严格的校验。 106 | Strict bool 107 | } 108 | ``` 109 | 110 | ### MetaFactory 111 | 112 | MetaFactory类型名定义其实挺忽悠人的,工厂类都是用来构造对象的,MetaFactory的功能虽然也是构造类型元数据的,但是它更像是一个解析器,所以笔者认为"MetaParser"更加贴切,源码链接: 113 | 114 | ```go 115 | type MetaFactory interface { 116 | // 解析json数据中的元数据字段,返回GVK(Group/Version/Kind)。 117 | // 如果MetaFactory就这么一个接口函数,笔者认为叫解释器或者解析器更加合理。 118 | Interpret(data []byte) (*schema.GroupVersionKind, error) 119 | } 120 | 121 | // SimpleMetaFactory是MetaFactory的一种实现,用于检索在json中由"apiVersion"和"kind"字段标识的对象的类型和版本。 122 | type SimpleMetaFactory struct { 123 | } 124 | 125 | // Interpret()实现了MetaFactory.Interpret()接口。 126 | func (SimpleMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) { 127 | // 定义一种只有apiVersion和kind两个字段的匿名类型 128 | findKind := struct { 129 | // +optional 130 | APIVersion string `json:"apiVersion,omitempty"` 131 | // +optional 132 | Kind string `json:"kind,omitempty"` 133 | }{} 134 | // 只解析json中apiVersion和kind字段,这个玩法有点意思,但是笔者认为这个方法有点简单粗暴。 135 | // 读者可以尝试阅读json.Unmarshal(),该函数会遍历整个json,开销不小,其实必要性不强,因为只需要apiVersion和kind字段。 136 | // 试想一下,如果每次反序列化一个API对象都要有一次Interpret()和Decode(),它的开销相当于做了两次反序列化。 137 | if err := json.Unmarshal(data, &findKind); err != nil { 138 | return nil, fmt.Errorf("couldn't get version/kind; json parse error: %v", err) 139 | } 140 | // 将apiVersion解析为Group和Version 141 | gv, err := schema.ParseGroupVersion(findKind.APIVersion) 142 | if err != nil { 143 | return nil, err 144 | } 145 | // 返回API对象的GVK 146 | return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: findKind.Kind}, nil 147 | } 148 | ``` 149 | 150 | ### Decode 151 | 152 | 其实Serializer的重头戏在解码,因为解码需要考虑的事情比较多,比如提取类型元数据(GVK),根据类型元数据构造API对象,当然还要考虑复用传入的API对象。而编码就没有这么复杂,所以理解了解码的实现,编码就基本可以忽略不计了。源码链接: 153 | 154 | ```go 155 | // Decode实现了Decoder.Decode(),尝试从数据中提取的API类型(GVK),应用提供的默认GVK,然后将数据加载到所需类型或提供的'into'匹配的对象中: 156 | // 1. 如果into为*runtime.Unknown,则将提取原始数据,并且不执行解码; 157 | // 2. 如果into的类型没有在Schema注册,则使用json.Unmarshal()直接反序列化到'into'指向的对象中; 158 | // 3. 如果'into'不为空且原始数据GVK不全,则'into'的类型(GVK)将用于补全GVK; 159 | // 4. 如果'into'为空或数据中的GVK与'into'的GVK不同,它将使用ObjectCreater.New(gvk)生成一个新对象; 160 | // 成功或大部分错误都会返回GVK,GVK的计算优先级为originalData > default gvk > into. 161 | func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { 162 | data := originalData 163 | // 如果配置选项为yaml,则将yaml格式转为json格式,是不是有一种感觉:“卧了个槽”!所谓可支持json和yaml,就是先将yaml转为json! 164 | // 那我感觉我可以支持所有格式,都转为json就完了呗!其实不然,yaml的使用场景都是需要和人交互的地方,所以对于效率要求不高(qps低)。 165 | // 那么实现简单易于维护更重要,所以这种实现并没有什么毛病。 166 | if s.options.Yaml { 167 | // yaml转json 168 | altered, err := yaml.YAMLToJSON(data) 169 | 170 | if err != nil { 171 | return nil, nil, err 172 | } 173 | data = altered 174 | } 175 | // 此时data是json了,以后就不用再考虑yaml选项了 176 | 177 | // 解析类型元数据,原理已经在MetaFactory解释过了。 178 | actual, err := s.meta.Interpret(data) 179 | if err != nil { 180 | return nil, nil, err 181 | } 182 | 183 | // 解析类型元数据大部分情况是正确的,除非不是json或者apiVersion格式不对。 184 | // 但是GVK三元组可能有所缺失,比如只有Kind,Group/Version,其他字段就用默认的GVK补全。 185 | // 这也体现出了原始数据中的GVK的优先级最高,其次是默认的GVK。gvkWithDefaults()函数下面有注释。 186 | if gvk != nil { 187 | *actual = gvkWithDefaults(*actual, *gvk) 188 | } 189 | 190 | // 如果'into'是*runtime.Unknown类型,需要返回*runtime.Unknown类型的对象。 191 | // 需要注意的是,此处复用了'into'指向的对象,因为返回的类型与'into'指向的类型完全匹配。 192 | if unk, ok := into.(*runtime.Unknown); ok && unk != nil { 193 | // 输出原始数据 194 | unk.Raw = originalData 195 | // 输出数据格式为JSON,那么问题来了,不是'data'才能保证是json么?如s.options.Yaml == true,originalData是yaml格式才对。 196 | // 所以笔者有必要提一个issue,看看官方怎么解决,如此明显的一个问题为什么没有暴露出来? 197 | // 笔者猜测:runtime.Unknown只在内部使用,而内部只用json格式,所以自然不会暴露出来。 198 | unk.ContentType = runtime.ContentTypeJSON 199 | // 输出GVK。 200 | unk.GetObjectKind().SetGroupVersionKind(*actual) 201 | return unk, actual, nil 202 | } 203 | 204 | // 'into'不为空,通过into类型提取GVK,这样在原始数据中的GVK和默认GVK都没有的字段用into的GVK补全。 205 | if into != nil { 206 | // 判断'into'是否为runtime.Unstructured类型 207 | _, isUnstructured := into.(runtime.Unstructured) 208 | // 获取'into'的GVK,需要注意的是返回的是一组GVK,types[0]是推荐的GVK。 209 | types, _, err := s.typer.ObjectKinds(into) 210 | switch { 211 | // 'into'的类型如果没有被注册或者为runtime.Unstructured类型,则直接反序列成'into'指向的对象。 212 | // 没有被注册的类型自然无法构造对象,而非结构体等同于map[string]interface{},不可能是API对象(因为API对象必须是结构体)。 213 | // 所以这两种情况直接反序列化到'into'对象就可以了,此时与json.Unmarshal()没什么区别。 214 | case runtime.IsNotRegisteredError(err), isUnstructured: 215 | if err := caseSensitiveJSONIterator.Unmarshal(data, into); err != nil { 216 | return nil, actual, err 217 | } 218 | return into, actual, nil 219 | // 获取'into'类型出错,多半是因为不是指针 220 | case err != nil: 221 | return nil, actual, err 222 | // 用'into'的GVK补全未设置的GVK,所以GVK的优先级:originalData > default gvk > into 223 | default: 224 | *actual = gvkWithDefaults(*actual, types[0]) 225 | } 226 | } 227 | 228 | // 如果没有Kind 229 | if len(actual.Kind) == 0 { 230 | return nil, actual, runtime.NewMissingKindErr(string(originalData)) 231 | } 232 | // 如果没有Version 233 | if len(actual.Version) == 0 { 234 | return nil, actual, runtime.NewMissingVersionErr(string(originalData)) 235 | } 236 | // 那么问题来了,为什么不判断Group?Group为""表示"core",比如我们写ymal的时候Kind为Pod,apiVersion是v1,并没有设置Group。 237 | 238 | // 从函数名字可以看出复用'into'或者重新构造对象,复用的原则是:如果'into'注册的一组GVK有任何一个与*actual相同,则复用'into'。 239 | // runtime.UseOrCreateObject()源码读者感兴趣可以自己看下 240 | obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into) 241 | if err != nil { 242 | return nil, actual, err 243 | } 244 | 245 | // 反序列化对象,caseSensitiveJSONIterator暂且不用关心,此处可以理解为json.Unmarshal()。 246 | // 当然,读者非要知道个究竟,可以看看代码,笔者此处不注释。 247 | if err := caseSensitiveJSONIterator.Unmarshal(data, obj); err != nil { 248 | return nil, actual, err 249 | } 250 | 251 | // 如果是非strict模式,可以直接返回了。其实到此为止就可以了,后面是针对strict模式的代码,是否了解并不重要。 252 | if !s.options.Strict { 253 | return obj, actual, nil 254 | } 255 | 256 | // 笔者第一眼看到下面的以为看错了,但是擦了擦懵逼的双眼,发现就是YAMLToJSON。如果原始数据是json不会有问题么? 257 | // 笔者查看了一下yaml.YAMLToJSONStrict()函数注释:由于JSON是YAML的子集,因此通过此方法传递JSON应该是没有任何操作的。 258 | // 除非存在重复的字段,会解析出错。所以此处就是用来检测是否有重复字段的,当然,如果是yaml格式顺便转成了json。 259 | // 感兴趣的读者可以阅读源码,笔者只要知道它的功能就行了,就不“深究”了。 260 | altered, err := yaml.YAMLToJSONStrict(originalData) 261 | if err != nil { 262 | return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData)) 263 | } 264 | // 接下来会因为未知的字段报错,比如对象未定义的字段,未打标签的字段等。 265 | // 此处使用DeepCopyObject()等同于新构造了一个对象,而这个对象其实又没什么用,仅作为一个临时的变量使用。 266 | strictObj := obj.DeepCopyObject() 267 | if err := strictCaseSensitiveJSONIterator.Unmarshal(altered, strictObj); err != nil { 268 | return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData)) 269 | } 270 | // 返回反序列化的对象、GVK,所谓的strict模式无非是再做了一次转换和反序列化来校验数据的正确性,结果直接丢弃。 271 | // 所以说strict没有必要不用开启,除非你真正理解他的作用并且能够承受带来的后果。 272 | return obj, actual, nil 273 | } 274 | 275 | // gvkWithDefaults()利用defaultGVK补全actual中未设置的字段。 276 | // 需要注意的是,参数'defaultGVK'只是一次调用相对于actual的默认GVK,不是Serializer.Decode()的默认GVK。 277 | func gvkWithDefaults(actual, defaultGVK schema.GroupVersionKind) schema.GroupVersionKind { 278 | // actual如果没有设置Kind则用默认的Kind补全 279 | if len(actual.Kind) == 0 { 280 | actual.Kind = defaultGVK.Kind 281 | } 282 | // 如果Group和Version都没有设置,则用默认的Group和Version补全。 283 | // 为什么必须是都没有设置?缺少Version或者Group有什么问题么?下面的代码给出了答案。 284 | if len(actual.Version) == 0 && len(actual.Group) == 0 { 285 | actual.Group = defaultGVK.Group 286 | actual.Version = defaultGVK.Version 287 | } 288 | // 如果Version未设置,则用默认的Version补全,但是前提是Group与默认Group相同。 289 | // 因为Group不同的API即便Kind/Version相同可能是两个完全不同的类型,比如自定义资源(CRD)。 290 | if len(actual.Version) == 0 && actual.Group == defaultGVK.Group { 291 | actual.Version = defaultGVK.Version 292 | } 293 | // 如果Group未设置而Version与默认的Version相同,为什么不用默认的Group补全? 294 | // 前面已经解释过了,应该不用再重复了。 295 | return actual 296 | } 297 | ``` 298 | 299 | ### Encode 300 | 301 | 相比于解码,编码就简单很多,直接按照选项(yaml、pretty)编码就行了,源码链接: 302 | 303 | ```go 304 | // Encode()实现了Encoder.Encode()接口。 305 | func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error { 306 | // CacheableObject允许对象缓存其不同的序列化数据,以避免多次执行相同的序列化,这是一种出于效率考虑设计的类型。 307 | // 因为同一个对象可能会多次序列化json、yaml和protobuf,此时就需要根据编码器的标识符找到对应的序列化数据。 308 | if co, ok := obj.(runtime.CacheableObject); ok { 309 | // CacheableObject笔者不再注释了,感兴趣的读者可以自行阅读源码。 310 | // 其实根据传入的参数也能猜出来具体实现:利用标识符查一次map,如果有就输出,没有就调用一次s.doEncode()。 311 | return co.CacheEncode(s.Identifier(), s.doEncode, w) 312 | } 313 | // 非CacheableObject对象,就执行一次json的序列化。 314 | return s.doEncode(obj, w) 315 | } 316 | 317 | // doEncode()类似于于json.Marshal(),只是写入是io.Writer而不是[]byte。 318 | func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { 319 | // 序列化成yaml? 320 | if s.options.Yaml { 321 | // 序列化对象为json 322 | json, err := caseSensitiveJSONIterator.Marshal(obj) 323 | if err != nil { 324 | return err 325 | } 326 | // json->yaml 327 | data, err := yaml.JSONToYAML(json) 328 | if err != nil { 329 | return err 330 | } 331 | // 写入io.Writer。 332 | _, err = w.Write(data) 333 | return err 334 | } 335 | 336 | // 输出易于理解的格式?那么问题来了,为什么输出yaml不需要这个判断?很简单,yaml就是易于理解的。 337 | if s.options.Pretty { 338 | // 序列化对象为json 339 | data, err := caseSensitiveJSONIterator.MarshalIndent(obj, "", " ") 340 | if err != nil { 341 | return err 342 | } 343 | // 写入io.Writer。 344 | _, err = w.Write(data) 345 | return err 346 | } 347 | 348 | // 非pretty模式,用我们最常用的json序列化方法,无非我们最常用方法是json.Marshal()。 349 | // 如果需要写入io.Writer,下面的代码是标准写法。 350 | encoder := json.NewEncoder(w) 351 | return encoder.Encode(obj) 352 | } 353 | ``` 354 | 355 | ### identifier 356 | 357 | 前面笔者提到了,编码器标识符可以看做是标签的字符串形式,大家应该很熟悉Kubernetes中的标签选择器,符合标签选择器匹配规则的所有API对象都可以看做"同质"。同样的道理,编码器也有自己的标签,标签相同的所有编码器是同质的,即编码同一个API对象的结果都是一样的。编码器标识符的定义没有那么复杂,就是简单的字符串,匹配也非常简单,标识符相等即为匹配,所以标识符可以理解为标签的字符串形式。源码链接: 358 | 359 | ```go 360 | // identifier()根据给定的选项计算编码器的标识符。 361 | func identifier(options SerializerOptions) runtime.Identifier { 362 | // 编码器的唯一标识符是一个map[string]string,不同属性的组合形成更了唯一性。 363 | result := map[string]string{ 364 | // 名字是json,表明是json编码器。 365 | "name": "json", 366 | // 输出格式为yaml或json 367 | "yaml": strconv.FormatBool(options.Yaml), 368 | // 是否为pretty模式 369 | "pretty": strconv.FormatBool(options.Pretty), 370 | } 371 | // 序列化成json,生成最终的标识符,json序列化是标签的一种字符串形式。 372 | identifier, err := json.Marshal(result) 373 | if err != nil { 374 | klog.Fatalf("Failed marshaling identifier for json Serializer: %v", err) 375 | } 376 | // 也就是说,只要yaml和pretty选项相同的任意两个json.Serializer,任何时候编码同一个API对象输出一定是相同的。 377 | // 所以当API对象被多个编码器多次编码时,以编码器标识符为键利用缓冲避免重复编码。 378 | return runtime.Identifier(identifier) 379 | } 380 | ``` 381 | 382 | ## yaml 383 | 384 | 其实json.Serializer支持json和ymal,那么还有必要定义yaml.Serializer么?毕竟构造两个json.Serializer对象,一个开启yaml选型,一个关闭yaml选项就可以了。来看看yaml.Serializer的定义,源码链接: 385 | 386 | ```go 387 | // yamlSerializer实现了runtime.Serializer()。 388 | // 没有定义Serializer类型,而是一个包内的私有类型yamlSerializer,如果需要使用这个类型必须通过包内的公有接口创建。 389 | type yamlSerializer struct { 390 | // 直接继承了runtime.Serializer,不用想肯定是json.Serializer。 391 | runtime.Serializer 392 | } 393 | 394 | // NewDecodingSerializer()向支持json的Serializer添加yaml解码支持。 395 | // 也就是说yamlSerializer编码json,解码yaml,当然从接口名字看,调用这个接口估计只需要用解码能力吧。 396 | // 好在笔者检索了一下源码,yamlSerializer以及NewDecodingSerializer()没有引用的地方,应该是历史遗留的代码。 397 | func NewDecodingSerializer(jsonSerializer runtime.Serializer) runtime.Serializer { 398 | return &yamlSerializer{jsonSerializer} 399 | } 400 | 401 | // Decode()实现了Decoder.Decode()接口。 402 | func (c yamlSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { 403 | // yaml->json 404 | out, err := yaml.ToJSON(data) 405 | if err != nil { 406 | return nil, nil, err 407 | } 408 | // 反序列化json 409 | data = out 410 | return c.Serializer.Decode(data, gvk, into) 411 | } 412 | ``` 413 | 414 | # 总结 415 | 416 | 1. json.Serializer可以实现json和yaml两种数据格式的序列化/反序列化,而yaml.Serializer基本不用了; 417 | 2. MetaFactory的功能就是提取apiVersion和kind字段,然后返回GVK; 418 | 3. json.Serializer也可以像json/yaml.Unmarshal()一样使用,只要传入的'into'的类型没有在Schema中注册就可以了; 419 | 4. json.Serializer在反序列化之前需要计算API对象的GVK,计算原则是优先使用json中的GVK,如果GVK有残缺,则采用默认GVK补全,最后用'into'指向的对象类型补全; 420 | 5. 其实runtime.Serializer只是比json/yaml.Unmarshal()多了类型提取并构造对象的过程,但是依然存在无法通用的问题,即解码json和yaml需要不同的对象,这就要[RecognizingDecoder](./RecognizingDecoder.md)来解决了; 421 | -------------------------------------------------------------------------------- /apimachinery/runtime/StreamSerializer.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 在[RecognizingDecoder](./RecognizingDecoder.md)文章中,笔者解析了具有辨识数据能力的解码器,并介绍了"万能解码器",可以解码Kubernetes所有序列化格式(json、yaml、protobuf)的API对象。无论是runtime.Decoder还是[RecognizingDecoder](./RecognizingDecoder.md),都是用于从内存([]byte)解码器单个API对象的,但是在Kubernetes的应用中,还有一种解码需求是从流中解码对象,比如比较常用的Watch功能,就是从流中持续读取数据并解码成一个一个的API对象。 10 | 11 | 从数据流中读取数据并解码API对象的解码器本文命名为**流解码器**,我们日常说的流是字节流,那么流解码器可以看做是API对象流,每调用一次返回一个API对象。如果想把字节流变成API对象流,首先需要将字节流变成json/yaml/protobuf序列化格式对象流,即先从字节流中读取一个一个的json/yaml/protobuf对象。然后再用解码器将json/yaml/protobuf对象解码成API对象就完成了。 12 | 13 | 本文引用源码为kubernetes的release-1.21分支。 14 | 15 | # StreamSerializer 16 | 17 | ## StreamSerializerInfo 18 | 19 | 既然Kubernetes有基于流读/写API对象的需求,StreamSerializerInfo为实现这个需求提供了支撑,源码链接: 20 | 21 | ```go 22 | // StreamSerializerInfo含有指定序列化格式流的序列化器(Serializer)信息,这句话是不是拗口,可以总结为以下2点: 23 | // 1. 指定的序列化格式,json/yaml/protubuf之一; 24 | // 2. 只包含流序列化器的信息,但不是流序列化器,说的直白点就是基于StreamSerializerInfo可以构造流序列化器 25 | type StreamSerializerInfo struct { 26 | // 标识此序列化器是否可以编码为UTF-8,举个例子:json.Serializer就可以编码UTF8,而protobuf.Serializer就不可以。 27 | EncodesAsText bool 28 | // 序列化器对象,可能是json.Serializer或protobuf.Serializer。 29 | // 关于Serializer参看 https://github.com/jindezgm/k8s-src-analysis/blob/master/apimachinery/runtime/Serializer.md 30 | Serializer 31 | // 在介绍Framer之前,笔者先介绍一下帧的概念。帧这个概念在很多地方都有应用,比如视频、网络通信。 32 | // 视频是由连续的图片组成,每一个图片就是一帧;而网络通信中的帧是数据链路层的协议数据单元。 33 | // 我们发现帧的概念一般应用在连续数据,可以将连续的数据分成以帧为单位的单元,此处的帧也是相同的原理。 34 | // 本文中流是API对象序列化数据的字节流,而帧就是该字节流的数据元,以json为例一帧就是一个json对象。 35 | // 按照这个思路在来看Framer就非常容易理解了,Framer是工厂类,该工厂可以构建读/写帧的对象。 36 | // 关于Framer参看下面的源码注释。 37 | Framer 38 | } 39 | 40 | // Framer是工厂类,用于构建帧的Reader和Writer 41 | type Framer interface { 42 | // 构建帧Reader,该Reader每次读取一个API的序列化格式对象,比如读取一个json对象。 43 | NewFrameReader(r io.ReadCloser) io.ReadCloser 44 | // 构建帧Writer,该Writer每次写入一个API的序列化格式对象,比如写入一个json对象。 45 | NewFrameWriter(w io.Writer) io.Writer 46 | } 47 | ``` 48 | 49 | 既然Framer是一个interface,自然就要有实现这个接口的类型。因为从字节流中提取不同序列化格式的帧的方法不同,所以Kubernetes为每种序列化格式都实现了Framer接口,本文以json为例做代码解析,yaml和protobuf建议读者自己查看源码。 50 | 51 | ### json 52 | 53 | 因为json有明显的特征"{}",所以找到成对的"{}"即为一个json对象,我们来看看Kubernetes的实现方法是不是跟笔者的想法一样。源码链接: 54 | 55 | ```go 56 | // jsonFramer实现了runtime.Framer。 57 | type jsonFramer struct{} 58 | 59 | // NewFrameWriter()实现了runtime.Framer.NewFrameWriter(). 60 | func (jsonFramer) NewFrameWriter(w io.Writer) io.Writer { 61 | // 其实写并没有什么特殊的实现,只要一个json接一个json写就行了,所以原生的io.Writer就能满足要求。 62 | return w 63 | } 64 | 65 | // NewFrameReader()实现了runtime.Framer.NewFrameReader(). 66 | func (jsonFramer) NewFrameReader(r io.ReadCloser) io.ReadCloser { 67 | // 通过另一个工具报实现,下面有注释 68 | return framer.NewJSONFramedReader(r) 69 | } 70 | ``` 71 | 72 | 正是因为写json不需要特殊实现,而读json需要根据json的数据格式特点读取数据,所以将json帧Reader单独封装在工具包中,源码链接: 73 | 74 | ```go 75 | // jsonFrameReader实现了io.ReadCloser,从io.ReadCloser中以json对象为粒度读取数据。 76 | // 这有点类似于jave的各种InputStream的套接。 77 | type jsonFrameReader struct { 78 | // jsonFrameReader从r一个接一个读取json对象 79 | r io.ReadCloser 80 | // 用于读取json对象,需要注意的是此处json包就是我们常用的json.Marshal()的那个包。 81 | // 因为json.Decoder.Decode()具备从io.Reader读取一个json对象的能力,所以jsonFrameReader复用了这部分能力。 82 | // 但是json.Decoder.Decode()读取一个json对象后会执行反序列化操作,而jsonFrameReader根本不需要反序列化操作。 83 | // jsonFrameReader是如何利用json.Decoder实现只读取json对象而不执行序列化操作的呢?详情见后面的实现。 84 | decoder *json.Decoder 85 | // remaining这个变量的存在是因为json对象大小不一致造成的,因为每个API对象的大小不可能都相同。 86 | // 用户调用Read()接口的时候无法预知json的大小,提供的缓冲可能不足以容纳一个json对象。 87 | // 此时部分json存入用户的缓存中,而剩余的部分存在remaining中,此时Read()会返回io.ErrShortBuffer。 88 | remaining []byte 89 | } 90 | 91 | // NewJSONFramedReader()是jsonFrameReader的构造函数,更准确的说是json的io.ReadCloser构造函数。 92 | func NewJSONFramedReader(r io.ReadCloser) io.ReadCloser { 93 | return &jsonFrameReader{ 94 | r: r, 95 | // 构造json.Decoder 96 | decoder: json.NewDecoder(r), 97 | } 98 | } 99 | 100 | // Read()实现了io.ReadCloser.Read()接口,用于读取一个json对象到'data'中。 101 | // 如果len(data)小于json对象的大小,则'data'中存储部分json,并返回io.ErrShortBuffer错误。 102 | func (r *jsonFrameReader) Read(data []byte) (int, error) { 103 | // 如果remaining有残留的数据,说明上次调用Read()并没有读取完整,需要将剩余的部分数据继续输出到'data'中。 104 | if n := len(r.remaining); n > 0 { 105 | // 如果'data'的大小比剩余的数据量大,直接将剩余的数据拷贝到'data'中。 106 | if n <= len(data) { 107 | // 下面的代码还是比较讲究的,必须需要解释一下: 108 | // 1. data[0:0]将len(data)变成0,但是内存的地址不变; 109 | // 2. append()用于将remain中的数据拷贝到data中,并将data的大小修改为len(r.remaining) 110 | // 其实下面的代码也可以写成这样: data = data[:copy(data, r.remain)] 111 | data = append(data[0:0], r.remaining...) 112 | r.remaining = nil 113 | return n, nil 114 | } 115 | 116 | // 如果data的大小依然比remain小,那么继续将部分数据拷贝到data中并返回io.ErrShortBuffer 117 | n = len(data) 118 | data = append(data[0:0], r.remaining[:n]...) 119 | r.remaining = r.remaining[n:] 120 | return n, io.ErrShortBuffer 121 | } 122 | 123 | // 获取data的大小,因为下面需要临时将data的大小调整为0,此处需要记录一下。 124 | n := len(data) 125 | // json.RawMessage这就是笔者前面提到利用json.Decoder只读取不反序列化的关键。 126 | // json.RawMessage是[]byte类型的重定义,所以可以将data转换为json.RawMessage类型。 127 | // json.RawMessage实现了Unmarshal接口,所以json.Decoder会调用json.RawMessage.Unmarshal()反序列化对象。 128 | // json.RawMessage.Unmarshal()的实现就是简单的拷贝数据,所以看似调用了Decode()实则做了拷贝。 129 | // 具体的实现读者可以阅读json.Decoder.Decode()的源码实现,笔者点到为止即可。 130 | m := json.RawMessage(data[:0]) 131 | if err := r.decoder.Decode(&m); err != nil { 132 | return 0, err 133 | } 134 | 135 | // 这里的实现也非常精髓,笔者需要解释一下: 136 | // 1. 首先通过类型转换的方式将data[:0]赋值给m,这就是精髓的所在,m地址指向了data,但是大小为0; 137 | // 2. json.RawMessage.Unmarshal()通过append()拷贝读取到的json对象,那么就会出现两种情况: 138 | // 2.1. cap(data)大于等于读取的json数据大小,那么json数据会直接拷贝到data中; 139 | // 2.2. cap(data)小于读取的json数据大小,则m会被赋值新缓冲,并且将数据拷贝到新的缓冲中; 140 | // 3. 简单直白的说:如果data的大小足以容纳读取的json对象则直接读取到data中,否则读取到新申请的缓存中; 141 | if len(m) > n { 142 | // len(m) > n说明m指向了新缓存,那么需要将一部分数据拷贝到data中,再用remain指向剩余部分数据的地址 143 | data = append(data[0:0], m[:n]...) 144 | r.remaining = m[n:] 145 | return n, io.ErrShortBuffer 146 | } 147 | return len(m), nil 148 | } 149 | ``` 150 | 151 | 虽然函数代码不多,逻辑也不复杂,但是实现的细节还是满满的,尽量避免不必要的内存拷贝,这需要对golang的slice类型有充分的认识才行。 152 | 153 | # streaming 154 | 155 | StreamSerializerInfo只是流序列化器的信息,没有任何序列化和反序列化能力。也就是说StreamSerializerInfo提供了构造流序列化器的参数(Framer和Serializer),但不是流序列化器。Kubernetes在streaming包中定义了流解码器和流编码器,专门基于流解码/编码API对象,本章节将解析流解码器/编码器的实现。 156 | 157 | 需要注意,笔者为了简便,将streaming.Decoder和streaming.Encoder简写成Decoder和Encoder;为了与runtime.Decoder和runtime.Encoder有所区分,runtime.Decoder和runtime.Encoder依然采用全名。 158 | 159 | ## Decoder 160 | 161 | Decoder也是一种解码器,与runtime.Decoder就好像json.Decoder.Decode()与json.Unmarshal()之间的区别,前者基于流解码API对象,后者基于[]byte解码API对象。源码链接: 162 | 163 | ```go 164 | // Decoder定义了流解码器的接口 165 | type Decoder interface { 166 | // 从流中解码一个API对象,当没有更多的对象时,将返回io.EOF。 167 | // 相比于runtime.Decoder,参数只是少了输入的数据([]byte),其他部分(参数、返回值)都是一样的。 168 | // 这个非常好理解,因为数据来源于流,需要Decoder读取流中的数据并解码对象。 169 | Decode(defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) 170 | // 关闭流解码器,其实主要是为了关闭底层的流(io.ReadCloser) 171 | Close() error 172 | } 173 | 174 | // decoder实现了Decoder 175 | type decoder struct { 176 | // 输入数据流,其实就是Framer.NewFrameReader()构造的序列化格式的对象流,比如json流。 177 | reader io.ReadCloser 178 | // 解码器,用于解码API对象,decoder应该与reader匹配,即都是json、yaml或者protobuf。 179 | // 这就是StreamSerializerInfo存在的价值,它提供了指定格式的Framer和Serializer。 180 | decoder runtime.Decoder 181 | // 数据缓冲,用于缓冲从reader读取的数据,然后在调用decoder来解码 182 | buf []byte 183 | // 缓冲最大值,单位为字节 184 | maxBytes int 185 | // 从变量名字来看是复位读取的标记,但是什么是复位读取,为什么复位读取? 186 | // 笔者先提一个问题,如果一个序列化对象非常大,以至于最大缓冲都无法容纳该怎么办?笔者认为: 187 | // 1. 这个对象肯定是无法解码的,因为缓冲无法容纳这个对象,也就无法调用解码器解码,所以只能返回一个错误表示对象太大; 188 | // 2. 这个超大的对象不能影响后续对象(因为是流)的解码; 189 | // 基于以上的分析,decoder在遇到一个超大对象后会返回一个错误,但是再次调用解码的时候流中可能残留超大对象的部分数据。 190 | // 所以resetRead的就是用来标记上一次是否读到了超大对象,当成功读取一个序列化对象且resetRead=true就是上一个超大对象的结尾, 191 | // 所以需要在复位读取一次,详情参看Decode()接口实现。 192 | resetRead bool 193 | } 194 | 195 | // NewDecoder()用于创建一个流解码器,从'r'中读取序列化对象并用'd'解码对象。 196 | // 此处对'r'有一些要求,当读取数据时提供的缓冲不够读取一个序列化对象的数据时需要返回ErrShortRead错误。 197 | // 需要注意的是,此处的io.ReadCloser其实是由Framer.NewFrameReader()构建的。 198 | func NewDecoder(r io.ReadCloser, d runtime.Decoder) Decoder { 199 | return &decoder{ 200 | reader: r, 201 | decoder: d, 202 | buf: make([]byte, 1024), // 初始申请1024大小的缓冲 203 | maxBytes: 16 * 1024 * 1024, // 缓冲最大为16MB。 204 | } 205 | } 206 | 207 | // Decode()实现了Decoder.Decode()。 208 | func (d *decoder) Decode(defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { 209 | // base是一个"指针",指向了未来从流中读取数据到缓冲的起始位置,初始为0就是读取的数据放到缓冲的起始位置。 210 | // 这么设计的目的是什么?很简单,序列化对象大小可能比当前缓冲大,一次读取不完整,缓冲扩容后再次读取时, 211 | // 指针就要指向缓冲已经读取对象数据的下一个位置,这个设计很简单,没什么难度。 212 | base := 0 213 | // 为什么需要无限循环,因为可能一次读不完,如果缓冲太小以至于无法读取一个完整的序列化对象,就需要持续读取直到读完为止。 214 | for { 215 | // 尝试读取一个序列化对象(帧) 216 | n, err := d.reader.Read(d.buf[base:]) 217 | // 缓冲大小不足? 218 | if err == io.ErrShortBuffer { 219 | // 读取了0个字节,又返回缓冲大小不足,这应该是异常了吧? 220 | if n == 0 { 221 | return nil, nil, fmt.Errorf("got short buffer with n=0, base=%d, cap=%d", base, cap(d.buf)) 222 | } 223 | if d.resetRead { 224 | // d.resetRead为true表示上一次读取到了超大对象,此次读取的数据还是该超大对象的一部分, 225 | // 所以应该丢弃当前读取的数据并继续读取直到读取到结尾为止。resetRead为true说明缓冲已经达到最大了,所以也没必要扩容了。 226 | continue 227 | } 228 | // 如果缓冲大小还没有达到最大值,就扩容缓冲,再读一次 229 | if len(d.buf) < d.maxBytes { 230 | // 偏移指针到下一次读取数据存放的位置 231 | base += n 232 | // 在原有缓冲大小的基础上再追加同等大小的空数据,可以理解为缓冲大小扩容2倍。 233 | // 一旦扩容后的大小大于cap(d.buf)就会申请新的内存,需要注意这个操作:拷贝已经读取的数据到新的缓冲中 234 | d.buf = append(d.buf, make([]byte, len(d.buf))...) 235 | continue 236 | } 237 | // 对象太大了,返回ErrObjectTooLarge的同时将resetRead设置为true, 238 | // 标识下次调用的时候需要将该超大对象的残留数据读取出来并丢弃。 239 | d.resetRead = true 240 | base = 0 // 其实这个赋值没啥必要,毕竟需要退出函数了 241 | return nil, nil, ErrObjectTooLarge 242 | } 243 | // 其他错误直接返回,大部分应该是io.EOF,当然也有其他错误的可能。 244 | if err != nil { 245 | return nil, nil, err 246 | } 247 | // 到此,已经读取了一个完整的对象数据 248 | if d.resetRead { 249 | // resetRead为true说明上一次解码读取了一个超大的对象,当前读取完该超大对象的残留数据。 250 | // 清除resetRead标记,重新读取一次才是此次需要解码的API对象,这也就是复位的由来吧。 251 | d.resetRead = false 252 | continue 253 | } 254 | // 偏移指针,因为要读取循环,一次指针偏移的目的就是为了计算读取的数据大小 255 | base += n 256 | break 257 | } 258 | // 解码对象,因为序列化对象的最后一个字节在d.buf[base-1],所以用缓冲的[0,base)来解码API对象。 259 | return d.decoder.Decode(d.buf[:base], defaults, into) 260 | } 261 | ``` 262 | 263 | ## Encoder 264 | 265 | 既然有流解码的需求,流编码器也是有存在的必要的。流解码器作为消费者消费流中的对象,而流编码器则是作为生产者向流中写入对象。流编码器要简单很多,只需要一个接一个对象的编码并写入流即可, 266 | 267 | ```go 268 | // Encoder定义了流编码器的接口。 269 | type Encoder interface { 270 | // 将obj写入流,与runtime.Encoder.Encode()接口不同的是参数中没有io.Writer参数。 271 | // 因为在构造Encoder对象时提供了io.Writer,这样就不用每次调用Encode()时再提供了。 272 | Encode(obj runtime.Object) error 273 | } 274 | 275 | // encoder实现了Encoder接口。 276 | type encoder struct { 277 | // 这个应该不用介绍了 278 | writer io.Writer 279 | // 因为Encode传入的是API对象,需要将API对象编码成字节数据后在写入流 280 | encoder runtime.Encoder 281 | // 用来缓存API对象编码后的数据。 282 | // 因为bytes.Buffer实现了io.Writer,所以runtime.Encoder.Encode()可以将序列化的数据写入bytes.Buffer。 283 | buf *bytes.Buffer 284 | } 285 | 286 | // NewEncoder()是Encoder的构造函数,需要提供io.Writer和解码器,这个应该比较好理解。 287 | func NewEncoder(w io.Writer, e runtime.Encoder) Encoder { 288 | return &encoder{ 289 | writer: w, 290 | encoder: e, 291 | buf: &bytes.Buffer{}, 292 | } 293 | } 294 | 295 | // Encode()实现了Encoder.Encode()。 296 | func (e *encoder) Encode(obj runtime.Object) error { 297 | // 编码API对象。 298 | if err := e.encoder.Encode(obj, e.buf); err != nil { 299 | return err 300 | } 301 | // 将序列化数据写入流 302 | _, err := e.writer.Write(e.buf.Bytes()) 303 | // 复位缓存,为下次编码做准备 304 | e.buf.Reset() 305 | return err 306 | } 307 | ``` 308 | 309 | # 总结 310 | 311 | 1. streaming.Decoder和runtime.Decoder都是用来解码器API对象,前者基于流(io.Reader)解码,后者基于缓存([]byte)解码; 312 | 2. streaming.Encoder和runtime.Encoder都是用来编码器API对象,前者基于流(io.Reader)编码,后者基于缓存([]byte)编码; 313 | 3. 流编解码器的典型应用场景是[RSETClient的Watch](../client-go/rest/../../../client-go/rest/Request.md#Watch) 314 | -------------------------------------------------------------------------------- /client-go.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /client-go/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 目录 8 | 9 | 1. [rest](./rest/README.md) 10 | 2. [tools](./tools/README.md) 11 | -------------------------------------------------------------------------------- /client-go/rest/Client.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | apiserver的REST客户端可以类比为http.Client,至少它继承了http.Client的功能(更准确的说应该是有一个http.Client的成员变量),如果读者对http.Client不了解,那么建议先简单学习一下,这非常有助于理解本文的内容。 10 | 11 | 为了区分apiserver的REST客户端和http.Client,本文将用RESTClient表示apiserver的REST客户端,而http.Client则直接引用齐全名。因为apiserver是一个相对标准的REST服务,所以访问apiserver的客户端最终都要通过http.Client实现,那么RESTClient继承http.Client的功能就很好理解了(笔者注:此处用继承功能而不是继承,是出于概念严谨考虑的,因为继承功能的方法很多,比如采用成员变量亦或是直接集成)。http.Client只是具备http客户端基本功能,Kubernetes对客户端还提出了其他需求,所以就有了RESTClient,具体是什么需求,后续章节会给出答案。 12 | 13 | `本文引用源码为github.com/kubernetes/client-go的release-1.21分支。` 14 | 15 | # Client 16 | 17 | ## Interface 18 | 19 | Interface是Kubernetes对REST客户端抽象的接口,从Interface的定义我们可以看出Kubernetes对REST于客户端的需求,源码链接: 20 | 21 | ```go 22 | type Interface interface { 23 | // 获取限速器,关于限速器读者可以暂时不用深入理解,只需要知道它是用来控制客户端向服务端提交请求的QPS即可。 24 | // 通过这个接口笔者可以得出一个结论:Kubernetes要求RESTClient有限速能力,这个是http.Client不具备的。 25 | // 限速的目的很简单,避免某个客户端恶意/异常操作对apiserver造成压力,进而影响整个系统。 26 | GetRateLimiter() flowcontrol.RateLimiter 27 | // 创建指定动词的REST请求(Request),这个有点意思了,说明Kubernetes的REST请求是可以RESTClient创建的。 28 | // 熟悉http.Request的同学都知道,http.Request是不需要依赖http.Client创建的。 29 | // 通过这个接口笔者可以得出一个结论:Kubernetes要求RESTClient有创建Request的能力,这也是http.Client不具备的。 30 | Verb(verb string) *Request 31 | // 等同于Verb("POST") 32 | Post() *Request 33 | // 等同于Verb("PUT") 34 | Put() *Request 35 | // 等同于Verb("PATCH") 36 | Patch(pt types.PatchType) *Request 37 | // 等同于Verb("GET") 38 | Get() *Request 39 | // 等同于Verb("DELETE") 40 | Delete() *Request 41 | // 获取API组和版本,Kubernetes的每个RESTClient只能操作某个组/版本的API对象。 42 | // 这也就可以理解为什么需要RESTClient创建Request,因为需要创建的Request操作范围指定在某个API组/版本内。 43 | // 是不是可以想象得到Clientset的实现?其实就是所有API组/版本的RESTClient集合,所以称之为Clientset。 44 | APIVersion() schema.GroupVersion 45 | } 46 | ``` 47 | 48 | 通过对Interface接口的分析,一句话概括为:“RESTClient用于创建操作指定组/版本API对象的Request,同时具有限速能力”。总结的是不是很到位?那么问题来了,只需要创建Request,这跟http.Client一毛钱关系都没有,那为啥前文说可以类比http.Client呢?你想啊,创建Request不是目的,目的是将Request提交给apiserver,那靠啥提交请求呢?Interface只是接口,不会反应具体的实现,它只是需求提出方。要想知道原因,且看下文的解析。 49 | 50 | ## RESTClient 51 | 52 | RESTClient确实有这个类型,而且实现了Interface接口,笔者前言就开始使用RESTClient也是为了思路的连贯性,源码链接: 53 | 54 | ```go 55 | // RESTClient实现了Interface接口,所有的成员变量除了限速器和APIVersion,其他的基本都是为了创建Request而设计的。 56 | type RESTClient struct { 57 | // 客户端所有调用的根URL,比如https://192.168.1.2:6443,是生成Request的最终URL的必须参数 58 | base *url.URL 59 | // 资源路径的一段,连接base就是资源的根路径,比如/apis/apps/v1(对应于apps组v1版本,/apis是绝大部分API的前缀) 60 | // versionedAPIPath是生成Reqeust的最终URL的必须参数 61 | versionedAPIPath string 62 | 63 | // 与HTTP内容相关的配置,下面有注释 64 | content ClientContentConfig 65 | 66 | // 退避管理器的构造函数,首先需要知道什么是退避,说的简单点就是向apiserver提交请求后出错,如果需要重试则需要等待一段时间,这就是退避。 67 | // 其次需要知道退避管理器的功能是什么?退避管理器有点类似于map[string]int,用来记录不同对象(键)的退避次数,然后根据退避次数计算退避时间。 68 | // 至于退避管理器的键是什么,后面会有介绍,此处只需要知道向apiserver提交请求可能会失败,失败可能需要重试,重试可能需要计算退避时间。 69 | // createBackoffMgr就是用来给Request创建自己的退避管理器的。 70 | createBackoffMgr func() BackoffManager 71 | 72 | // 限速器,首先是为了实现Interface.GetRateLimiter()接口,其次,所有创建的Request共享限速器。 73 | // 所以rateLimiter用来限制RESTClient创建的Request向apiserver提交请求的QPS。至于flowcontrol.RateLimiter的实现感兴趣的读者可以自己了解一下。 74 | rateLimiter flowcontrol.RateLimiter 75 | 76 | // warningHandler,而不是errorHandler,用来处理一些warning等级的异常,这些异常不会终止程序执行。 77 | // warningHandler也是Request之间共享使用的。 78 | warningHandler WarningHandler 79 | 80 | // 这个应该没什么好解释的了,提交HTTP请求必须的对象,如果没有设置将使用http.DefaultClient。 81 | // 前文提到RESTClient继承了http.Client就是通过成员变量实现的。 82 | Client *http.Client 83 | } 84 | 85 | // ClientContentConfig是客户端关于HTTP内容(http.Request.Body和http.Response.Body)的配置。 86 | type ClientContentConfig struct { 87 | // 指定客户端将接受的内容类型,如果没有设置,将使用ContentType设置Accept请求头。就是RESTClient希望apiserver响应内容的类型。 88 | // 关于Accept请求头参看:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept 89 | AcceptContentTypes string 90 | // 指定用于与服务端通信的格式,如果未设置AcceptContentTypes,则将用于设置Accept请求头,并设置为给服务端的任何对象的默认内容类型。 91 | // 如果没有设置,则使用"application/json"。就是告诉服务端(apiserver)http.Request.Body的格式。 92 | ContentType string 93 | // API组和版本,直接初始化RESTClient时必须提供。 94 | GroupVersion schema.GroupVersion 95 | // Negotiator用于获取支持的多种媒体类型的编码器和解码器,关于Negotiator读者可以暂时不用深入理解,只需要知道他可以根据媒体类型获取编解码器就可以了。 96 | // 至于什么是编解码器,说白了就是用来序列化和反序列化API对象的,可以直接理解为json.Marshal()和json.Unmarsal()。 97 | // 因为AcceptContentTypes和ContentType两个配置,所以需要根据相应的类型获取编解码器。 98 | Negotiator runtime.ClientNegotiator 99 | } 100 | ``` 101 | 102 | 看为了RESTClient的定义,接下来看看RESTClient的构造函数,源码链接: 103 | 104 | ```go 105 | // NewRESTClient()是RESTClient的构造函数,从构造函数的参数上看,RESTClient大部分成员变量都是外部传入的。 106 | func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ClientContentConfig, rateLimiter flowcontrol.RateLimiter, client *http.Client) (*RESTClient, error) { 107 | // ContentType如果没有设置,默认使用"application/json",默认的编码器和解码器也可以看做是json.Marshal()和json.Unmarshal()。 108 | if len(config.ContentType) == 0 { 109 | config.ContentType = "application/json" 110 | } 111 | 112 | // 初始化baseURL,这个比较好理解。 113 | base := *baseURL 114 | if !strings.HasSuffix(base.Path, "/") { 115 | base.Path += "/" 116 | } 117 | base.RawQuery = "" 118 | base.Fragment = "" 119 | 120 | // 返回RESTClient对象 121 | return &RESTClient{ 122 | base: &base, 123 | versionedAPIPath: versionedAPIPath, 124 | content: config, 125 | // readExpBackoffConfig是构造退避管理器的函数,下面有注释 126 | createBackoffMgr: readExpBackoffConfig, 127 | rateLimiter: rateLimiter, 128 | 129 | Client: client, 130 | }, nil 131 | } 132 | 133 | // readExpBackoffConfig()是RESTClient构造退避管理器的函数 134 | func readExpBackoffConfig() BackoffManager { 135 | // 通过环境变量获取退避的初始时间和最大时间,单位为秒。 136 | backoffBase := os.Getenv(envBackoffBase) 137 | backoffDuration := os.Getenv(envBackoffDuration) 138 | 139 | // 因为环境变量是字符串型,所以此处需要转为整型 140 | backoffBaseInt, errBase := strconv.ParseInt(backoffBase, 10, 64) 141 | backoffDurationInt, errDuration := strconv.ParseInt(backoffDuration, 10, 64) 142 | // 如果没有配置环境变量或者错误配置,则无需退避 143 | if errBase != nil || errDuration != nil { 144 | return &NoBackoff{} 145 | } 146 | // URLBackoff是以URL为键计算退避时间,也就是说某个URL访问错误,如果立刻访问相同的URL就需要退避一段时间,退避时间随着错误次数增加以2的指数增加。 147 | // 此处需要注意的是,URL是baseURL,这个应该好理解,某个主机访问出错,立刻访问这个主机大概率还是可能会出错,即便访问的是不同的API。 148 | return &URLBackoff{ 149 | Backoff: flowcontrol.NewBackOff( 150 | time.Duration(backoffBaseInt)*time.Second, 151 | time.Duration(backoffDurationInt)*time.Second)} 152 | } 153 | ``` 154 | 155 | 看完了RESTClient的定义,接下来看看RESTClient的构造函数,源码链接: 156 | 157 | ```go 158 | // Verb()创建了Request对象,然后设置Request的verb属性。 159 | func (c *RESTClient) Verb(verb string) *Request { 160 | return NewRequest(c).Verb(verb) 161 | } 162 | 163 | // Post()利用Verb()实现,这个非常简单。 164 | func (c *RESTClient) Post() *Request { 165 | return c.Verb("POST") 166 | } 167 | 168 | // Put()利用Verb()实现,这个非常简单。 169 | func (c *RESTClient) Put() *Request { 170 | return c.Verb("PUT") 171 | } 172 | 173 | // Patch()利用Verb()创建了Request,然后将Patch类型设置Content-Type请求头,读者可以看下types.PatchType的枚举定义就更容易理解了。 174 | func (c *RESTClient) Patch(pt types.PatchType) *Request { 175 | return c.Verb("PATCH").SetHeader("Content-Type", string(pt)) 176 | } 177 | 178 | // Get()利用Verb()实现,这个非常简单。 179 | func (c *RESTClient) Get() *Request { 180 | return c.Verb("GET") 181 | } 182 | 183 | // Delete()利用Verb()实现,这个非常简单。 184 | func (c *RESTClient) Delete() *Request { 185 | return c.Verb("DELETE") 186 | } 187 | 188 | // 获取API组和版本。 189 | func (c *RESTClient) APIVersion() schema.GroupVersion { 190 | return c.content.GroupVersion 191 | } 192 | ``` 193 | 194 | 感觉RESTClient实现Interface接口非常简单,其实这里面最核心的函数是NewRequest(),利用RESTClient构造Request对象,笔者会在[解析Request的文章](./Request.md)详细说明。 195 | 196 | # 总结 197 | 198 | 1. RESTClient是操作某个组/版本(比如apps/v1)的API对象的REST客户端; 199 | 2. RESTClient的主要功能就是创建Request,甚至可以理解为RequestFactory; 200 | 3. RESTClient的限流器是所有Request共享使用的,也就是说RESTClient创建的Request虽然可以并发提交,但是都会被限流器统一限制在某个QPS以内; 201 | 4. RESTClient的构造函数传入了限流器,不是自己构造的,可以推测多个RESTClient(比如apps/v1、core/v1)之间共享同一个限流器; 202 | 5. 退避管理器是以URL(base)为键管理退避时间,同一个Request向不同的host提交产生的错误不会累加,单独计算退避时间; 203 | -------------------------------------------------------------------------------- /client-go/rest/Config.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | # 前言 9 | 10 | [RESTClient](./Client.md#RESTClient)的文章中关于构造函数部分,虽然清晰的解析了构造函数的实现,但是对于构造函数的参数来源并没有给出答案。同时在总结部分推测多个RESTClient(比如apps/v1、core/v1)之间共享同一个限流器,本文将针对这部分内容给出答案。这就需要深度解析配置(Config)的实现,Kubernetes利用配置创建RESTClient,这很好理解,也是我们日常变成中比较常见的方法。 11 | 12 | `本文引用源码为github.com/kubernetes/client-go的release-1.21分支。` 13 | 14 | # Config 15 | 16 | 我们平时一般用一个结构体描述配置,Kubernetes也不例外,源码链接: 17 | 18 | ```go 19 | // 在了解Config之前,笔者需要在这里简单回顾一下RESTClient构造函数的参数,因为Config就是用来配置RESTClient构造函数的参数: 20 | // 1. baseURL(url.URL): 基础URL,比如https://192.168.1.2:6443 21 | // 2. versionedAPIPath(string): API组/版本的路径,比如/apis/app/v1 22 | // 3. config(ClientContentConfig): http.Request.Body和http.Response.Body的内容类型以及相关的编解码器 23 | // 4. rateLimiter(flowcontrol.RateLimiter): 限速器 24 | // 5. client(http.Client): HTTP客户端 25 | type Config struct { 26 | // Host必须是一个主机字符串,host:port(比如https://192.168.1.2:6443)或一个指向apiserver基础的URL。 27 | // 简单一句话就是apiserver的URL,Host是API对象的全URL的一部分,用来定位apiserver的位置。 28 | // Host最终会通过类型转换成(string->url.URL)为baseURL 29 | Host string 30 | // API根路径,不同的API组可能在不同的根目录下,大部分API组都在/apis路径下,core组的API在/api路径下。 31 | // 如果说Host用来定位apiserver的位置,那么APIPath用来定位API组在apiserver的哪个路径下。 32 | // 此处为什么强调API组?因为RESTClient是某个API组的客户端,所以APIPath是某个API组在apiserver的根路径。 33 | APIPath string 34 | 35 | // 关于HTTP内容(http.Request.Body和http.Response.Body)的配置。 36 | // 与RESTClient文章中介绍ClientContentConfig相似,最终需要转换为ClientContentConfig来创建RESTClient. 37 | ContentConfig 38 | 39 | // 用户名和密码,大部分情况应该不使用这种认证方式。 40 | // Config中有很多成员变量是用来创建hppt.Client的,比如Username、Password。 41 | // 本文的目标是分析如何利用Config创建RESTClient,虽然http.Client也是创建RESTClient的必须参数之一。 42 | // 但是通过Config创建http.Client的部分笔者会单独在transport相关文章中进行解析,本文可以假设使用http.DefaultClient即可。 43 | Username string 44 | Password string `datapolicy:"password"` 45 | 46 | // 创建http.Client相关的参数,暂不注释。 47 | BearerToken string `datapolicy:"token"` 48 | 49 | // 创建http.Client相关的参数,暂不注释。 50 | BearerTokenFile string 51 | 52 | // 创建http.Client相关的参数,暂不注释。 53 | Impersonate ImpersonationConfig 54 | 55 | // 创建http.Client相关的参数,暂不注释。 56 | AuthProvider *clientcmdapi.AuthProviderConfig 57 | 58 | // 创建http.Client相关的参数,暂不注释。 59 | AuthConfigPersister AuthProviderConfigPersister 60 | 61 | // 创建http.Client相关的参数,暂不注释。 62 | ExecProvider *clientcmdapi.ExecConfig 63 | 64 | // TLS配置,创建http.Client相关的参数,暂不注释。 65 | TLSClientConfig 66 | 67 | // 创建http.Client相关的参数,暂不注释。 68 | // UserAgent请求头参看:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/User-Agent 69 | UserAgent string 70 | 71 | // 创建http.Client相关的参数,暂不注释。 72 | DisableCompression bool 73 | 74 | // 创建http.Client相关的参数,暂不注释。 75 | Transport http.RoundTripper 76 | // 创建http.Client相关的参数,暂不注释。 77 | WrapTransport transport.WrapperFunc 78 | 79 | // QPS指示从此客户端到主服务端的最大QPS。如果为零,则创建的RESTClient将使用默认QPS(5) 80 | QPS float32 81 | 82 | // 与QPS一起用于创建限流器,RESTClient限流器采用令牌桶算法,Burst是一次可以获取的最大令牌数,感兴趣的读者可以自己看下相关实现。 83 | // 如果为零,则创建的RESTClient将使用默认Burst(10)。 84 | Burst int 85 | 86 | // 限速器,如果设置了限速器则覆盖QPS/Burst构造的限速器 87 | RateLimiter flowcontrol.RateLimiter 88 | 89 | // warning等级异常处理器,赋值给在RESTClient,所有的Request共享使用。 90 | // Kubernetes默认的WarningHandler实现就是简单的写日志,后文有注释。 91 | WarningHandler WarningHandler 92 | 93 | // 等待超时时间,零值表示没有超时。 94 | Timeout time.Duration 95 | 96 | // 创建http.Client相关的参数,暂不注释。 97 | Dial func(ctx context.Context, network, address string) (net.Conn, error) 98 | 99 | // 创建http.Client相关的参数,暂不注释。 100 | Proxy func(*http.Request) (*url.URL, error) 101 | } 102 | ``` 103 | 104 | 从Config的定义可以看出大部分都是用来创建http.Client的,这也间接的说明创建http.Client是一个相对复杂的过程,所以笔者单独一系列文章解析Kubernetes是如何创建http.Client的,相信能够学到不少东西。 105 | 106 | ## RESTClientFor 107 | 108 | 前面已经知道了Config,接下来我们来看看是如何利用Config创建RESTClient的,源码链接: 109 | 110 | ```go 111 | // RESTClientFor()根据配置创建RESTClient 112 | func RESTClientFor(config *Config) (*RESTClient, error) { 113 | // 必须配置API组/版本,因为RESTClient用来操作指定组/版本的API对象,不理解的话可以阅读笔者的RESTClient文章 114 | if config.GroupVersion == nil { 115 | return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient") 116 | } 117 | // 必须配置NegotiatedSerializer(不好翻译),因为提交http.Request和处理http.Response需要API对象的编码器和解码器 118 | if config.NegotiatedSerializer == nil { 119 | return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") 120 | } 121 | 122 | // defaultServerUrlFor()函数本文就不详细注释了,可以简单理解为以下两步: 123 | // 1. baseURL将config.Host字符串转换为url.URL类型,可以理解为一次类型装换,这个是创建RESTClient需要的; 124 | // 2. versionedAPIPath=/config.APIPath/config.GroupVersion.Group/config.GroupVersion.Version,比如/apis/apps/v1 125 | baseURL, versionedAPIPath, err := defaultServerUrlFor(config) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | // 创建http.Client,这是一个比较复杂的过程,此处不做详细说明。 131 | transport, err := TransportFor(config) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | var httpClient *http.Client 137 | if transport != http.DefaultTransport { 138 | httpClient = &http.Client{Transport: transport} 139 | if config.Timeout > 0 { 140 | httpClient.Timeout = config.Timeout 141 | } 142 | } 143 | 144 | // 如果配置了限速器就用配置的限速器,否则根据QPS和Burst创建一个限速器 145 | rateLimiter := config.RateLimiter 146 | if rateLimiter == nil { 147 | qps := config.QPS 148 | if config.QPS == 0.0 { 149 | // 如果没有配置QPS则使用默认值5 150 | qps = DefaultQPS 151 | } 152 | burst := config.Burst 153 | if config.Burst == 0 { 154 | // 如果没有配置Burst则使用默认值100 155 | burst = DefaultBurst 156 | } 157 | if qps > 0 { 158 | // 创建一个令牌桶算法的限速器,感兴趣的读者可以了解一下。 159 | // 需要注意的是,新建的限速器并没有赋值给config,所以一点没有配置限速器,则RESTClient自己独享一个限速器。 160 | rateLimiter = flowcontrol.NewTokenBucketRateLimiter(qps, burst) 161 | } 162 | } 163 | 164 | // 创建ClientContentConfig,如果看过了RESTClient的文章这部分就很好理解了。 165 | var gv schema.GroupVersion 166 | if config.GroupVersion != nil { 167 | gv = *config.GroupVersion 168 | } 169 | clientContent := ClientContentConfig{ 170 | AcceptContentTypes: config.AcceptContentTypes, 171 | ContentType: config.ContentType, 172 | GroupVersion: gv, 173 | Negotiator: runtime.NewClientNegotiator(config.NegotiatedSerializer, gv), 174 | } 175 | 176 | // 创建RESTClient 177 | restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient) 178 | if err == nil && config.WarningHandler != nil { 179 | // 为RESTClient设置WarningHandler 180 | restClient.warningHandler = config.WarningHandler 181 | } 182 | return restClient, err 183 | } 184 | ``` 185 | 186 | 上面的代码虽然将Config.WarningHandler赋值给了RESTClient,但是Config.WarningHandler可能是nil。如果RESTClient.warningHandler为nil,将使用默认的WarningHandler,源码链接: 187 | 188 | ```go 189 | // WarningLogger实现了WarningHandler,是默认的WarningHandler,当然Kubernetes也提供了设置默认WarningHandler的接口。 190 | type WarningLogger struct{} 191 | 192 | // HandleWarningHeader()实现了WarningHandler.HandleWarningHeader()接口。 193 | func (WarningLogger) HandleWarningHeader(code int, agent string, message string) { 194 | // 只处理299的状态码,说明这些报警都使用299状态码,然后用message区分 195 | if code != 299 || len(message) == 0 { 196 | return 197 | } 198 | // 就是将message打印到日志中 199 | klog.Warning(message) 200 | } 201 | ``` 202 | 203 | 笔者在RESTClient和Request的文章中只是简单的提了一下WarningHandler,并没有解释到底有什么功能,本文给出了默认的实现,就是写日志。当前还有其他的WarningHandler实现,笔者就不一一注释了,毕竟不是本文的重点内容。 204 | 205 | 通过Config创建RESTClient除了创建http.Client部分,其他都还比较好理解,但是笔者还有一个问题,就是到底有没有配置限速器呢?这就需要追溯到Clientset,Clientset是所有API组/版本RESTClient的集合,在Clientset的构造函数中需要创建每个API组/版本的RESTClient。后续章节将通过Clientset的构造函数解析Kubernetes是如何通过Config创建RESTClient。 206 | 207 | ## NewForConfig 208 | 209 | kubernetes.NewForConfig()是Client的构造函数,此处加了包名是避免与后文同名的函数混淆,源码链接: 210 | 211 | ```go 212 | func NewForConfig(c *rest.Config) (*Clientset, error) { 213 | // 报备Config,是因为函数会修改Config,这样可以避免修改用户传入的参数。 214 | // configShallowCopy变量名字有点意思,特意强调ShallowCopy而非DeepCopy,这二者的不同读者真的了解么? 215 | configShallowCopy := *c 216 | // 如果没有配置限速器,同时设置了QPS,则创建限速器,结合RESTClientFor()函数,总结如下: 217 | // 1. Config.RateLimiter == nil && Config.QPS > 0,则Clientset创建一个Config.QPS的限速器,所有RESTClient共享; 218 | // 2. Config.RateLimiter == nil && Config.QPS == 0,则每个API组/版本的RESTClient自己创建一个默认QPS为5的限速器; 219 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 220 | if configShallowCopy.Burst <= 0 { 221 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 222 | } 223 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 224 | } 225 | var cs Clientset 226 | var err error 227 | // 下面是Clientset创建每个API组/版本的RESTClient,比较简单,就不在注释了 228 | cs.admissionregistrationV1, err = admissionregistrationv1.NewForConfig(&configShallowCopy) 229 | if err != nil { 230 | return nil, err 231 | } 232 | cs.admissionregistrationV1beta1, err = admissionregistrationv1beta1.NewForConfig(&configShallowCopy) 233 | if err != nil { 234 | return nil, err 235 | } 236 | cs.internalV1alpha1, err = internalv1alpha1.NewForConfig(&configShallowCopy) 237 | if err != nil { 238 | return nil, err 239 | } 240 | cs.appsV1, err = appsv1.NewForConfig(&configShallowCopy) 241 | if err != nil { 242 | return nil, err 243 | } 244 | cs.appsV1beta1, err = appsv1beta1.NewForConfig(&configShallowCopy) 245 | if err != nil { 246 | return nil, err 247 | } 248 | cs.appsV1beta2, err = appsv1beta2.NewForConfig(&configShallowCopy) 249 | if err != nil { 250 | return nil, err 251 | } 252 | cs.authenticationV1, err = authenticationv1.NewForConfig(&configShallowCopy) 253 | if err != nil { 254 | return nil, err 255 | } 256 | cs.authenticationV1beta1, err = authenticationv1beta1.NewForConfig(&configShallowCopy) 257 | if err != nil { 258 | return nil, err 259 | } 260 | cs.authorizationV1, err = authorizationv1.NewForConfig(&configShallowCopy) 261 | if err != nil { 262 | return nil, err 263 | } 264 | cs.authorizationV1beta1, err = authorizationv1beta1.NewForConfig(&configShallowCopy) 265 | if err != nil { 266 | return nil, err 267 | } 268 | cs.autoscalingV1, err = autoscalingv1.NewForConfig(&configShallowCopy) 269 | if err != nil { 270 | return nil, err 271 | } 272 | cs.autoscalingV2beta1, err = autoscalingv2beta1.NewForConfig(&configShallowCopy) 273 | if err != nil { 274 | return nil, err 275 | } 276 | cs.autoscalingV2beta2, err = autoscalingv2beta2.NewForConfig(&configShallowCopy) 277 | if err != nil { 278 | return nil, err 279 | } 280 | cs.batchV1, err = batchv1.NewForConfig(&configShallowCopy) 281 | if err != nil { 282 | return nil, err 283 | } 284 | cs.batchV1beta1, err = batchv1beta1.NewForConfig(&configShallowCopy) 285 | if err != nil { 286 | return nil, err 287 | } 288 | cs.certificatesV1, err = certificatesv1.NewForConfig(&configShallowCopy) 289 | if err != nil { 290 | return nil, err 291 | } 292 | cs.certificatesV1beta1, err = certificatesv1beta1.NewForConfig(&configShallowCopy) 293 | if err != nil { 294 | return nil, err 295 | } 296 | cs.coordinationV1beta1, err = coordinationv1beta1.NewForConfig(&configShallowCopy) 297 | if err != nil { 298 | return nil, err 299 | } 300 | cs.coordinationV1, err = coordinationv1.NewForConfig(&configShallowCopy) 301 | if err != nil { 302 | return nil, err 303 | } 304 | cs.coreV1, err = corev1.NewForConfig(&configShallowCopy) 305 | if err != nil { 306 | return nil, err 307 | } 308 | cs.discoveryV1, err = discoveryv1.NewForConfig(&configShallowCopy) 309 | if err != nil { 310 | return nil, err 311 | } 312 | cs.discoveryV1beta1, err = discoveryv1beta1.NewForConfig(&configShallowCopy) 313 | if err != nil { 314 | return nil, err 315 | } 316 | cs.eventsV1, err = eventsv1.NewForConfig(&configShallowCopy) 317 | if err != nil { 318 | return nil, err 319 | } 320 | cs.eventsV1beta1, err = eventsv1beta1.NewForConfig(&configShallowCopy) 321 | if err != nil { 322 | return nil, err 323 | } 324 | cs.extensionsV1beta1, err = extensionsv1beta1.NewForConfig(&configShallowCopy) 325 | if err != nil { 326 | return nil, err 327 | } 328 | cs.flowcontrolV1alpha1, err = flowcontrolv1alpha1.NewForConfig(&configShallowCopy) 329 | if err != nil { 330 | return nil, err 331 | } 332 | cs.flowcontrolV1beta1, err = flowcontrolv1beta1.NewForConfig(&configShallowCopy) 333 | if err != nil { 334 | return nil, err 335 | } 336 | cs.networkingV1, err = networkingv1.NewForConfig(&configShallowCopy) 337 | if err != nil { 338 | return nil, err 339 | } 340 | cs.networkingV1beta1, err = networkingv1beta1.NewForConfig(&configShallowCopy) 341 | if err != nil { 342 | return nil, err 343 | } 344 | cs.nodeV1, err = nodev1.NewForConfig(&configShallowCopy) 345 | if err != nil { 346 | return nil, err 347 | } 348 | cs.nodeV1alpha1, err = nodev1alpha1.NewForConfig(&configShallowCopy) 349 | if err != nil { 350 | return nil, err 351 | } 352 | cs.nodeV1beta1, err = nodev1beta1.NewForConfig(&configShallowCopy) 353 | if err != nil { 354 | return nil, err 355 | } 356 | cs.policyV1, err = policyv1.NewForConfig(&configShallowCopy) 357 | if err != nil { 358 | return nil, err 359 | } 360 | cs.policyV1beta1, err = policyv1beta1.NewForConfig(&configShallowCopy) 361 | if err != nil { 362 | return nil, err 363 | } 364 | cs.rbacV1, err = rbacv1.NewForConfig(&configShallowCopy) 365 | if err != nil { 366 | return nil, err 367 | } 368 | cs.rbacV1beta1, err = rbacv1beta1.NewForConfig(&configShallowCopy) 369 | if err != nil { 370 | return nil, err 371 | } 372 | cs.rbacV1alpha1, err = rbacv1alpha1.NewForConfig(&configShallowCopy) 373 | if err != nil { 374 | return nil, err 375 | } 376 | cs.schedulingV1alpha1, err = schedulingv1alpha1.NewForConfig(&configShallowCopy) 377 | if err != nil { 378 | return nil, err 379 | } 380 | cs.schedulingV1beta1, err = schedulingv1beta1.NewForConfig(&configShallowCopy) 381 | if err != nil { 382 | return nil, err 383 | } 384 | cs.schedulingV1, err = schedulingv1.NewForConfig(&configShallowCopy) 385 | if err != nil { 386 | return nil, err 387 | } 388 | cs.storageV1beta1, err = storagev1beta1.NewForConfig(&configShallowCopy) 389 | if err != nil { 390 | return nil, err 391 | } 392 | cs.storageV1, err = storagev1.NewForConfig(&configShallowCopy) 393 | if err != nil { 394 | return nil, err 395 | } 396 | cs.storageV1alpha1, err = storagev1alpha1.NewForConfig(&configShallowCopy) 397 | if err != nil { 398 | return nil, err 399 | } 400 | 401 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 402 | if err != nil { 403 | return nil, err 404 | } 405 | return &cs, nil 406 | } 407 | ``` 408 | 409 | Clientset中虽然很多RESTClient对象,但是创建方法都是千篇一律,本文只挑选创建core/v1的RESTClient的代码作为代表,其他的读者可以自己阅读,源码链接: 410 | 411 | ```go 412 | // NewForConfig()创建core/v1的RESTClient,需要注意的是CoreV1Client继承了rest.Interface,而RESTClient是rest.Interface的唯一实现。 413 | // 所以创建CoreV1Client基本等同于创建RESTClient。 414 | func NewForConfig(c *rest.Config) (*CoreV1Client, error) { 415 | // 设置默认配置,这个有意思了,传入的配置还需要设置什么默认配置?难道说有什么配置参数还没有设置不成?详情见下面注释。 416 | config := *c 417 | if err := setConfigDefaults(&config); err != nil { 418 | return nil, err 419 | } 420 | // 根据Config创建RESTClient 421 | client, err := rest.RESTClientFor(&config) 422 | if err != nil { 423 | return nil, err 424 | } 425 | // 返回CoreV1Client对象 426 | return &CoreV1Client{client}, nil 427 | } 428 | 429 | // setConfigDefaults()设置默认配置 430 | func setConfigDefaults(config *rest.Config) error { 431 | // 哈哈,这下明白默认配置是啥了,每个API的组/版本不同,而Clientset使用同一个Config创建所有的RESTClient, 432 | // 所以需要每个API组/版本设置自己的相关配置,没毛病。 433 | gv := v1.SchemeGroupVersion 434 | // 设置API的组/版本 435 | config.GroupVersion = &gv 436 | // core/v1的API路径是/api,apps/v1的API路径是/apis 437 | config.APIPath = "/api" 438 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 439 | 440 | // 如果没有设置用户代理,Kubernetes有默认的用户代理,就是用应用程序名、版本、操作系统、体系架构以及git提交ID构建的。 441 | if config.UserAgent == "" { 442 | config.UserAgent = rest.DefaultKubernetesUserAgent() 443 | } 444 | 445 | return nil 446 | } 447 | ``` 448 | 449 | # 总结 450 | 451 | 1. Config如果配置了限速器,则Clientset中的所有RESTClient共享该限速器 452 | 2. Config如果没有配置限速器并且QPS大于0,则Clientset中的所有RESTClient共享一个限速器,该限速器的QPS等于Config.QPS 453 | 3. Config如果没有配置限速器并且QPS为0,则Clientset中的所有RESTClient都有一个独享的限速器,QPS为默认值5; 454 | 455 | 至于Config到底有没有配置限速器和QPS?这个就要看具体的应用(比如kubelet、kube-scheduler)了。 456 | -------------------------------------------------------------------------------- /client-go/rest/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 目录 8 | 9 | 1. [Client](./Client.md) 10 | 2. [Request](./Request.md) 11 | -------------------------------------------------------------------------------- /client-go/rest/Request.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 在阅读本文之前建议先阅读[Kubernetes RestClient解析](./Client.md)。如果把RestClient类比为http.Client,那么本文的主角Request则可以类比为http.Request。与RestClient直接继承http.Client功能不同,Request并没有继承http.Request的功能,而是随时可以根据Request构造http.Request对象。 10 | 11 | 还有一个与http.Request核心区别需要注意,那就是http.Request只是一个请求,它需要通过http.Client才能提交给服务端,即http.Client.Do(http.Request)。但是Kubernetes的Request修改了这个编程逻辑,赋予了Request提交请求给服务端的能力,即Request.Do()。不要小看这一点点的修改,这改变的是编程思想,意味着所有的Request都是活的,不需要显式通过RestClient就可以发送给服务端。这简化了使用者的开发,因为只需要一个Request对象就可以了。Request不仅可以用来提交请求,还实现了Watch()接口,即监视指定路径下的API对象,接下来笔者就来解析Request是如何实现这些吊炸天的功能。 12 | 13 | `本文引用源码为github.com/kubernetes/client-go的release-1.21分支。` 14 | 15 | # Request 16 | 17 | 先来看看Request的定义,源码链接: 18 | 19 | ```go 20 | type Request struct { 21 | // Request不需要显式依赖RESTClient的方法就是将RESTClient作为成员变量。 22 | c *RESTClient 23 | 24 | // warning等级异常处理器,指向了RESTClient.warningHandler,所有的Request共享使用 25 | warningHandler WarningHandler 26 | 27 | // 限速器,指向了RESTClient.rateLimiter,Request之间共享使用 28 | rateLimiter flowcontrol.RateLimiter 29 | // 退避管理器,Request独享使用,也就是说该Request退避去其他请求无关 30 | backoff BackoffManager 31 | // 请求超时时间 32 | timeout time.Duration 33 | // 请求最多尝试次数 34 | maxRetries int 35 | 36 | // HTTP请求的动词,比如POST、GET、DELETE、PATCH、PUT等 37 | verb string 38 | // 资源路径前缀,其实就是RESTClient.base/RESTClient.versionedAPIPath,详情参看Request构造函数 39 | pathPrefix string 40 | // 子路径,这是与子资源相关的,详情参看笔者关于API的解析文章。 41 | subpath string 42 | // HTTP请求的参数,就是请求URL后面的那些参数,例如https://xxxxx?a=b&c=d 43 | params url.Values 44 | // HTTP请求头,最终会赋值给http.Request.Header 45 | headers http.Header 46 | 47 | // 以下这些变量是Kubernetes API约定的一部分 48 | // 关于Kubernetes API约定参看:https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md 49 | // 请求需要操作API对象的命名空间 50 | namespace string 51 | // 命名空间是否已经设定的标记,直接判断len(namespaces) == 0不就完了?此处需要知道的是,空命名空间是Kubernetes保留使用的。 52 | // 所以有没有设置命名空间和命名空间是否为空不是一回事。 53 | namespaceSet bool 54 | // 请求操作API对象的种类,比如Pod 55 | resource string 56 | // 请求操作API对象的名字,比如Pod.Name 57 | resourceName string 58 | // 请求操作API对象的子资源,比如bind、proxy 59 | subresource string 60 | 61 | // 请求的输出变量,包括错误代码和HTTP响应body 62 | err error 63 | body io.Reader 64 | } 65 | ``` 66 | 67 | ## NewRequest 68 | 69 | 在[RESTClient解析](./Client.md)中实现Interface章节,笔者提到了核心就是Request的构造函数,现在就来看看Request构造函数的实现,源码链接: 70 | 71 | ```go 72 | // NewRequest()是Request的构造函数,需要传入RESTClient对象。 73 | func NewRequest(c *RESTClient) *Request { 74 | // 创建Request的退避管理器,关于退避管理器的功能已经在RESTClient中解释很多了,此处不再赘述了。 75 | // 只需要知道一点,每个Request都有一个独立的退避管理器。 76 | var backoff BackoffManager 77 | if c.createBackoffMgr != nil { 78 | backoff = c.createBackoffMgr() 79 | } 80 | // 如果RESTClient没有设置创建退避管理器的函数,则不使用退避策略。 81 | if backoff == nil { 82 | backoff = noBackoff 83 | } 84 | 85 | // 设置Request的URL路径前缀,就是RESTClient.base.Path/RESTClient.versionedAPIPath 86 | // 需要注意的是,一般RESTClient.base.Path是空的,而https://192.168.1.2:6443分散在RESTClient.base.Scheme和RESTClient.base.Host中。 87 | // 所以此处的pathPrefix与c.versionedAPIPath基本是相同的,例如/apis/apps/v1 88 | var pathPrefix string 89 | if c.base != nil { 90 | pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath) 91 | } else { 92 | pathPrefix = path.Join("/", c.versionedAPIPath) 93 | } 94 | 95 | // 复用http.Client的超时作为请求的超时 96 | var timeout time.Duration 97 | if c.Client != nil { 98 | timeout = c.Client.Timeout 99 | } 100 | 101 | // 创建Request对象 102 | r := &Request{ 103 | c: c, 104 | rateLimiter: c.rateLimiter, 105 | backoff: backoff, 106 | timeout: timeout, 107 | pathPrefix: pathPrefix, 108 | // 默认最多尝试次数是10次 109 | maxRetries: 10, 110 | warningHandler: c.warningHandler, 111 | } 112 | 113 | // 设置Accept请求头,优先使用AcceptContentTypes,如果没有设置则使用ContentType 114 | // 此处如果对AcceptContentTypes和ContentType不了解,请参看笔者解析RESTClient的文章。 115 | switch { 116 | case len(c.content.AcceptContentTypes) > 0: 117 | r.SetHeader("Accept", c.content.AcceptContentTypes) 118 | case len(c.content.ContentType) > 0: 119 | r.SetHeader("Accept", c.content.ContentType+", */*") 120 | } 121 | return r 122 | } 123 | ``` 124 | 125 | 现在应该理解为什么通过RESTClient创建Request了,因为RESTClient包含了大量Request之间共享的信息,比如限流器、退避管理器构造函数、路径前缀,更关键的是Request.c指向了RESTClient,这样Request就具备了向服务端提交请求的能力。 126 | 127 | ## Setter/Getter 128 | 129 | 在Request的构造函数中有很多成员变量没有初始化,比如verb、namespace、resource等,这些都是与具体的请求相关的了。Request提供了大量的Setter/Getter接口用来设置和获取这些成员变量,因为大部分接口过于简单,笔者不一一介绍,只挑选一些重点的接口函数进行解析。 130 | 131 | 首先来看看Request是如何设置body的,例如向apiserver提交创建Deployment的请求,就需要将Deployment对象放入到Request的body中。而Request.body是io.Reader类型,这里面就有编码的过程,源码链接: 132 | 133 | ```go 134 | // Body()用于设置Request.body成员变量,有没有发现该接口的返回值是自己,这种设计可以优雅的实现连续Setter接口的调用,例如: 135 | // request := RESTClient.Get().Namespace(ns).Resource("pods").SubResource("proxy").Name(net.JoinSchemeNamePort(scheme, name, port)).Suffix(path) 136 | // 而不是这样 137 | // request := RESTClient.Get() 138 | // request.Namespace(ns) 139 | // request.Resource("pods") 140 | // ... 141 | // 但是这种实现也有一个缺点,那就是无法返回错误,出错调用者感知不到,只能将错误记录在Request中,这就是Request.err存在的原因。 142 | // 还要Body()传入的参数是interface{},不是runtime.Object,这让Request的使用范围非常广。 143 | func (r *Request) Body(obj interface{}) *Request { 144 | // 在这之前调用某个接口(多半是Setter)的时候已经出错了,所以就没必要再继续执行,直接返回即可。 145 | if r.err != nil { 146 | return r 147 | } 148 | // 现在我们来看看interface{}到底比runtime.Object好在哪里! 149 | switch t := obj.(type) { 150 | // 字符串类型,必须是文件的路径,这一点需要注意,也就是说可以将某个文件的内容写入Request.body()。 151 | // 这种用法是不是很熟悉?是不是感觉kubectl create -f ...可以用这种方法实现? 152 | case string: 153 | data, err := ioutil.ReadFile(t) 154 | if err != nil { 155 | r.err = err 156 | return r 157 | } 158 | glogBody("Request Body", data) 159 | r.body = bytes.NewReader(data) 160 | // []byte类型,直接转为io.Reader还是比较容易的,适用于已经将对象序列化成[]byte的情况 161 | case []byte: 162 | glogBody("Request Body", t) 163 | r.body = bytes.NewReader(t) 164 | // io.Reader本尊,直接使用即可,适用于已经将对象转换为io.Reader的情况 165 | case io.Reader: 166 | r.body = t 167 | // 终于到Kubernetes的API对象了。 168 | case runtime.Object: 169 | // 需要校验obj是否为空,这个有点意思了,为什么不用if nil == obj呢?这就考验读者的语言基本功了。 170 | // 1. 如果obj为nil是不会进入这个分支的,说明obj != nil 171 | // 2. interface{}其实就是一个指针二元组(typeptr, objectptr),分别指向对象的类型和对象本身,所以obj不为空是正常的 172 | // 3. switch type语法无非是把interface{}类型转换为runtime.Object类型,t依然还是一个(typeptr, objectptr)的二元组,所以t也不是nil 173 | // 所以此处只能通过反射的方法获取对象指针来判断指向的对象是不是空地址,那么问题来了,为什么我们平时编程的时候不这么判断? 174 | // 那是因为我们平时编程很难造出这种类型的对象,大部分代码中都明确了对象的类型和指针,除非你这样写: 175 | // obj := (*appsv1.Deployment)(unsafe.Pointer(nil)),此时的obj就是一个*appsv1.Deployment类型但是实际指向了一个nil对象。 176 | // 有没有看到unsafe关键字,就是告诉使用者这是不安全的,如果没有足够的把握建议不要使用,但是在Kubernetes里面还是有人这么用的。 177 | if reflect.ValueOf(t).IsNil() { 178 | return r 179 | } 180 | // 根据配置的内容类型获得编码器,如果不理解可以把encoder想象为json.Marshal()函数。 181 | encoder, err := r.c.content.Negotiator.Encoder(r.c.content.ContentType, nil) 182 | if err != nil { 183 | r.err = err 184 | return r 185 | } 186 | // 编码对象,其实就是序列化对象,此处可以直接看做json.Marshal(obj),这样好理解一点 187 | data, err := runtime.Encode(encoder, t) 188 | if err != nil { 189 | r.err = err 190 | return r 191 | } 192 | // 将编码后的对象设置为body 193 | glogBody("Request Body", data) 194 | r.body = bytes.NewReader(data) 195 | // 设置Content-Type请求头,告知apiserver编码类型 196 | r.SetHeader("Content-Type", r.c.content.ContentType) 197 | // 其他类型的obj不知道怎么处理,所以报错 198 | default: 199 | r.err = fmt.Errorf("unknown type used for body: %+v", obj) 200 | } 201 | return r 202 | } 203 | ``` 204 | 205 | 有没有发现Request没有一个成员变量记录最终的URL?这是因为很多成员变量没有设置是无法知道最终的URL的。而构造http.Request需要传入最终的URL,这就是需要Request提供获取最终URL的接口,源码链接: 206 | 207 | ```go 208 | // URL()函数将生成Request的最终URL,例如https://192.168.1.2:6443/apis/apps/v1/namespaces/ns/deployments/name/subresource/subpath?params 209 | func (r *Request) URL() *url.URL { 210 | // URL路径以前缀作为起始,例如/apis/apps/v1 211 | p := r.pathPrefix 212 | // 如果Request设置了命名空间,则在路径上追加命名空间,需要注意的是增加了一个"namespaces",例如/apis/apps/v1/namespaces/ns 213 | if r.namespaceSet && len(r.namespace) > 0 { 214 | p = path.Join(p, "namespaces", r.namespace) 215 | } 216 | // 如果Request设置了资源种类(deployments),则追加资种类,此处需要注意的是将资源种类转换为小写。 217 | // 例如/apis/apps/v1/namespaces/ns/deployments/,此处需要注意资源(Resource)和种类(Kind)是不同的, 218 | // 所以是deployments而不是deployment,这部分会在Kubernetes API约定中有介绍。 219 | if len(r.resource) != 0 { 220 | p = path.Join(p, strings.ToLower(r.resource)) 221 | } 222 | // 如果Request设置了资源名或者子路径或者子资源,则追加到路径中,例如/apis/apps/v1/namespaces/ns/deployments/name/subresources/subpath. 223 | // 此处的资源名就是Deployment.Name 224 | if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 { 225 | p = path.Join(p, r.resourceName, r.subresource, r.subpath) 226 | } 227 | 228 | // 准备返回最终URL 229 | finalURL := &url.URL{} 230 | if r.c.base != nil { 231 | *finalURL = *r.c.base 232 | } 233 | finalURL.Path = p 234 | 235 | // 将Request的请求参数转换为url.Values类型,因为url.Values类型具有URL编码能力 236 | query := url.Values{} 237 | for key, values := range r.params { 238 | for _, value := range values { 239 | query.Add(key, value) 240 | } 241 | } 242 | 243 | // 特殊处理一下超时,超时是作为请求参数的一部分 244 | if r.timeout != 0 { 245 | query.Set("timeout", r.timeout.String()) 246 | } 247 | // 编码参数并赋值给最终URL 248 | finalURL.RawQuery = query.Encode() 249 | return finalURL 250 | } 251 | ``` 252 | -------------------------------------------------------------------------------- /client-go/tools/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 目录 8 | 9 | 1. [cache](./cache/README.md) -------------------------------------------------------------------------------- /client-go/tools/cache/Controller.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | Controller,中文翻译是控制器。本文的Controller是SharedIndexInformer的控制器,不是kube-controller-manager的控制器,所以千万不要搞混概念,虽然他们都叫控制器。 10 | 11 | 既然叫控制器,那么它控制什么呢?还记得这个经典的图片么? 12 | 13 | ![控制器图片](./controller.jpeg) 14 | 15 | Controller就是负责从Reflector接手Deltas并发送到DeltaFIFO的过程,看起来确实控制了Deltas的流动。此图中红色的controller是kube-controller,不是本文的Controller。 16 | 17 | 本文采用kubenete的release-1.20分支。 18 | 19 | # Controller定义 20 | 21 | 源码连接: 22 | 23 | ```go 24 | type Controller interface { 25 | // Run主要做两件事情:1)创建并运行Reflector,从Config.WatcherLister列举/监视并发送到Config.Queue,可能周期性的调用Queue.Resync() 26 | // 2)持续从Queue弹出对象增量并调用Config.ProcessFunc。这两个事情都是通过独立的协程运行直到stopC关闭 27 | // 关于Config后文有注释 28 | Run(stopCh <-chan struct{}) 29 | 30 | // 这个函数会给SharedIndexInformer使用,目的是判断当前是否已经同步完成了。同步的意思就是至少完成了一次全量列举 31 | HasSynced() bool 32 | 33 | // 获取上一次同步的版本,这个版本是列举全量对象的最大版本号 34 | LastSyncResourceVersion() string 35 | } 36 | ``` 37 | 38 | 单纯的从Controller的定义上看,确实起到了控制器的作用,至少它在驱动数据流动,包括全量列举、监视增量已经定时的在同步。 39 | 40 | # Controller实现 41 | 42 | ## Config 43 | 44 | Config是Controller的配置,在创建Controller时使用,以此来配置Controller。源码连接: 45 | 46 | ```go 47 | type Config struct { 48 | // 队列,当前实现为DeltaFIFO 49 | Queue 50 | 51 | // ListerWatcher用来创建Reflector 52 | ListerWatcher 53 | 54 | // 处理从Queue中弹出的增量(Deltas)的函数,ProcessFunc下面有注释 55 | Process ProcessFunc 56 | 57 | // 对象类型,比如v1.Pod,说明SharedIndexInformer是每个API类型都需要创建一个 58 | ObjectType runtime.Object 59 | 60 | // 在同步的周期,再同步不是同步apiserver,是Queue与Indexer、ResourceEventHandler之间的在同步 61 | FullResyncPeriod time.Duration 62 | 63 | // 是否需要再同步的函数,如果没有任何ResourceEventHandler设置resyncPeriod,则不需要在同步 64 | // ShouldResyncFunc下面有注释 65 | ShouldResync ShouldResyncFunc 66 | 67 | // 如果为true,当调用Process()返回错误时,重新放回Queue 68 | RetryOnError bool 69 | 70 | // 用来创建Reflector,每个Reflector.ListAndWatch()断开连接并出现错误时调用这个函数 71 | WatchErrorHandler WatchErrorHandler 72 | 73 | // 初始化监视列表和重新列举全量对象的请求块大小,说白了就是用来分页列举的,与SQL中的limit类似 74 | WatchListPageSize int64 75 | } 76 | 77 | // ShouldResyncFunc是一种函数类型,该类型的函数需要告知是否需要再同步,函数实际指向了sharedProcessor.shouldResync(). 78 | // 关于sharedProcessor.shouldResync()请参看SharedIndexInformer的文档 79 | type ShouldResyncFunc func() bool 80 | 81 | // ProcessFunc从定义上看是处理一个对象的函数类型,函数实际指向了sharedIndexInformer.HandleDeltas(),所以对象就是Deltas 82 | // 关于sharedIndexInformer.HandleDeltas()请参看SharedIndexInformer的文档 83 | type ProcessFunc func(obj interface{}) error 84 | ``` 85 | 86 | 从Config的成员变量可以看出,Controller需要ListerWatcher、Queue、ProcessFunc,这基本上算是SharedIndexInformer的数据流,从ListerWatcher->Queue->ProcessFunc,所以称之为控制器名副其实。 87 | 88 | ## controller 89 | 90 | controller是Controller的实现,这种定义方式已经是golang不成文的规则了。源码连接: 91 | 92 | ```go 93 | type controller struct { 94 | // Config不用多说了,上一个章节介绍过了 95 | config Config 96 | // Reflector请参看Reflector的文档 97 | reflector *Reflector 98 | // 后面两个应该没什么好解释的了 99 | reflectorMutex sync.RWMutex 100 | clock clock.Clock 101 | } 102 | ``` 103 | 104 | 从controller的定义上看,基本等于Config+Reflector,不能再多了。 105 | 106 | ### Run()接口实现 107 | 108 | Run()是controller非常核心的函数,因为controller的核心功能点都是在这里开始的,也就是说在controller的构造函数中没有做太多的工作,读者可以自行了解。因为controller是[SharedIndexInformer](./SharedIndexInformer.md)的核心,所以controller.Run()必然在SharedIndexInformer.Run()执行的。 109 | 110 | 源码连接: 111 | 112 | ```go 113 | func (c *controller) Run(stopCh <-chan struct{}) { 114 | defer utilruntime.HandleCrash() 115 | // 如果收到停止信号,需要把Queue关闭掉,此处有一个疑问:有必要卡一个协程等待信号然后关闭Queue么? 116 | // 直接在函数的结尾关闭不行么?毕竟函数需要等待停止型号并且Run()退出后才结束。 117 | // 其实在DeltaFIFO的文档中介绍过了,DeltaFIFO.Pop()通过sync.Cond阻塞协程, 118 | // 此时stopCh关闭也不会激活阻塞的协程,除非有新的对象或者关闭Queue 119 | // 所以常见一个协程关闭Queue是非常有必要的,否则就和controller.processLoop(后面章节会介绍)形成死锁了 120 | go func() { 121 | <-stopCh 122 | c.config.Queue.Close() 123 | }() 124 | // 创建Reflector,需要注意的是传入了ListerWatcher、Queue、FullResyncPeriod,在Reflector文章中提到了: 125 | // 1.通过ListerWatcher同步apiserver的对象; 126 | // 2.定期的调用Queue.Resync(); 127 | r := NewReflector( 128 | c.config.ListerWatcher, 129 | c.config.ObjectType, 130 | c.config.Queue, 131 | c.config.FullResyncPeriod, 132 | ) 133 | // 感情Config中的大部分参数都是给Reflector的... 134 | r.ShouldResync = c.config.ShouldResync 135 | r.WatchListPageSize = c.config.WatchListPageSize 136 | r.clock = c.clock 137 | if c.config.WatchErrorHandler != nil { 138 | r.watchErrorHandler = c.config.WatchErrorHandler 139 | } 140 | 141 | // 记录Reflector 142 | c.reflectorMutex.Lock() 143 | c.reflector = r 144 | c.reflectorMutex.Unlock() 145 | 146 | var wg wait.Group 147 | // 启动协程运行Reflector.Run() 148 | wg.StartWithChannel(stopCh, r.Run) 149 | // 启动协程运行processLoop()函数,虽然wait.Util()是每一秒执行一次processLoop(),但是processLoop()内部是一个死循环直到Queue关闭。 150 | // 初期的设计应该是遇到错误一秒后再重试,直到收到停止信号,现在来看只有Queue关闭的错误processLoop()才会退出。 151 | // 其他的错误当Config.RetryOnError为true时,会重新放入Queue,否则就丢弃,所以用wait.Until()当前的版本来看没有意义。难道Queue关闭了还会重新创建不成? 152 | wait.Until(c.processLoop, time.Second, stopCh) 153 | // 当前来看,只等待运行Reflector.Run()函数的协程退出 154 | wg.Wait() 155 | } 156 | ``` 157 | 158 | ### 其他接口实现 159 | 160 | 源码连接: 161 | 162 | ```go 163 | // HasSynced实现了Controller.HasSynced(),无非是对Queue.HasSynced()的在封装 164 | func (c *controller) HasSynced)() bool { 165 | return c.config.Queue.HasSynced() 166 | } 167 | 168 | // LastSyncResourceVersion无非是对Reflector.LastSyncResourceVersion()的再封装 169 | func (c *controller) LastSyncResourceVersion() string { 170 | c.reflectorMutex.RLock() 171 | defer c.reflectorMutex.RUnlock() 172 | if c.reflector == nil { 173 | return "" 174 | } 175 | return c.reflector.LastSyncResourceVersion() 176 | } 177 | ``` 178 | 179 | ### controller的核心处理函数processLoop() 180 | 181 | 源码连接: 182 | 183 | ```go 184 | // processLoop才是controller真正所事情的函数 185 | func (c *controller) processLoop() { 186 | for { 187 | // 从Queue中弹出对象并交给Config.ProcessFunc()处理 188 | obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) 189 | if err != nil { 190 | // 如果是队列关闭错误,直接退出,因为队列关闭可能收到了停止信号,所以需要退出 191 | // 如果没有收到停止信号关闭了Queue也没问题,processLoop是周期性调用的,1秒过后还会被调用 192 | if err == ErrFIFOClosed { 193 | return 194 | } 195 | // 如果配置了错误重试,那么就把对象重新放回队列 196 | if c.config.RetryOnError { 197 | c.config.Queue.AddIfNotPresent(obj) 198 | } 199 | } 200 | } 201 | } 202 | ``` 203 | 204 | 就这么简答,是不是有一种说自己是核心但是没做啥事的感觉。 205 | 206 | # 总结 207 | 208 | 1. Controller是SharedIndexInformer的控制器,是核心模块,控制API对象从WatcherLister->Queue->ProcessFunc; 209 | 2. 虽然Controller看似控制整个数据流程,但是WatcherLister->Queue是Reflector实现的,Controller只是负责创建Reflector并运行它; 210 | 3. Controller实际上只实现了Queue->ProcessFunc; 211 | 4. 遍地的Controller,都是控制器,重点要看控制啥的,别和kube-controller搞混了,命名需谨慎啊!我有个前同事,就是喜欢命名各种Manager,我也是服了... 212 | -------------------------------------------------------------------------------- /client-go/tools/cache/ListerWatcher.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | ListerWatcher是Lister和Watcher的结合体,前者负责列举全量对象,后者负责监视(本文将watch翻译为监视)对象的增量变化。为什么要有这个接口?原因很简单,提交访问效率。众所周知,kubernetes所有API对象都存储在etcd中,并只能通过apiserver访问。如果很多客户端频繁的列举全量对象(比如列举所有的Pod),这会造成apiserver不堪重负。那么如果在客户端做本地缓存如何?至少在没有任何状态变化的情况下只需要读取本地缓存即可,效率提升显而易见。通过列举全量对象完成本地缓存,而监视增量则是为了及时的将apiserver的状态变化更新到本地缓存。所以,在apiserver与客户端之间绝大部分传输的是对象的增量变化,当然在异常的情况下还是要重新列举一次全量对象。 10 | 11 | 本文值得客户端本地缓存就是[Indexer](./Indexer.md),client-go不仅实现了缓存,同时还加了索引,进一步提升了检索效率。 12 | 13 | 本文采用kubernetes的release-1.20分支。 14 | 15 | # ListerWatcher定义 16 | 17 | ## Lister定义 18 | 19 | 源码连接: 20 | 21 | ```go 22 | type Lister interface { 23 | // metav1.ListOptions和runtime.Object的定义在apimachinery目录,此处不做相关说明 24 | // 只需要知道列举要有选项,返回的是一个列表对象,runtime.Object既可以是单个API对象,也可以是API列表对象 25 | List(options metav1.ListOptions) (runtime.Object, error) 26 | } 27 | ``` 28 | 29 | ## Watcher定义 30 | 31 | 源码连接: 32 | 33 | ```go 34 | type Watcher interface { 35 | // 需要指定监视的起始版本,一般是列举全量对象的最大版本号+1 36 | Watch(options metav1.ListOptions) (watch.Interface, error) 37 | } 38 | ``` 39 | 40 | ## ListerWatcher定义 41 | 42 | 源码连接: 43 | 44 | ```go 45 | // 没什么好说的 46 | type ListerWatcher interface { 47 | Lister 48 | Watcher 49 | } 50 | ``` 51 | 52 | # ListerWatcher实现 53 | 54 | 源码连接: 55 | 56 | ```go 57 | // ListFunc是对ListerWatcher.List()接口的函数类型的定义 58 | type ListFunc func(options metav1.ListOptions) (runtime.Object, error) 59 | 60 | // WatchFunc是对ListerWatcher.Watch()接口的函数类型的定义 61 | type WatchFunc func(options metav1.ListOptions) (watch.Interface, error) 62 | 63 | // ListWatch实现了ListerWatcher,它方便了使用者实现ListerWatcher,因为只需要设置两个函数就可以了。 64 | // 比如传入闭包,后面ListerWatcher应用的案例就是使用的闭包,ListFunc和WatchFunc不能为空。 65 | type ListWatch struct { 66 | ListFunc ListFunc 67 | WatchFunc WatchFunc 68 | // 全量列举对象的时候禁止分页,但是没看到有设置为true的地方,等同于没用 69 | DisableChunking bool 70 | } 71 | // List实现了ListerWatcher.List() 72 | func (lw *ListWatch) List(options metav1.ListOptions) (runtime.Object, error) { 73 | return lw.ListFunc(options) 74 | } 75 | 76 | // Watch实现了ListerWatcher.Watch() 77 | func (lw *ListWatch) Watch(options metav1.ListOptions) (watch.Interface, error) { 78 | return lw.WatchFunc(options) 79 | } 80 | ``` 81 | 82 | # ListerWatcher应用 83 | 84 | ListerWatcher主要用于创建各种API对象的SharedIndexInformer,本文只截取Pod对象的代码,其他API对象都是一样的,只是API对象类型的差异。源码连接: 85 | 86 | ```go 87 | // NewFilteredPodInformer用来创建Pod类型的SharedIndexInformer,其中kubernetes.Interface用来实现ListerWatcher 88 | // kubernetes.Interface是啥?但是它的实现kubernetes.Clientset应该很熟悉了吧,所以ListerWatcher就是使用kubernetes.Clientset各个资源的List和Watch函数实现的 89 | // 这样Clientset和SharedIndexInformer之间的就联系起来了,至于函数的其他参数的说明请阅读相关的文档。 90 | func NewFilteredPodInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 91 | return cache.NewSharedIndexInformer( 92 | &cache.ListWatch{ 93 | // ListFunc就是Clientset的List函数 94 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 95 | if tweakListOptions != nil { 96 | tweakListOptions(&options) 97 | } 98 | return client.CoreV1().Pods(namespace).List(context.TODO(), options) 99 | }, 100 | // WatchFunc就是Clientset的Watch函数 101 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 102 | if tweakListOptions != nil { 103 | tweakListOptions(&options) 104 | } 105 | return client.CoreV1().Pods(namespace).Watch(context.TODO(), options) 106 | }, 107 | }, 108 | &corev1.Pod{}, 109 | resyncPeriod, 110 | indexers, 111 | ) 112 | } 113 | ``` 114 | 115 | # 总结 116 | 117 | 1. ListerWatcher就是为SharedIndexInformer结局全量对象、监视对象增量变化设计的接口,实现就是Clientset的List和Watch函数; 118 | 2. SharedIndexInformer利用ListerWatcher实现了本地缓存与apiserver之间的状态一致性; 119 | 3. 不仅可以提升客户端访问API对象的效率,同时可以将对象的增量变化回调给使用者; 120 | 4. 从原理上讲,可以用etcd的clientv3.Client实现ListerWatcher,SharedIndexInformer同步etcd的对象,这样一些简单的醒目就可以复用SharedIndexInformer了,毕竟不是所有的项目都需要一个apiserver; -------------------------------------------------------------------------------- /client-go/tools/cache/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 目录 8 | 9 | 1. [ListerWatcher](./ListerWatcher.md) 10 | 2. [Controller](./Controller.md) 11 | 3. [SharedIndexInformer](./SharedIndexInformer.md) 12 | -------------------------------------------------------------------------------- /client-go/tools/cache/SharedIndexInformer.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # sharedIndexInformer流程 8 | 9 | ![流程图](./SharedIndexInformerFlow.png) 10 | 11 | 流程图注释如下: 12 | 13 | * 圆角矩形为类型名,代表有该类型的成员变量。灰色T为interface,紫色T为struct 14 | * 矩形为函数,带有F标记,带有旋转标记为协程函数 15 | * 下划线为成员变量,该变量类型一般不在cache包内,带有F标记为函数型成员变量 16 | * 平行四边形为伪代码,伪代码,伪代码,重要的事情说三遍 17 | 18 | 执行流程: 19 | 20 | 1. NewSharedIndexInformer()创建SharedIndexInformer时传入参数defaultEventHandlerResyncPeriod设置sharedIndexInformer.resyncCheckPeriod; 21 | 2. sharedIndexInformer.Run()创建Controller对象,通过Config传入resyncCheckPeriod,DeltaFIFO,HandelDeltas,shouldResync等,创建协程运行sharedProcessor.run(),进入Controller.Run()函数; 22 | 3. Controller.Run()利用Config的FullResyncPeriod,Queue,ListerWatcher等创建Reflector,创建协程运行Reflector.Run()和Controller.processLoop(); 23 | 4. Reflector.Run()调用ListAndWatch(),利用ListerWatcher.List()将对象通过Replace加入Queue,利用ListerWatcher.Watch()通过Add/Update/Delete操作Queue。同时创建匿名协程函数根据Config.FullResyncPeriod定期执行Queue.Resync,前提时通过ShouldResync判断是否需要Resync; 24 | 5. Controller.processLoop()从Queue中弹出Deltas然后调用Config.Process,也就是sharedIndexInformer.HandleDeltas(); 25 | 6. sharedIndexInformer.HandleDeltas()根据Deltas同步操作Indexer并分发(distribute)到sharedProcessor; 26 | 7. sharedProcessor.distribute()会调用processorListener.add()将delta通知到所有的processorLister,processorListener最后负责通知到所有注册的ResourceEventHandler; 27 | -------------------------------------------------------------------------------- /client-go/tools/cache/SharedIndexInformerFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/client-go/tools/cache/SharedIndexInformerFlow.png -------------------------------------------------------------------------------- /client-go/tools/cache/controller.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/client-go/tools/cache/controller.jpeg -------------------------------------------------------------------------------- /controller/ControllerExpectations.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 本文涉及的部分名词参看[名词解释](./README.md),此处还要强调两个概念:Controller和Controllee,在本文其实二者是同一个意思,代表XxxController(比如DeploymentController)管理的Xxx(比如Deployment)对象。ControllerExpectations可以看做是XxxExpectations,只是各种Xxx的Expectations接口是相同的,就用ControllerExpectations统一实现了。 10 | 11 | 要想理解Kubernetes的Controller,需要对异步有足够的了解,否则XxxController的源码实现可能会让人看得非常懵逼。ControllerExpectations是XxxContrller中实现同步操作的Xxx的关键,它有点类似于sync.WaitGroup增加了非阻塞Wait()接口。以ReplicaSetController为例,根据ReplicaSet的声明创建N个Pod,此时用ControllerExpectations记录该ReplicaSet期望创建的Pod的数量为N(等同于sync.WaitGroup.Add(N))。ReplicaSetController监视(SharedIndexInformer的Pod的事件)到该ReplicaSet的一个Pod创建成功就会通知ControllerExpectations对期望创建的Pod数量减一(等同于sync.WaitGroup.Done())。直到达到期望创建的Pod数量归0(N个Pod全部创建完成),ReplicaSetController才会根据ReplicaSet的状态做下一步操作,直到达到Xxx声明的状态为止。那么,从ReplicaSetController从创建N个Pod开始直到确认N个Pod创建完成,整个过程其实存在很多异步操作(例如SharedIndexInformer的各种事件),因为有ControllerExpectations的存在,让整个操作跟同步操作一样,这也是很多XxxController都有一个函数叫做syncXxx()的原因,它等同于同步处理一个Xxx对象。类似用异步操作实现成同步操作的方法很多,比如用sync.Cond。 12 | 13 | 当然,XxxController并不是只创建Xxx子对象,还会删除Xxx的子对象,所以ControllerExpectations中有两个计数,分别用于创建和删除子对象的期望数量。 14 | 15 | 本文引用源码为kubernetes的release-1.20分支。 16 | 17 | # ControllerExpectations 18 | 19 | ## ControllerExpectationsInterface 20 | 21 | ControllerExpectationsInterface是ControllerExpectations抽象接口,这个抽象接口仅仅为了单元测试使用的(即便有一些XxxController已经使用了这个接口),所以读者简单了解一下就可以了。源码链接: 。 22 | 23 | ```go 24 | type ControllerExpectationsInterface interface { 25 | // 获取Controller的期望,ControlleeExpectations后文有介绍,此处的controllerKey就是Xxx的唯一键,即NS/Name 26 | GetExpectations(controllerKey string) (*ControlleeExpectations, bool, error) 27 | // 判断Controller的期望是否达成 28 | SatisfiedExpectations(controllerKey string) bool 29 | // 删除Controller的期望 30 | DeleteExpectations(controllerKey string) 31 | // 设置Controller的期望,其中add是创建子对象的数量,del是删除子对象的数量 32 | SetExpectations(controllerKey string, add, del int) error 33 | // Controller期望创建adds个子对象 34 | ExpectCreations(controllerKey string, adds int) error 35 | // Controller期望删除dels个子对象 36 | ExpectDeletions(controllerKey string, dels int) error 37 | // 观测到Controller的一个子对象创建完成,此时Controller期望创建的子对象数量减一 38 | CreationObserved(controllerKey string) 39 | // 观测到Controller的一个子对象删除完成,此时Controller期望删除的子对象数量减一 40 | DeletionObserved(controllerKey string) 41 | // 提升Controller的期望,add和del分别是创建和删除子对象的增量 42 | RaiseExpectations(controllerKey string, add, del int) 43 | // 降低Controller的期望,add和del分别是创建和删除子对象的增量 44 | LowerExpectations(controllerKey string, add, del int) 45 | } 46 | ``` 47 | 48 | ## ControlleeExpectations 49 | 50 | ControllerExpectations管理所有Xxx(比如ReplicaSet)对象的期望,ControlleeExpectations就是某一个Xxx对象的期望。ControllerExpectations可以简单理解为map\[string\]ControlleeExpectations结构,map的键是Xxx对象的唯一键(比如NS/Name)。源码链接: 。 51 | 52 | ```go 53 | type ControlleeExpectations struct { 54 | // 期望创建/删除子对象(比如Pod)的数量 55 | add int64 56 | del int64 57 | // Controller的唯一键 58 | key string 59 | // 创建时间 60 | timestamp time.Time 61 | } 62 | 63 | // 增加期望值,参数是创建和删除的增量值 64 | func (e *ControlleeExpectations) Add(add, del int64) { 65 | atomic.AddInt64(&e.add, add) 66 | atomic.AddInt64(&e.del, del) 67 | } 68 | 69 | // 期望是否达成 70 | func (e *ControlleeExpectations) Fulfilled() bool { 71 | // 如何才算是期望达成?就是期望创建和删除的子对象是否归零,如果二者全部归零说明没有任何期望了,也就是期望达成了 72 | return atomic.LoadInt64(&e.add) <= 0 && atomic.LoadInt64(&e.del) <= 0 73 | } 74 | 75 | // 获取期望值,需要返回创建和删除子对象的期望值 76 | func (e *ControlleeExpectations) GetExpectations() (int64, int64) { 77 | return atomic.LoadInt64(&e.add), atomic.LoadInt64(&e.del) 78 | } 79 | 80 | // 判断期望是否过期(超时),这是一个比较有用的接口,一旦有什么异常造成期望长期无法达成,Controller就会一直没有进展。 81 | // 利用超时机制可以解决此类异常(超时真是分布式系统中非常好用的方法),期望过期等于期望达成(无非可能是一个0值期望),XxxController会继续根据Xxx当前的状态进一步调整以达到Xxx声明的状态。 82 | // 这种异常很常见?笔者举个不恰当的例子,只是为了方便理解: 比如删除一个Pod,Pod僵死一直不退出,那么期望删除一个Pod就会一直无法达成,Xxx的状态就一直没有进展。 83 | func (exp *ControlleeExpectations) isExpired() bool { 84 | // 从创建期望到现在超过ExpectationsTimeout(5分钟)则认为过期 85 | return clock.RealClock{}.Since(exp.timestamp) > ExpectationsTimeout 86 | } 87 | ``` 88 | 89 | ## ControllerExpectations 90 | 91 | XxxController利用ControllerExpectations管理了所有Xxx对象的期望,源码链接: 。 92 | 93 | ```go 94 | // ControllerExpectations实现了ControllerExpectationsInterface。 95 | type ControllerExpectations struct { 96 | // cache包是client-go的cache,基本等同于map,不了解的读者可以阅读笔者关于client-go的Cache的文章 97 | cache.Store 98 | } 99 | 100 | // SatisfiedExpectations实现了ControllerExpectationsInterface.SatisfiedExpectations()接口 101 | func (r *ControllerExpectations) SatisfiedExpectations(controllerKey string) bool { 102 | // 获取Controller的期望 103 | if exp, exists, err := r.GetExpectations(controllerKey); exists { 104 | if exp.Fulfilled() { 105 | // 期望已达成 106 | klog.V(4).Infof("Controller expectations fulfilled %#v", exp) 107 | return true 108 | } else if exp.isExpired() { 109 | // 期望已过期,返回true,这样XxxController可以根据Xxx最新的状态进行调整 110 | klog.V(4).Infof("Controller expectations expired %#v", exp) 111 | return true 112 | } else { 113 | // 期望未达成且未过期,继续等待期望达成 114 | klog.V(4).Infof("Controller still waiting on expectations %#v", exp) 115 | return false 116 | } 117 | } else if err != nil { 118 | // 获取期望错误 119 | klog.V(2).Infof("Error encountered while checking expectations %#v, forcing sync", err) 120 | } else { 121 | // 期望不存在 122 | klog.V(4).Infof("Controller %v either never recorded expectations, or the ttl expired.", controllerKey) 123 | } 124 | // 获取期望错误或者期望不存在,对于Controller来说等同于期望达成,否则Controller将不会有任何进展。 125 | // 什么是期望达成?就是Xxx上一次的操作已经完成,告知XxxController可以对Xxx执行下一步操作了,关键在于可以开始下一步工作了。 126 | // 所以在必要(比如出现异常)的时候,也可以看做是期望达成,可能此时期望值是0。 127 | return true 128 | } 129 | 130 | // ControllerExpectations其他接口的实现读者自己看就行了,非常简单,都是利用ControlleeExpectations实现的。 131 | ``` 132 | 133 | # UIDTrackingControllerExpectations 134 | 135 | UIDTrackingControllerExpectations从名字上看继承了ControllerExpectations,同时还有跟踪UID的能力,此处的UID指导就是API对象的UID。所以期望已经不仅仅是数量了,还有具体是哪些对象,这可以得知期望是有状态的,因为他比ControllerExpectations多了指定的UID。换句话说,无论是创建还是删除子对象,必须是指定的那些对象,否则期望不会达成。所以UIDTrackingControllerExpectations就用在StatefulSetController中。。 136 | 137 | ```go 138 | type UIDTrackingControllerExpectations struct { 139 | // 继承了ControllerExpectationsInterface 140 | ControllerExpectationsInterface 141 | // 用cache.Store加锁管理跟踪的UID,需要注意的是跟踪的UID只用于期望删除的Pod,期望创建的Pod不需要跟踪UID。 142 | uidStoreLock sync.Mutex 143 | uidStore cache.Store 144 | } 145 | 146 | // 获取Controller跟踪的UID,是一个字符串集合 147 | func (u *UIDTrackingControllerExpectations) GetUIDs(controllerKey string) sets.String { 148 | // 函数没有上锁,所以调用此函数的地方需要加锁保护 149 | if uid, exists, err := u.uidStore.GetByKey(controllerKey); err == nil && exists { 150 | // UIDSet是一个结构体,包括string.Set(String)字段和一个key,读者可以自己看一下 151 | return uid.(*UIDSet).String 152 | } 153 | return nil 154 | } 155 | 156 | // ExpectDeletions覆盖了ControllerExpectationsInterface.ExpectDeletions(),参数从dels整数变成了字符串slice。 157 | // 这一点可以看出跟踪UID是删除子对象的UID。 158 | func (u *UIDTrackingControllerExpectations) ExpectDeletions(rcKey string, deletedKeys []string) error { 159 | // 字符串slice转set 160 | expectedUIDs := sets.NewString() 161 | for _, k := range deletedKeys { 162 | expectedUIDs.Insert(k) 163 | } 164 | klog.V(4).Infof("Controller %v waiting on deletions for: %+v", rcKey, deletedKeys) 165 | u.uidStoreLock.Lock() 166 | defer u.uidStoreLock.Unlock() 167 | 168 | // 如果已存在并跟踪了一些UID,就写错误日志,也就是说理论上不应该发生这种情况 169 | if existing := u.GetUIDs(rcKey); existing != nil && existing.Len() != 0 { 170 | klog.Errorf("Clobbering existing delete keys: %+v", existing) 171 | } 172 | // 记录新的UID集合 173 | if err := u.uidStore.Add(&UIDSet{expectedUIDs, rcKey}); err != nil { 174 | return err 175 | } 176 | // 设置期望删除子对象的数量 177 | return u.ControllerExpectationsInterface.ExpectDeletions(rcKey, expectedUIDs.Len()) 178 | } 179 | 180 | // DeletionObserved()覆盖了ControllerExpectationsInterface.DeletionObserved(),增加了已删除子对象的UID(deleteKey)。 181 | func (u *UIDTrackingControllerExpectations) DeletionObserved(rcKey, deleteKey string) { 182 | u.uidStoreLock.Lock() 183 | defer u.uidStoreLock.Unlock() 184 | 185 | // deleteKey必须是跟踪的UID,否则不会影响期望值 186 | uids := u.GetUIDs(rcKey) 187 | if uids != nil && uids.Has(deleteKey) { 188 | klog.V(4).Infof("Controller %v received delete for pod %v", rcKey, deleteKey) 189 | u.ControllerExpectationsInterface.DeletionObserved(rcKey) 190 | uids.Delete(deleteKey) 191 | } 192 | } 193 | 194 | // DeleteExpectations()覆盖了ControllerExpectationsInterface.DeleteExpectations()。 195 | func (u *UIDTrackingControllerExpectations) DeleteExpectations(rcKey string) { 196 | u.uidStoreLock.Lock() 197 | defer u.uidStoreLock.Unlock() 198 | 199 | // 删除Controller的期望,同时删除跟踪的UID(如果存在的话) 200 | u.ControllerExpectationsInterface.DeleteExpectations(rcKey) 201 | if uidExp, exists, err := u.uidStore.GetByKey(rcKey); err == nil && exists { 202 | if err := u.uidStore.Delete(uidExp); err != nil { 203 | klog.V(2).Infof("Error deleting uid expectations for controller %v: %v", rcKey, err) 204 | } 205 | } 206 | } 207 | ``` 208 | -------------------------------------------------------------------------------- /controller/ControllerRefManager.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | [名词解释](./README.md)中已经解释了什么是Controller,那么ControllerRef就是对Controller的引用。在单进程编程中引用一般等同于指针(指针是对象的唯一标识),但是分布式系统中引用一般是对象的唯一键,这个唯一键可能是多维度的(以K8S为例:NS、Name、Kind等)。ControllerRefManager从名词上理解就是管理对象的Controller引用,而本文的Controller指的是对象的父对象(Owner),所以ControllerRefManager用来管理对象的Onwer引用的。比如某个Pod的Owner是ReplicaSet,同时该ReplicaSet的Owner是一个Deployment,所以ControllerRefManager用来管理Pod的父对象ReplicaSet和ReplicaSet的父对象Deployment。 10 | 11 | 本文应用源码为kubernetes的release-1.21分支。 12 | 13 | # OwnerReference 14 | 15 | 现在我们来看看Kubernetes里对于Owner引用的定义,源码链接: 16 | 17 | ```go 18 | type OwnerReference struct { 19 | // 拥有者的API version,例如ReplicaSet对应的APIVersion可以是apps/v1 20 | APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"` 21 | // 拥有者的API Kind,复用上面的例子,Kind就是ReplicaSet 22 | Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` 23 | // 拥有者的名字,复用上面的例子就是ReplicaSet的名字。 24 | // 有没有发现没有NS?这是为什么?不是NS/Name才是唯一键么?道理很简单,父对象和子对象在同一个NS下,所以没有NS。 25 | Name string `json:"name" protobuf:"bytes,3,opt,name=name"` 26 | // 拥有者的UID,复用上面的例子就是ReplicaSet的UID,对象删除又创建会造成NS/Name相同但UID不同 27 | UID types.UID `json:"uid" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"` 28 | // 如果为true,表示拥有者是Controller类型,比如Deployment、ReplicaSet、Job等 29 | Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"` 30 | // 如果为true,并且拥有者通过foreground(例如kubectl delete xxx)删除时,只有该引用被删除时拥有者对象才能删除。 31 | BlockOwnerDeletion *bool `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"` 32 | } 33 | ``` 34 | 35 | # ControllerRefManager 36 | 37 | ## BaseControllerRefManager 38 | 39 | BaseControllerRefManager是XxxControllerRefManager的基类,此处Xxx就是需要设置拥有者的对象类型,比如Pod或者ReplicaSet(后文会有介绍)。源码链接: 。 40 | 41 | ```go 42 | type BaseControllerRefManager struct { 43 | // Controller(Owner)的meta 44 | Controller metav1.Object 45 | // Controller匹配子对象的Selector,很好理解,很为我们经常在yaml中写Label和Selector。 46 | Selector labels.Selector 47 | 48 | // 判断是否可以接纳子对象的函数,函数由外部传入,并且只会调用一次,后面介绍PodControllerRefManager的时候会详细说明,此处先忽略。 49 | // 什么是接纳?这要从没有ControllerRef的对象说起,这些对象对于Controller来说是‘孤儿’,只要是XxxController创建的子对象都会设置ControllerRef。 50 | // 如果通过子对象的标签匹配选择器,就可以考虑是否接纳这个子对象,即为这个子对象设置ControllerRef。当然,接纳孤儿没有那么简单,后面会详细说明。 51 | // 笔者将adopt翻译为‘接纳’,寓意为本身不属于Controller的子对象,经过各种匹配后才会被接纳,即获得子对象的拥有权。 52 | canAdoptErr error 53 | canAdoptOnce sync.Once 54 | CanAdoptFunc func() error 55 | } 56 | 57 | // CanAdopt()将canAdoptErr、canAdoptOnce、CanAdoptFunc组合成判断能否可以接纳对象的函数。 58 | // 为什么只调用一次,这个在具体使用的地方说明更加合适,此处只需要知道只调用一次即可。 59 | // 至少知道一点,能否能够接纳与具体的对象无关,因为函数不传入任何参数,所以可以推测应该与Controller的状态有关。 60 | func (m *BaseControllerRefManager) CanAdopt() error { 61 | m.canAdoptOnce.Do(func() { 62 | if m.CanAdoptFunc != nil { 63 | m.canAdoptErr = m.CanAdoptFunc() 64 | } 65 | }) 66 | return m.canAdoptErr 67 | } 68 | 69 | // ClaimObject()尝试获取对象(obj)的拥有权,match是匹配函数,adopt和release是接纳和释放obj拥有权的函数。 70 | // 1. 如果match()返回true,则接纳孤儿; 71 | // 2. 如果match()返回false,则释放对象的拥有权; 72 | // 那么问题来了,已经有选择器了,为什么还要传入匹配函数?匹配什么呢?这个笔者在调用该函数的地方再说明。 73 | func (m *BaseControllerRefManager) ClaimObject(obj metav1.Object, match func(metav1.Object) bool, adopt, release func(metav1.Object) error) (bool, error) { 74 | // 感兴趣的读者可以阅读GetControllerOfNoCopy()函数源码,笔者在此简单描述一下这个函数的功能: 75 | // 遍历obj.OwnerReferences,找到第一个*obj.OwnerReferences[i].Controller == true的引用。 76 | // 此处也可以得出一个结论:子对象只能有一个Controller类型的拥有者,否则不会发现第一个ControllerRef就返回。 77 | controllerRef := metav1.GetControllerOfNoCopy(obj) 78 | // 如果controllerRef不为空,说明obj已经有一个Controller的父对象 79 | if controllerRef != nil { 80 | // Owner是自己么? 81 | if controllerRef.UID != m.Controller.GetUID() { 82 | // 对象的拥有者不是自己,返回失败,毕竟对象已经有ControllerRef。 83 | return false, nil 84 | } 85 | // 匹配对象 86 | if match(obj) { 87 | // 已经拥有对象并且匹配成功,返回成功 88 | return true, nil 89 | } 90 | // 拥有对象但是匹配失败,如果Controller正在删除则不用释放,因为Controller删除会释放对象拥有权 91 | if m.Controller.GetDeletionTimestamp() != nil { 92 | return false, nil 93 | } 94 | // 拥有对象但是匹配失败,释放对象拥有权。前面提到孤儿的时候并没有解释孤儿怎么产生,此处就是产生孤儿的一种情况。 95 | // 即Controller与子对象不匹配,此时将释放子对象的拥有权。 96 | if err := release(obj); err != nil { 97 | // Pod不存在,等同于释放成功 98 | if errors.IsNotFound(err) { 99 | return false, nil 100 | } 101 | // 释放失败 102 | return false, err 103 | } 104 | // 释放成功 105 | return false, nil 106 | } 107 | 108 | // 因为obj没有ControllerRef,所以它是一个孤儿对象,如果Controller正在被删除亦或是匹配失败,则无法接纳它 109 | if m.Controller.GetDeletionTimestamp() != nil || !match(obj) { 110 | return false, nil 111 | } 112 | // 不能接纳一个正在删除的孤儿对象 113 | if obj.GetDeletionTimestamp() != nil { 114 | return false, nil 115 | } 116 | // 匹配成功,尝试接纳孤儿 117 | if err := adopt(obj); err != nil { 118 | // 对象不存在,接纳失败,这并不是错误,因为对象已经被删除了 119 | if errors.IsNotFound(err) { 120 | return false, nil 121 | } 122 | // 接纳错误 123 | return false, err 124 | } 125 | // 接纳成功 126 | return true, nil 127 | } 128 | 129 | ``` 130 | 131 | # PodControllerRefManager 132 | 133 | PodControllerRefManager用于管理Pod的ControllerRef,也就是说PodControllerRefManager.ClaimObject()函数传入的对象都是Pod,源码链接: 。 134 | 135 | ```go 136 | type PodControllerRefManager struct { 137 | // 继承了BaseControllerRefManager,因为BaseControllerRefManager用于管理通用对象的ControllerRef 138 | BaseControllerRefManager 139 | // ControllerRef的Group、Version、Kind三元组,比如apps、v1、ReplicaSet 140 | controllerKind schema.GroupVersionKind 141 | // PodControlInterface用于创建/删除/更新Pod,详情查看:https://github.com/jindezgm/k8s-src-analysis/blob/master/controller/PodControl.md 142 | podControl PodControlInterface 143 | } 144 | 145 | // NewPodControllerRefManager()是PodControllerRefManager的构造函数,由kube-controller-manager调用。 146 | func NewPodControllerRefManager( 147 | podControl PodControlInterface, 148 | controller metav1.Object, 149 | selector labels.Selector, 150 | controllerKind schema.GroupVersionKind, 151 | canAdopt func() error, 152 | ) *PodControllerRefManager { 153 | // PodControllerRefManager所有参数基本都是外部传入的... 154 | return &PodControllerRefManager{ 155 | BaseControllerRefManager: BaseControllerRefManager{ 156 | Controller: controller, 157 | Selector: selector, 158 | CanAdoptFunc: canAdopt, 159 | }, 160 | controllerKind: controllerKind, 161 | podControl: podControl, 162 | } 163 | } 164 | 165 | // ClaimPods()尝试获取一组Pod的拥有权,同时传入了一组过滤函数(想必是给BaseControllerRefManager.ClaimObject()的匹配函数使用的)。 166 | func (m *PodControllerRefManager) ClaimPods(pods []*v1.Pod, filters ...func(*v1.Pod) bool) ([]*v1.Pod, error) { 167 | var claimed []*v1.Pod 168 | var errlist []error 169 | 170 | // 终于看到匹配函数了,Pod的匹配函数是标签匹配同时通过所有过滤器,标签选择器好理解,过滤器到底过滤了啥就得看XxxController的具体实现了。 171 | match := func(obj metav1.Object) bool { 172 | pod := obj.(*v1.Pod) 173 | if !m.Selector.Matches(labels.Set(pod.Labels)) { 174 | return false 175 | } 176 | for _, filter := range filters { 177 | if !filter(pod) { 178 | return false 179 | } 180 | } 181 | return true 182 | } 183 | // 接纳Pod和释放Pod拥有权的函数,下面有这两个函数的注释 184 | adopt := func(obj metav1.Object) error { 185 | return m.AdoptPod(obj.(*v1.Pod)) 186 | } 187 | release := func(obj metav1.Object) error { 188 | return m.ReleasePod(obj.(*v1.Pod)) 189 | } 190 | 191 | // 遍历Pod,逐个获得/释放Pod拥有权 192 | for _, pod := range pods { 193 | // 尝试获取Pod的拥有权 194 | ok, err := m.ClaimObject(pod, match, adopt, release) 195 | if err != nil { 196 | errlist = append(errlist, err) 197 | continue 198 | } 199 | if ok { 200 | claimed = append(claimed, pod) 201 | } 202 | } 203 | // 返回获得拥有权的Pod以及没有获得拥有权的错误 204 | return claimed, utilerrors.NewAggregate(errlist) 205 | } 206 | 207 | // AdoptPod()发送Patch更新请求到apieserver来获得Pod的拥有权,将ControllerRef合并到Pod.OwnerReferences. 208 | func (m *PodControllerRefManager) AdoptPod(pod *v1.Pod) error { 209 | // 还记得CanAdopt()是一个只会真正调用一次的函数么?此处需要知道PodControllerRefManager.ClaimPods()传入的是多个Pod。 210 | // 也就是说这些Pod如果有多个Pod需要校验CanAdopt(),则第一个Pod决定了后面所有的Pod,即要么都能接纳,要么都不能接纳。 211 | // 这更加证明了能否接纳子对象看Controller的状态而不是子对象的状态。 212 | if err := m.CanAdopt(); err != nil { 213 | return fmt.Errorf("can't adopt Pod %v/%v (%v): %v", pod.Namespace, pod.Name, pod.UID, err) 214 | } 215 | 216 | // ownerRefControllerPatch()会生成ControllerRef的补丁,读者可以自己看一下,非常简单 217 | patchBytes, err := ownerRefControllerPatch(m.Controller, m.controllerKind, pod.UID) 218 | if err != nil { 219 | return err 220 | } 221 | // Patch更新Pod。 222 | return m.podControl.PatchPod(pod.Namespace, pod.Name, patchBytes) 223 | } 224 | 225 | // ReleasePod()发送Patch更新请求到apiserver来释放Pod拥有权,从Pod.OwnerReferences中通过UID删除ControllerRef。 226 | func (m *PodControllerRefManager) ReleasePod(pod *v1.Pod) error { 227 | klog.V(2).Infof("patching pod %s_%s to remove its controllerRef to %s/%s:%s", 228 | pod.Namespace, pod.Name, m.controllerKind.GroupVersion(), m.controllerKind.Kind, m.Controller.GetName()) 229 | // deleteOwnerRefStrategicMergePatch()会生成一个删除指定UID的OwnerReference的补丁。 230 | // 关于Patch更新的实现笔者会有单独的文章介绍,此处只需要知道Patch更新能够删除指定Pod(UID)的指定OwnerReferences(UID)即可。 231 | patchBytes, err := deleteOwnerRefStrategicMergePatch(pod.UID, m.Controller.GetUID()) 232 | if err != nil { 233 | return err 234 | } 235 | // Patch更新Pod 236 | err = m.podControl.PatchPod(pod.Namespace, pod.Name, patchBytes) 237 | if err != nil { 238 | if errors.IsNotFound(err) { 239 | // 如果Pod不存在,跟已经释放Pod拥有权是一样的 240 | return nil 241 | } 242 | if errors.IsInvalid(err) { 243 | // 返回Invalid错误有两种可能: 244 | // 1. Pod没有OwnerReferences,即为nil; 245 | // 2. 补丁包中PodUID不存在,即Pod被删除后又创建 246 | // 这两种可能造成的错误都可以忽略,因为Controller就没有Pod的拥有权,所以返回成功 247 | return nil 248 | } 249 | } 250 | return err 251 | } 252 | ``` 253 | 254 | ## ReplicaSetControllerRefManager 255 | 256 | 了解PodControllerRefManager,那么ReplicaSetControllerRefManager就非常容易了,它是用来管理ReplicaSet的ControllerRef的。那么问题来了,谁的的子对象会是ReplicaSet?答案是Deployment,所以此处得到一个结论:Deployment对Pod的控制是通过ReplicaSet实现的,那么Deployment控制什么?这个可以参看[DeploymentController](./DeploymentController.md)。 257 | 258 | 因为有了PodControllerRefManager的铺垫,笔者不会对ReplicaSetControllerRefManager做比较详细的注释,源码链接: 。 259 | 260 | ```go 261 | // ReplicaSetControllerRefManager基本上与PodControllerRefManager相同,只是资源类型的不同。 262 | type ReplicaSetControllerRefManager struct { 263 | BaseControllerRefManager 264 | controllerKind schema.GroupVersionKind 265 | rsControl RSControlInterface 266 | } 267 | 268 | // ReplicaSetControllerRefManager的构造函数,与PodControllerRefManager基本相同,不多解释 269 | func NewReplicaSetControllerRefManager( 270 | rsControl RSControlInterface, 271 | controller metav1.Object, 272 | selector labels.Selector, 273 | controllerKind schema.GroupVersionKind, 274 | canAdopt func() error, 275 | ) *ReplicaSetControllerRefManager { 276 | return &ReplicaSetControllerRefManager{ 277 | BaseControllerRefManager: BaseControllerRefManager{ 278 | Controller: controller, 279 | Selector: selector, 280 | CanAdoptFunc: canAdopt, 281 | }, 282 | controllerKind: controllerKind, 283 | rsControl: rsControl, 284 | } 285 | } 286 | 287 | // ClaimReplicaSets()尝试获取一组ReplicaSet的拥有权,比ClaimPods()少了过滤函数。 288 | func (m *ReplicaSetControllerRefManager) ClaimReplicaSets(sets []*apps.ReplicaSet) ([]*apps.ReplicaSet, error) { 289 | var claimed []*apps.ReplicaSet 290 | var errlist []error 291 | 292 | // 匹配函数就是用标签选择器实现的,笔者认为标签选择器实现的匹配函数应该实现在BaseControllerRefManager中,子类直接继承使用。 293 | match := func(obj metav1.Object) bool { 294 | return m.Selector.Matches(labels.Set(obj.GetLabels())) 295 | } 296 | // 接纳/释放ReplicaSet拥有权的函数。 297 | adopt := func(obj metav1.Object) error { 298 | return m.AdoptReplicaSet(obj.(*apps.ReplicaSet)) 299 | } 300 | release := func(obj metav1.Object) error { 301 | return m.ReleaseReplicaSet(obj.(*apps.ReplicaSet)) 302 | } 303 | 304 | // 一个接一个的获取ReplicaSet的拥有权 305 | for _, rs := range sets { 306 | ok, err := m.ClaimObject(rs, match, adopt, release) 307 | if err != nil { 308 | errlist = append(errlist, err) 309 | continue 310 | } 311 | if ok { 312 | claimed = append(claimed, rs) 313 | } 314 | } 315 | // 返回已经获取拥有权的ReplicaSet和获取拥有权失败的错误 316 | return claimed, utilerrors.NewAggregate(errlist) 317 | } 318 | 319 | // AdoptReplicaSet()和ReleaseReplicaSet()与前面的AdoptPod()和ReleasePod()基本一样,笔者不在重复注释了 320 | func (m *ReplicaSetControllerRefManager) AdoptReplicaSet(rs *apps.ReplicaSet) error { 321 | if err := m.CanAdopt(); err != nil { 322 | return fmt.Errorf("can't adopt ReplicaSet %v/%v (%v): %v", rs.Namespace, rs.Name, rs.UID, err) 323 | } 324 | patchBytes, err := ownerRefControllerPatch(m.Controller, m.controllerKind, rs.UID) 325 | if err != nil { 326 | return err 327 | } 328 | return m.rsControl.PatchReplicaSet(rs.Namespace, rs.Name, patchBytes) 329 | } 330 | 331 | func (m *ReplicaSetControllerRefManager) ReleaseReplicaSet(replicaSet *apps.ReplicaSet) error { 332 | klog.V(2).Infof("patching ReplicaSet %s_%s to remove its controllerRef to %s/%s:%s", 333 | replicaSet.Namespace, replicaSet.Name, m.controllerKind.GroupVersion(), m.controllerKind.Kind, m.Controller.GetName()) 334 | patchBytes, err := deleteOwnerRefStrategicMergePatch(replicaSet.UID, m.Controller.GetUID()) 335 | if err != nil { 336 | return err 337 | } 338 | err = m.rsControl.PatchReplicaSet(replicaSet.Namespace, replicaSet.Name, patchBytes) 339 | if err != nil { 340 | if errors.IsNotFound(err) { 341 | return nil 342 | } 343 | if errors.IsInvalid(err) { 344 | return nil 345 | } 346 | } 347 | return err 348 | } 349 | ``` 350 | 351 | ## ControllerRevisionControllerRefManager 352 | 353 | ControllerRevisionControllerRefManager笔者留给读者自己阅读,因为ReplicaSetControllerRefManager已经有大量的重复内容了。 354 | -------------------------------------------------------------------------------- /controller/NodeLifecycleController/NoExecuteTaintManger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/controller/NodeLifecycleController/NoExecuteTaintManger.jpg -------------------------------------------------------------------------------- /controller/PodControl.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 阅读本文前请参看[名词解释](./README.md)。 10 | 11 | PodControl是XxxController操作Pod的接口,比如ReplicaSetController创建、删除Pod。熟悉Clientset的读者肯定会质疑有必要再定义接口来操作Pod么?直接用Clientset提供的接口不就可以了么?答案肯定是有必要的,否则笔者就没必要介绍它了。因为很多Controller的子对象都是Pod,比如Job、Deamonset、ReplicaSet等等。这些Controller对于Pod都有类似的操作,比如设置Pod的拥有者(这一点不同于用kubectl create方式创建的Pod)。所以就有了PodControl接口提供给多个XxxController使用。 12 | 13 | 关于拥有者的介绍可以参看[ControllerRefManager](./ControllerRefManager.md)。 14 | 15 | 本文引用源码为kubernetes的release-1.20分支。 16 | 17 | # PodControl 18 | 19 | ## PodControlInterface 20 | 21 | PodControlInterface定义了操作Pod的接口,源码链接: 22 | 23 | ```go 24 | type PodControlInterface interface { 25 | // 根据Pod模板创建Pod,这是XxxController创建Pod的共性,因为很多Workload都有Pod模板。 26 | CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error 27 | // CreatePodsOnNode()与CreatePods()一样根据Pod模板创建Pod,同时还指定了Pod运行的Node以及拥有者。 28 | // 遗憾的是CreatePodsOnNode()与CreatePods()都没有被XxxController所使用。 29 | CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error 30 | // CreatePodsWithControllerRef()与CreatePods()一样根据Pod模板创建Pod,同时指定了拥有者。 31 | // 这是XxxController大量使用的接口,也证明了笔者前面的观点,即XxxController创建Pod是有共性的。 32 | CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error 33 | // 删除一个Pod,这个接口与Clientset的接口没什么不同,无非多了个object参数。 34 | // 这也是XxxController操作Pod的另一个共性,就是为Pod的父对象记录操作Pod的事件。 35 | DeletePod(namespace string, podID string, object runtime.Object) error 36 | // Patch更新Pod。 37 | PatchPod(namespace, name string, data []byte) error 38 | } 39 | ``` 40 | 41 | ## RealPodControl 42 | 43 | RealPodControl实现了(仅此一个实现)PodControlInterface接口,源码链接:。 44 | 45 | ```go 46 | type RealPodControl struct { 47 | // 利用Clientset操作Pod 48 | KubeClient clientset.Interface 49 | // 用于记录Pod父对象操作Pod的事件 50 | Recorder record.EventRecorder 51 | } 52 | ``` 53 | 54 | ### CreatePodXxx 55 | 56 | 虽然PodControlInterface定义了3个创建Pod的接口,但是笔者搜索发现只有CreatePodsWithControllerRef()被多大量使用,另外两个没有被使用。不排除早期版本中有使用,这并没有什么问题,因为这三个接口都是通过一个函数实现的。源码链接:。 57 | 58 | ```go 59 | // CreatePods()实现了PodControlInterface.CreatePods()接口。 60 | func (r RealPodControl) CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object) error { 61 | // createPods()下面有注释。 62 | return r.createPods("", namespace, template, object, nil) 63 | } 64 | 65 | // CreatePodsWithControllerRef()实现了PodControlInterface.CreatePodsWithControllerRef()接口。 66 | func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, controllerObject runtime.Object, controllerRef *metav1.OwnerReference) error { 67 | // 校验controllerRef的合法性 68 | if err := validateControllerRef(controllerRef); err != nil { 69 | return err 70 | } 71 | return r.createPods("", namespace, template, controllerObject, controllerRef) 72 | } 73 | 74 | // CreatePodsOnNode()实现了PodControlInterface.CreatePodsOnNode()接口。 75 | func (r RealPodControl) CreatePodsOnNode(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error { 76 | if err := validateControllerRef(controllerRef); err != nil { 77 | return err 78 | } 79 | return r.createPods(nodeName, namespace, template, object, controllerRef) 80 | } 81 | 82 | // createPods()是所有创建Pod接口的最终实现。 83 | func (r RealPodControl) createPods(nodeName, namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error { 84 | // 根据模板创建Pod的API对象,GetPodFromTemplate()下面有注释。 85 | pod, err := GetPodFromTemplate(template, object, controllerRef) 86 | if err != nil { 87 | return err 88 | } 89 | // 是否指定了Node,对应于CreatePodsOnNode()接口。 90 | if len(nodeName) != 0 { 91 | pod.Spec.NodeName = nodeName 92 | } 93 | // 不能创建没有标签的Pod,为什么?因为Controller需要根据Pod标签匹配,如果是Controller创建的Pod必须都有标签。 94 | if len(labels.Set(pod.Labels)) == 0 { 95 | return fmt.Errorf("unable to create pods, no labels") 96 | } 97 | // 通过Clientset创建Pod,有没有发现PodControl创建Pod的接口都没有metav1.CreateOptions? 98 | // 这也算是PodControl存在的另一个价值,省去了没用的参数。 99 | newPod, err := r.KubeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod, metav1.CreateOptions{}) 100 | if err != nil { 101 | if !apierrors.HasStatusCause(err, v1.NamespaceTerminatingCause) { 102 | // 记录为object创建Pod失败事件 103 | r.Recorder.Eventf(object, v1.EventTypeWarning, FailedCreatePodReason, "Error creating: %v", err) 104 | } 105 | return err 106 | } 107 | // 获取object对象的meta来打印日志 108 | accessor, err := meta.Accessor(object) 109 | if err != nil { 110 | klog.Errorf("parentObject does not have ObjectMeta, %v", err) 111 | return nil 112 | } 113 | // 记录为object创建Pod成功事件。 114 | klog.V(4).Infof("Controller %v created pod %v", accessor.GetName(), newPod.Name) 115 | r.Recorder.Eventf(object, v1.EventTypeNormal, SuccessfulCreatePodReason, "Created pod: %v", newPod.Name) 116 | 117 | return nil 118 | } 119 | 120 | // 根据Pod模板创建Pod的API对象。 121 | func GetPodFromTemplate(template *v1.PodTemplateSpec, parentObject runtime.Object, controllerRef *metav1.OwnerReference) (*v1.Pod, error) { 122 | // 从模板中提取Labels、Finalizers和Annotations 123 | desiredLabels := getPodsLabelSet(template) 124 | desiredFinalizers := getPodsFinalizers(template) 125 | desiredAnnotations := getPodsAnnotationSet(template) 126 | accessor, err := meta.Accessor(parentObject) 127 | // 获取Pod父对象的meta 128 | if err != nil { 129 | return nil, fmt.Errorf("parentObject does not have ObjectMeta, %v", err) 130 | } 131 | // 利用父对象的名字+'-'作为Pod的前缀,这也是为什么我们看到Deployment创建出来的Pod都是xxx-yyy-zzz格式。 132 | // 其中xxx是Deployment名字,yyy是ReplicaSet名字,zzz是Pod名字,也就是Deployment创建了ReplicatSet,ReplicatSet创建了Pod 133 | prefix := getPodsPrefix(accessor.GetName()) 134 | 135 | // 常见Pod的API对象 136 | pod := &v1.Pod{ 137 | ObjectMeta: metav1.ObjectMeta{ 138 | Labels: desiredLabels, 139 | Annotations: desiredAnnotations, 140 | GenerateName: prefix, 141 | Finalizers: desiredFinalizers, 142 | }, 143 | } 144 | // 如果指定了Pod的Controller引用,将其追加到Pod的拥有者引用中 145 | if controllerRef != nil { 146 | pod.OwnerReferences = append(pod.OwnerReferences, *controllerRef) 147 | } 148 | // 从Pod模板深度拷贝PodSpec。 149 | pod.Spec = *template.Spec.DeepCopy() 150 | return pod, nil 151 | } 152 | ``` 153 | 154 | ### DeletePod 155 | 156 | 这个接口也没什么好解释的,直接上代码吧,源码链接:。 157 | 158 | ```go 159 | // DeletePod()实现了PodControlInterface.DeletePod()接口。 160 | func (r RealPodControl) DeletePod(namespace string, podID string, object runtime.Object) error { 161 | 获取Pod父对象的meta,单纯为了打印日志,这应该是也是XxxController操作Pod的共性吧。 162 | accessor, err := meta.Accessor(object) 163 | if err != nil { 164 | return fmt.Errorf("object does not have ObjectMeta, %v", err) 165 | } 166 | klog.V(2).InfoS("Deleting pod", "controller", accessor.GetName(), "pod", klog.KRef(namespace, podID)) 167 | // 通过Clientset删除Pod 168 | if err := r.KubeClient.CoreV1().Pods(namespace).Delete(context.TODO(), podID, metav1.DeleteOptions{}); err != nil { 169 | if apierrors.IsNotFound(err) { 170 | klog.V(4).Infof("pod %v/%v has already been deleted.", namespace, podID) 171 | return err 172 | } 173 | // 为object记录删除Pod失败的事件 174 | r.Recorder.Eventf(object, v1.EventTypeWarning, FailedDeletePodReason, "Error deleting: %v", err) 175 | return fmt.Errorf("unable to delete pods: %v", err) 176 | } 177 | // 为object记录删除Pod成功的事件 178 | r.Recorder.Eventf(object, v1.EventTypeNormal, SuccessfulDeletePodReason, "Deleted pod: %v", podID) 179 | 180 | return nil 181 | } 182 | ``` 183 | 184 | ### PatchPod 185 | 186 | Patch更新接口比Clientset的接口要简洁一些,少了一些看不懂的参数,这也是PodControl存在的价值,。 187 | 188 | ```go 189 | // PatchPod()实现了PodControlInterface.PatchPod()接口。 190 | func (r RealPodControl) PatchPod(namespace, name string, data []byte) error { 191 | // PatchPod()接口实现比较简单,直接用Clientset接口实现Patch更新。但是要求所有的XxxController都使用策略性合并(types.StrategicMergePatchType). 192 | // 为什么不为Patch更新Pod记录事件?这一点笔者也在思考。 193 | // 关于Patch类型的说明请参看:https://kubernetes.io/zh/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ 194 | _, err := r.KubeClient.CoreV1().Pods(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, data, metav1.PatchOptions{}) 195 | return err 196 | } 197 | ``` 198 | 199 | # 总结 200 | 201 | 1. PodControl是Clientset操作Pod接口的再封装,但是在接口上去掉了一些对于XxxController'无用'的参数,比如metav1.PatchOptions、metav1.DeleteOptions、metav1.CreateOptions等,同时增加了XxxController需要的参数,比如父对象、ControllerRef等; 202 | 2. PodControl能够为Pod的父对象记录操作Pod的事件,避免每个Controller都实现类似的代码; 203 | -------------------------------------------------------------------------------- /controller/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 名词解释 8 | 9 | 1. XxxController: 泛指各种Workload控制器,比如DeploymentController、DeamonSetController、StatefulSetController等等; 10 | 2. Xxx: 泛指各种Workload,比如Deployment、DeamonSet、StatefulSet等等; 11 | 3. Owner: Kubernetes的API对象的一个meta属相,即对象的拥有者,也称之为父对象,该API对象也称为Owner的子对象; 12 | 4. Controller: XxxController的功能是让系统达到Xxx声明的状态,XxxController负责执行创建、删除子对象操作;换个角度看,Xxx才是子对象的控制器,无非由XxxController代为执行而已(用C/C++视角,XxxController就是Xxx的线程池+静态成员),所以在很多代码中Controller指的就是Xxx,而不是XxxController,泛指Xxx是其子对象的控制器; 13 | 14 | # 目录 15 | 16 | 1. [PodControl](./PodControl.md) 17 | 2. [ControllerExpectations](./ControllerExpectations.md) 18 | 3. [ControllerRefManager](./ControllerRefManager.md) 19 | 4. [JobController](./JobController.md) 20 | -------------------------------------------------------------------------------- /kube-scheduler/Configurator.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | [调度器配置](./KubeSchedulerConfiguration.md)中解析了kube-scheduler的各种配置,但是自始至终都没有看到如何根据配置构造[调度器](./Scheduler.md)。虽然[调度器](./Scheduler.md)文章中提到了构造函数,但是其核心实现是Configurator,本文将解析Configurator构造调度器的过程。虽然意义远没有[调度队列](./SchedulingQueue.md)、[调度框架](./Framework.md)、[调度插件](./Plugin.md)等重大,但是对于了解[调度器](./Scheduler.md)从生到死整个过程来说是必要一环,况且确实能学到点东西。 10 | 11 | # Configurator 12 | 13 | ## Configurator定义 14 | 15 | Configurator类似于[Scheduler](./Scheduler.md)的工厂,但是一般的工厂类很少会有这么多的配置,所以Configurator这个名字比Factroy更合适。源码链接: 16 | 17 | ```go 18 | // Configurator根据配置构造调度器 19 | type Configurator struct { 20 | // 因为kube-scheduler需要访问apiserver,所以clientset.Interface是必须的。 21 | client clientset.Interface 22 | 23 | recorderFactory profile.RecorderFactory 24 | 25 | // informerFactory和client都是用来访问apiserver,informerFactory用来读API对象,client用于写API对象(比如绑定) 26 | informerFactory informers.SharedInformerFactory 27 | 28 | // 这个关闭信号,用chan实现是非常普遍的做法 29 | StopEverything <-chan struct{} 30 | 31 | // 调度缓存,调度器必须的模块,这可以证明调度缓存不是Scheduler构造的,而是通过Configurator传给Scheduler 32 | schedulerCache internalcache.Cache 33 | 34 | // 是否运行所有的FilterPlugin,即便中间某个插件返回失败。 35 | // 了解调度插件的读者都知道,有任何过滤插件返回失败,表示Pod不可调度,所有的过滤插件是与的关系。 36 | // 所以排序靠前的过滤插件返回失败理论上是无需再用后面的插件过滤了,这个配置就是是否运行所有的过滤插件。 37 | alwaysCheckAllPredicates bool 38 | 39 | // 这几个配置参数已经在调度器配置、调度框架、调度队列等文章中详细解释过了,Configurator只需要透明传递就好了 40 | percentageOfNodesToScore int32 41 | podInitialBackoffSeconds int64 42 | podMaxBackoffSeconds int64 43 | 44 | // 每个KubeSchedulerProfile对应一个调度框架(Framework)的配置,所以Configurator需要根据配置构造Framework 45 | profiles []schedulerapi.KubeSchedulerProfile 46 | // 调度插件工厂注册表,即根据调度插件名字与调度插件工厂的映射。 47 | // 因为Configurator根据配置为Framework创建插件,所以需要根据插件名字获取调度插件工厂来构造插件 48 | registry frameworkruntime.Registry 49 | // 调度缓存快照,熟悉调度缓存的读者应该知道,Scheduler每次调度之前都会更新调度缓存快照,调度缓存只需要将更新与快照diff的部分即可。 50 | // 所以调度缓存快照是和Scheduler一起创建,生命周期与调度器是相同的,由Configurator创建再传递给Scheduler。 51 | nodeInfoSnapshot *internalcache.Snapshot 52 | // 调度扩展程序配置,Configurator需要根据配置构造调度扩展程序并传递给Scheduler 53 | extenders []schedulerapi.Extender 54 | // frameworkCapturer是回调函数,用来捕获每最终的KubeSchedulerProfile,目的是什么? 55 | // 因为输入的KubeSchedulerProfile里包含有使能和禁止的插件配置,而Configurator构造Scheduler时会合并出最终的KubeSchedulerProfile。 56 | // 如果需要获取最终的KubeSchedulerProfile怎么办?就可以通过FrameworkCapturer捕获。 57 | frameworkCapturer FrameworkCapturer 58 | // 用来配置最大并行度,这个参数再调度器配置中有详细说明,说的简单点就是配置最大协程的数量。 59 | parallellism int32 60 | } 61 | ``` 62 | 63 | ## create 64 | 65 | 直接进入Configurator最核心的函数,就是更具配置构造Scheduler,源码链接: 66 | 67 | ```go 68 | // create()创建Scheduler对象 69 | func (c *Configurator) create() (*Scheduler, error) { 70 | var extenders []framework.Extender 71 | var ignoredExtendedResources []string 72 | // 是否配置了Extender? 73 | if len(c.extenders) != 0 { 74 | var ignorableExtenders []framework.Extender 75 | // 遍历所有的Extender配置 76 | for ii := range c.extenders { 77 | // 根据配置构造Extender(当前只有HTTPExtender实现) 78 | klog.V(2).InfoS("Creating extender", "extender", c.extenders[ii]) 79 | extender, err := core.NewHTTPExtender(&c.extenders[ii]) 80 | if err != nil { 81 | return nil, err 82 | } 83 | // 根据Extender'是否可以被忽略'将Extender分为两个集合:extenders和ignorableExtenders 84 | if !extender.IsIgnorable() { 85 | extenders = append(extenders, extender) 86 | } else { 87 | ignorableExtenders = append(ignorableExtenders, extender) 88 | } 89 | // 遍历Extender管理的资源,统计Scheduler可以忽略的资源 90 | for _, r := range c.extenders[ii].ManagedResources { 91 | if r.IgnoredByScheduler { 92 | ignoredExtendedResources = append(ignoredExtendedResources, r.Name) 93 | } 94 | } 95 | } 96 | // 最后将ignorableExtenders追加到extenders尾部,这样做的目的笔者在解析Extender的文章中有说明。 97 | // 此处简单描述一下:在遍历Extender调度Pod时,因为二者存在明确的分界线,当调用错误时可以根据当前Extender是否可忽略选择返回错误(否)或者成功(是) 98 | extenders = append(extenders, ignorableExtenders...) 99 | } 100 | 101 | // 如果Extender中有任何资源需要Scheduler忽略掉,需要通过追加到每个Profile的PluginConfig一个忽略种资源的参数。 102 | // 这仅对v1beta1有效,可以在配置Extender和插件参数,对于较早的版本,不允许同时使用策略(Policy)和自定义插件配置。 103 | // 这非常有意思,没想到还可以这么玩儿,这样在接下来构造调度插件的时候就可以把忽略的资源通过参数告知调度插件。 104 | // 笔者在解析调度插件的文章中提到了noderesources.Fit这个插件,专门负责资源匹配的插件,所以忽略哪些资源应该告知这个插件。 105 | if len(ignoredExtendedResources) > 0 { 106 | for i := range c.profiles { 107 | prof := &c.profiles[i] 108 | pc := schedulerapi.PluginConfig{ 109 | Name: noderesources.FitName, 110 | Args: &schedulerapi.NodeResourcesFitArgs{ 111 | IgnoredResources: ignoredExtendedResources, 112 | }, 113 | } 114 | prof.PluginConfig = append(prof.PluginConfig, pc) 115 | } 116 | } 117 | 118 | // 构造PodNominator,所有的调度框架(Framework)共享使用。 119 | // 不可能每个Framework独享一个PodNominator,这会导致Pod1提名了Node1而在调度Pod2时全然不知,因为Pod1和Pod2可能使用不同的调度框架。 120 | nominator := internalqueue.NewPodNominator() 121 | // ClusterEvent抽象了系统资源状态是如何更改的,比如Pod的Added,感兴趣的读者可以看看ClusterEvent的定义。 122 | // clusterEventMap是系统资源状态的更改到插件名字集合的映射,说白了就是有哪些插件更改了资源,至于有什么用读者自己研究吧~ 123 | clusterEventMap := make(map[framework.ClusterEvent]sets.String) 124 | // 根据[]KubeSchedulerProfile构造map[string]Framework(map查找更快),传入了调度框架和调度插件需要的参数。 125 | // 如何根据KubeSchedulerProfile构造Framework,读者可以自己看一下,相对比较简单,笔者此处不再注释了。 126 | profiles, err := profile.NewMap(c.profiles, c.registry, c.recorderFactory, 127 | frameworkruntime.WithClientSet(c.client), 128 | frameworkruntime.WithInformerFactory(c.informerFactory), 129 | frameworkruntime.WithSnapshotSharedLister(c.nodeInfoSnapshot), 130 | frameworkruntime.WithRunAllFilters(c.alwaysCheckAllPredicates), 131 | frameworkruntime.WithPodNominator(nominator), 132 | frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(c.frameworkCapturer)), 133 | frameworkruntime.WithClusterEventMap(clusterEventMap), 134 | frameworkruntime.WithParallelism(int(c.parallellism)), 135 | ) 136 | if err != nil { 137 | return nil, fmt.Errorf("initializing profiles: %v", err) 138 | } 139 | if len(profiles) == 0 { 140 | return nil, errors.New("at least one profile is required") 141 | } 142 | // 此处需要注意了,使用第一个Profile的QueueSortPlugin的排序函数构造调度队列。 143 | // 我们知道调度队列只有一个,Shecuduler不会为每个调度框架创建一个调度队列,这就强行要求所有Profile必须配置同一个QueueSortPlugin。 144 | // 这一点笔者在解析调度器配置的文章中已经提到了,但是并没有从代码层面给出证明,此处算是证明了这一点。 145 | lessFn := profiles[c.profiles[0].SchedulerName].QueueSortFunc() 146 | podQueue := internalqueue.NewSchedulingQueue( 147 | lessFn, 148 | c.informerFactory, 149 | internalqueue.WithPodInitialBackoffDuration(time.Duration(c.podInitialBackoffSeconds)*time.Second), 150 | internalqueue.WithPodMaxBackoffDuration(time.Duration(c.podMaxBackoffSeconds)*time.Second), 151 | internalqueue.WithPodNominator(nominator), 152 | internalqueue.WithClusterEventMap(clusterEventMap), 153 | ) 154 | 155 | // 与核心内容关系不大,本文忽略。 156 | debugger := cachedebugger.New( 157 | c.informerFactory.Core().V1().Nodes().Lister(), 158 | c.informerFactory.Core().V1().Pods().Lister(), 159 | c.schedulerCache, 160 | podQueue, 161 | ) 162 | debugger.ListenForSignal(c.StopEverything) 163 | 164 | // 构造调度算法,笔者在解析调度算法的文章中提到了,调度算法的唯一实现就是genericScheduler 165 | algo := core.NewGenericScheduler( 166 | c.schedulerCache, 167 | c.nodeInfoSnapshot, 168 | extenders, 169 | c.percentageOfNodesToScore, 170 | ) 171 | 172 | // 最终返回Scheduler对象 173 | return &Scheduler{ 174 | SchedulerCache: c.schedulerCache, 175 | Algorithm: algo, 176 | Profiles: profiles, 177 | // 注入获取下一个待调度Pod的函数 178 | NextPod: internalqueue.MakeNextPodFunc(podQueue), 179 | // 注入调度Pod错误的处理函数 180 | Error: MakeDefaultErrorFunc(c.client, c.informerFactory.Core().V1().Pods().Lister(), podQueue, c.schedulerCache), 181 | StopEverything: c.StopEverything, 182 | SchedulingQueue: podQueue, 183 | }, nil 184 | } 185 | ``` 186 | 187 | 虽然create()函数可以构造Scheduler,但是有没有发现缺少默认的插件配置?也就是说,create()直接利用Profile构造Framework,那么用户配置的Profile是怎么与默认的合并的呢?接下来的章节解析Configurator是如何生成默认配置并合并自定义配置。 188 | 189 | ## createFromProvider 190 | 191 | kube-scheduler通过算法源获取插件的默认配置,算法源分为Provider和Policy两种,本章节介绍Provider,源码链接: 192 | 193 | ```go 194 | // createFromProvider()根据名字找到已注册的算法提供者以此构造Scheduler。 195 | func (c *Configurator) createFromProvider(providerName string) (*Scheduler, error) { 196 | klog.V(2).InfoS("Creating scheduler from algorithm provider", "algorithmProvider", providerName) 197 | // NewRegistry()返回了默认的插件配置(笔者在在解析调度插件的文章中介绍了默认插件配置)。 198 | // 感兴趣的读者可以了解一下这个函数,代码量不多,如果不了解的读者也没关系,就简单认为只有一种默认插件配置就可以了。 199 | r := algorithmprovider.NewRegistry() 200 | defaultPlugins, exist := r[providerName] 201 | if !exist { 202 | return nil, fmt.Errorf("algorithm provider %q is not registered", providerName) 203 | } 204 | 205 | // 遍历所有的Profile. 206 | for i := range c.profiles { 207 | prof := &c.profiles[i] 208 | plugins := &schedulerapi.Plugins{} 209 | // 在默认插件配置基础上应用用户的自定义配置,即使能新的插件或者禁止某些默认插件,相关函数读者自己查看即可。 210 | plugins.Append(defaultPlugins) 211 | plugins.Apply(prof.Plugins) 212 | // 将最终的插件配置更新到Profile中用于创建Framework,这个在前面已经提到了。 213 | prof.Plugins = plugins 214 | } 215 | // 创建Scheduler 216 | return c.create() 217 | } 218 | ``` 219 | 220 | createFromProvider()就是利用默认的插件配置再合并用户自定义插件配置形成最终的插件配置,以此来创建Scheduler。有没有发现该方法是静态配置的,也就是已注册的Provider都是写在代码里的。在没有KubeSchedulerProfile这一功能之前,如果需要多种不同的Provider只能在代码里注册多个Provider,这明显是非常不友好的设计,并且无法为插件配置自定义参数,所以就有了基于策略(Policy)的插件配置。 221 | 222 | ## createFromConfig 223 | 224 | 基于策略(Policy)的插件配置可以通过配置文件、ConfigMap配置插件,这在KubeSchedulerProfile出来之间很好用,无奈KubeSchedulerProfile后来一同江湖了。源码链接: 225 | 226 | ```go 227 | // 从配置文件创建Scheduler,仅在v1alpha1可使用。新版本已经不推荐使用算法院(包括Provider和Policy),改用KubeSchedulerProfile实现。 228 | // 所以笔者不会对基于Policy构建Scheduler做详细解析,因为相对比较复杂且意义不大,只是简单介绍一下,感兴趣的读者可以自己详细看看。 229 | func (c *Configurator) createFromConfig(policy schedulerapi.Policy) (*Scheduler, error) { 230 | // NewLegacyRegistry()创建基于策略的默认插件注册表,这一点与algorithmprovider.NewRegistry()功能类似。 231 | lr := frameworkplugins.NewLegacyRegistry() 232 | args := &frameworkplugins.ConfigProducerArgs{} 233 | 234 | klog.V(2).InfoS("Creating scheduler from configuration", "policy", policy) 235 | 236 | // 验证策略配置的有效性 237 | if err := validation.ValidatePolicy(policy); err != nil { 238 | return nil, err 239 | } 240 | 241 | // 根据策略配置找到所有已注册的Predicate的插件名字,Predicate在对应于当前的Filter 242 | predicateKeys := sets.NewString() 243 | if policy.Predicates == nil { 244 | klog.V(2).InfoS("Using predicates from algorithm provider", "algorithmProvider", schedulerapi.SchedulerDefaultProviderName) 245 | predicateKeys = lr.DefaultPredicates 246 | } else { 247 | for _, predicate := range policy.Predicates { 248 | klog.V(2).InfoS("Registering predicate", "predicate", predicate.Name) 249 | predicateName, err := lr.ProcessPredicatePolicy(predicate, args) 250 | if err != nil { 251 | return nil, err 252 | } 253 | predicateKeys.Insert(predicateName) 254 | } 255 | } 256 | 257 | // 根据策略配置找到所有已注册的Priority的插件名字,Priority在对应于当前的Score 258 | priorityKeys := make(map[string]int64) 259 | if policy.Priorities == nil { 260 | klog.V(2).InfoS("Using default priorities") 261 | priorityKeys = lr.DefaultPriorities 262 | } else { 263 | for _, priority := range policy.Priorities { 264 | if priority.Name == frameworkplugins.EqualPriority { 265 | klog.V(2).InfoS("Skip registering priority", "priority", priority.Name) 266 | continue 267 | } 268 | klog.V(2).InfoS("Registering priority", "priority", priority.Name) 269 | priorityName, err := lr.ProcessPriorityPolicy(priority, args) 270 | if err != nil { 271 | return nil, err 272 | } 273 | priorityKeys[priorityName] = priority.Weight 274 | } 275 | } 276 | 277 | // 设置Pod硬亲和权重参数 278 | if policy.HardPodAffinitySymmetricWeight != 0 { 279 | args.InterPodAffinityArgs = &schedulerapi.InterPodAffinityArgs{ 280 | HardPodAffinityWeight: policy.HardPodAffinitySymmetricWeight, 281 | } 282 | } 283 | 284 | if policy.AlwaysCheckAllPredicates { 285 | c.alwaysCheckAllPredicates = policy.AlwaysCheckAllPredicates 286 | } 287 | 288 | klog.V(2).InfoS("Creating scheduler", "predicates", predicateKeys, "priorities", priorityKeys) 289 | 290 | // 因为在Policy中没有队列排序、抢占、绑定相关的配置,这些都用默认的插件 291 | plugins := schedulerapi.Plugins{ 292 | QueueSort: schedulerapi.PluginSet{ 293 | Enabled: []schedulerapi.Plugin{{Name: queuesort.Name}}, 294 | }, 295 | PostFilter: schedulerapi.PluginSet{ 296 | Enabled: []schedulerapi.Plugin{{Name: defaultpreemption.Name}}, 297 | }, 298 | Bind: schedulerapi.PluginSet{ 299 | Enabled: []schedulerapi.Plugin{{Name: defaultbinder.Name}}, 300 | }, 301 | } 302 | // 基于策略配置生产插件配置 303 | var pluginConfig []schedulerapi.PluginConfig 304 | var err error 305 | if plugins, pluginConfig, err = lr.AppendPredicateConfigs(predicateKeys, args, plugins, pluginConfig); err != nil { 306 | return nil, err 307 | } 308 | if plugins, pluginConfig, err = lr.AppendPriorityConfigs(priorityKeys, args, plugins, pluginConfig); err != nil { 309 | return nil, err 310 | } 311 | if pluginConfig, err = dedupPluginConfigs(pluginConfig); err != nil { 312 | return nil, err 313 | } 314 | // 以下就和Provider一样了 315 | for i := range c.profiles { 316 | prof := &c.profiles[i] 317 | prof.Plugins = &schedulerapi.Plugins{} 318 | prof.Plugins.Append(&plugins) 319 | prof.PluginConfig = pluginConfig 320 | } 321 | 322 | return c.create() 323 | } 324 | ``` 325 | 326 | 基于策略的配置无需修改代码可以动态的调整插件配置,也可以配置插件参数。并且基于策略配置方法多样化,支持ConfigMap和文件。这些都是比Provider友好的地方,但是由于调度框架的升级,原有Predicates/Priority的设计已经无法适应,使得相关的配置也被淘汰。不得不说,基于策略的配置有它的先进性,而且从KubeSchedulerProfile的设计来看也有借鉴它的地方,比如插件参数。 327 | 328 | # 总结 329 | 330 | 1. 无论是基于Provider还是Policy,都是配置调度算法的方法,前者是静态的,后者是动态,都称之为算法源; 331 | 2. 在KubeSchedulerProfile出来之前,算法源是配置插件的唯一方法,Provider通过代码静态编译不是很友好,Policy虽然不用修改代码,可以用ConfigMap和文件配置,但是依然无法适应多Scheduler的需求; 332 | 3. 算法源配置已经不推荐使用了,改用KubeSchedulerProfile方法,Configurator将算法源作为默认的插件配置,在此基础上合并用户自定的配置生成最终的[]KubeSchedulerProfile; 333 | 4. 既然不推荐使用算法源,但是从Configurator只有createFromProvider()和createFromConfig()两种创建Scheduler的接口,Scheduler构造函数选择的是createFromProvider(),默认配置了"DefaultProvider",这就是KubeSchedulerProfile所基于的默认配置。 334 | -------------------------------------------------------------------------------- /kube-scheduler/Extender.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | kube-scheduler基于Kubernetes管理的资源(比如CPU、内存、PV等)进行调度,但是,当需要对在Kubernetes外部管理的资源进行调度时,在Extender之前没有任何机制可以做到这一点。有一种方法可以让kubernetes具有扩展性,就是增加HTTP调度扩展程序。 10 | 11 | 有三种方法可以向kube-scheduler添加新的调度规则: 12 | 13 | 1. 更新现有的或添加新的调度插件并重新编译,这个在[调度插件](./Plugin.md)和[调度框架](./Framework.md)可以找到答案; 14 | 2. 开发一个自定义的kube-scheduler,该进程可代替标准kube-scheduler运行或与之并行运行; 15 | 3. 开发一个调度扩展程序(scheduler extender)程序,kube-scheduler在做调度决策的过程中调用扩展程序一起决策; 16 | 17 | 第一种和第二种方法对初学者有很高的要求,调度插件必须用go编写,并与kube-scheduler一起编译。开发一个自定义的kube-scheduler如果需要支持所有功能(例如扩展,亲和力,污点),则需要大量的工作量。第三种方法虽然也有缺点,例如性能低下和缓存不一致,但它也有几个优点: 18 | 19 | 1. 无需重新编译kube-scheduler就可以扩展现有调度程序的功能; 20 | 2. 调度扩展程序可以用任何语言编写; 21 | 3. 一旦实现调度扩展程序,可以用于扩展不同版本的kube-scheduler; 22 | 23 | 本文介绍第三种方法,如果不关心延迟和缓存一致性,并且不想维护自定义构建的kube-scheduler,那么这种方法可能是更好的选择。如果关心性能并希望最大程度地自定义kube-scheduler,那么开发新调度插件可能是更好的选择。 24 | 25 | 本文参考了[调度扩展程序官方设计文档](https://github.com/kubernetes/enhancements/tree/master/keps/sig-scheduling/1819-scheduler-extender),感兴趣的读者可以阅读原文。本文引用源码为kubernetes的release-1.21分支。 26 | 27 | # Extender 28 | 29 | ## Extender接口 30 | 31 | 对于kube-scheduler,需要定义调度扩展程序的接口,就是Extender接口,源码链接: 32 | 33 | ```go 34 | type Extender interface { 35 | // 调度扩展程序的唯一名字,因为可能会有多个调度扩展程序。 36 | Name() string 37 | 38 | // Filter()和FilterPlugin.Filter()类似,不同的是传入了全部的Node,而插件传入的是一个Node,这个是出于调用效率考虑的,毕竟是远程调用。 39 | // 因为参数与过滤插件不同,所以返回值也略有不同,返回了已过滤的Node(通过过滤)和过滤失败(未通过过滤)的Node。 40 | Filter(pod *v1.Pod, nodes []*v1.Node) (filteredNodes []*v1.Node, failedNodesMap extenderv1.FailedNodesMap, err error) 41 | 42 | // Prioritize()这接口名字是有历史原因的,因为以前的调度器分为‘predicate’和‘prioritize’两个阶段,对应调度插件的Filter和Score。 43 | // Prioritize()接口要求输入Pod以及Filter()返回的Node集合,输出所有Node的评分(hostPriorities)以及调度扩展程序的权重。 44 | // 这样kube-scheduler就可以将所有扩展程序返回的分数乘以权重再累加起来,这一点和调度插件原理是一样的。 45 | Prioritize(pod *v1.Pod, nodes []*v1.Node) (hostPriorities *extenderv1.HostPriorityList, weight int64, err error) 46 | 47 | // Bind()与BindPlugin.Bind()功能一样,只是参数的差异,了解DefaultBinder.Bind()读者应该知道,该函数最终将接口参数转换成了v1.Binding类型在执行绑定的。 48 | Bind(binding *v1.Binding) error 49 | 50 | // 告诉kube-scheduler调度扩展程序是否有绑定能力,如果有绑定能力kube-scheduler会优先用调度扩展程序绑定。 51 | // 需要注意: kube-scheduler会优先用调度扩展程序绑定还有一个条件,那就是Pod有些资源是由Extender管理。 52 | IsBinder() bool 53 | 54 | // 判断Pod是否有任何资源是被Extender管理的,因为有资源被Extender管理交给它绑定才有意义,否则不如直接用默认的绑定插件。 55 | IsInterested(pod *v1.Pod) bool 56 | 57 | // Extender的抢占调度接口,传入待调度Pod,Node和被强占的Pod候选‘nodeNameToVictims’,key是node名字,value是node上被强占的Pod。 58 | // 有同学肯定会问,不是让Extender执行抢占调度么?哪来的Node和被强占的Pod候选?这些不应该是ProcessPreemption()返回的么? 59 | // 这是因为DefaultPreemption(唯一的抢占调度插件)在调用Extender.ProcessPreemption()之前已经执行了一部分抢占调度的来降低 60 | // Extender.ProcessPreemption()候选的数量,毕竟想要实现抢占调度既要满足调度插件的需求也要满足Extender的要求。 61 | // 所以先用调度插件选出一部分候选,可以减少不必要的数据传输,因为这是http调用。关于抢占调度的实现笔者会单独写一个文件解析。 62 | // ProcessPreemption()返回的结果可能:1)nodeNameToVictims的子集;2)候选Node上不同的被强占Pod集合 63 | ProcessPreemption( 64 | pod *v1.Pod, 65 | nodeNameToVictims map[string]*extenderv1.Victims, 66 | nodeInfos NodeInfoLister, 67 | ) (map[string]*extenderv1.Victims, error) 68 | 69 | // 告知kube-scheduler是否具有抢占调度的能力。 70 | SupportsPreemption() bool 71 | 72 | // 告知kube-scheduler如果Extender不可用是否忽略,如果忽略,kube-scheduler不会返回错误。 73 | // 因为Extender的实现是HTTP服务,所以不可用是一种正常现象。 74 | IsIgnorable() bool 75 | } 76 | ``` 77 | 78 | 调度Pod时,扩展程序允许外部进程对节点进行过滤和评分(对应于[调度插件](./Plugin.md)的Filter和Score)。向调度扩展程序程序发出了两个独立的http/https调用,一个用于“过滤”操作,一个用于“评分”操作。如果无法调度Pod,则kube-scheduler将尝试抢占节点中优先级较低的Pod,并将其发送给调度扩展程序“preempt”动词(如果已配置)。调度扩展程序可以将Node和新的被强占Pod子集返回给调度程序。此外,调度扩展程序可以选择通过实现“绑定”操作将Pod绑定到apiserver。 79 | 80 | ## HTTPExtender 81 | 82 | Extender是kube-scheduler抽象的调度扩展程序的接口,而调度扩展程序的实现是一个HTTP服务,也就是Extender的一些接口都对应的是远程调用。所以把Extender看做一个RPC也是可以的,既然是RPC,总要有客户端(Client)的实现,熟悉GRPC的读者应该都懂得哈~ 83 | 84 | 此时就要引入HTTPExtender这个类型了,它实现了Extender,将Extender的一些接口转换为HTTP请求,所以是调度扩展程序的客户端。源码链接: 85 | 86 | ```go 87 | // HTTPExtender实现了Extender接口 88 | type HTTPExtender struct { 89 | // 调度扩展程序的URL,比如https://127.0.0.1:8080。 90 | extenderURL string 91 | // xxxVerb是HTTPExtender.Xxx()接口的HTTP请求的URL,比如https://127.0.0.1:8080/'preemptVerb' 用于ProcessPreemption()接口。 92 | preemptVerb string 93 | filterVerb string 94 | prioritizeVerb string 95 | bindVerb string 96 | // 调度扩展程序的权重,用来与ScorePlugin计算出最终的分数 97 | weight int64 98 | // HTTP客户端 99 | client *http.Client 100 | // 调度扩展程序是否缓存了Node信息,如果调度扩展程序已经缓存了集群中所有节点的全部详细信息,那么只需要发送非常少量的Node信息即可,比如Node名字。 101 | // 毕竟是HTTP调用,想法设法提升效率。但是为什么有podCacheCapable?这就要分析一下HTTPExtender发送的数据包括哪些了? 102 | // 1. 待调度的Pod 103 | // 2. Node(候选) 104 | // 3. 候选Node上的候选Pod(仅抢占调度) 105 | // 试想一下每次HTTP请求中Pod(包括候选Pod)可能不是不同的,而Node呢?有的请求可能会有不同,但于Filter请求因为需要的是Node全量,所以基本是相同。 106 | // 会造成较大的无效数据传输,所以当调度扩展程序能够缓存Node信息时,客户端只需要传输很少的信息就可以了。 107 | nodeCacheCapable bool 108 | // 调度扩展程序管理的资源名称 109 | managedResources sets.String 110 | // 如果调度扩展程序不可用是否忽略 111 | ignorable bool 112 | } 113 | ``` 114 | 115 | ## [Extender配置](./KubeSchedulerConfiguration.md#Extender) 116 | 117 | ## Extender构造函数 118 | 119 | Extender的配置与基本上与HTTPExtender一一对应,所以可以推测HTTPExtender的构造函数主要是通过配置赋值的过程,源码链接: 120 | 121 | ```go 122 | func NewHTTPExtender(config *schedulerapi.Extender) (framework.Extender, error) { 123 | // 没有配置超时,就用默认超时,5秒钟 124 | if config.HTTPTimeout.Duration.Nanoseconds() == 0 { 125 | config.HTTPTimeout.Duration = time.Duration(DefaultExtenderTimeout) 126 | } 127 | // 创建http.Client,makeTransport读者有兴趣自己看一下,跟大部分人用法一样 128 | transport, err := makeTransport(config) 129 | if err != nil { 130 | return nil, err 131 | } 132 | client := &http.Client{ 133 | Transport: transport, 134 | Timeout: config.HTTPTimeout.Duration, 135 | } 136 | // 管理的资源从slice转为map[string]struct{} 137 | managedResources := sets.NewString() 138 | for _, r := range config.ManagedResources { 139 | managedResources.Insert(string(r.Name)) 140 | } 141 | // 各种通过配置赋值 142 | return &HTTPExtender{ 143 | extenderURL: config.URLPrefix, 144 | preemptVerb: config.PreemptVerb, 145 | filterVerb: config.FilterVerb, 146 | prioritizeVerb: config.PrioritizeVerb, 147 | bindVerb: config.BindVerb, 148 | weight: config.Weight, 149 | client: client, 150 | nodeCacheCapable: config.NodeCacheCapable, 151 | managedResources: managedResources, 152 | ignorable: config.Ignorable, 153 | }, nil 154 | } 155 | ``` 156 | 157 | 要使用调度扩展程序,必须创建一个kube-scheduler配置文件,将上面的配置加进去。是不是感觉有点土?每次加一个调度扩展程序都要重启一下kube-scheduler,调度扩展程序将配置写入一个configmap,kube-scheudler监视(watch)配置,然后动态的添加、删除、更新它不香么?这个套路是笔者在实际项目中最常用的方法,保不齐哪天笔者会向社区提交一个可以动态配置调度扩展程序的PR! 158 | 159 | ## send 160 | 161 | Kubernetes定义了各种请求(xxxVerb)的参数以及结果的类型,在中,本文就不一一列举了。所有的请求参数和结果都是序列化为JSON格式放在HTTP的Body中,这一点可以从send()函数看出来,源码链接: 162 | 163 | ```go 164 | func (h *HTTPExtender) send(action string, args interface{}, result interface{}) error { 165 | // 将请求参数(比如filter和prioritize请求是ExtenderArgs,preempt请求是ExtenderPreemptionArgs)序列化为JSON格式。 166 | out, err := json.Marshal(args) 167 | if err != nil { 168 | return err 169 | } 170 | // 格式化请求的最终URL 171 | url := strings.TrimRight(h.extenderURL, "/") + "/" + action 172 | // 创建HTTP请求(看来POST还是比较香的),并将JSON格式的参数放到Body中 173 | req, err := http.NewRequest("POST", url, bytes.NewReader(out)) 174 | if err != nil { 175 | return err 176 | } 177 | // 内容格式是JSON 178 | req.Header.Set("Content-Type", "application/json") 179 | // 发送HTTP请求 180 | resp, err := h.client.Do(req) 181 | if err != nil { 182 | return err 183 | } 184 | defer resp.Body.Close() 185 | // 检查HTTP的状态码,如果不是200就返回错误 186 | if resp.StatusCode != http.StatusOK { 187 | return fmt.Errorf("failed %v with extender at URL %v, code %v", action, url, resp.StatusCode) 188 | } 189 | // 解析Body中的结果(比如filter请求是ExtenderFilterResult,prioritize请求是HostPriorityList,preempt请求是ExtenderPreemptionResult) 190 | return json.NewDecoder(resp.Body).Decode(result) 191 | } 192 | ``` 193 | 194 | 看到send函数的实现,基本可以推测Filter()、Prioritize()以及ProcessPreemption()的实现,但是笔者还是象征性的对代码做一下简单的注释。本文不对ProcessPreemption()做注释,因为笔者一直都在说有一个单独的文章分析kube-scheduler的抢占调度的实现,ProcessPreemption()的实现会放到那个文章中分析,感兴趣的读者自己可以看看。 195 | 196 | ## Filter实现 197 | 198 | 不废话了,直接上代码,源码链接: 199 | 200 | ```go 201 | func (h *HTTPExtender) Filter( 202 | pod *v1.Pod, 203 | nodes []*v1.Node, 204 | ) ([]*v1.Node, extenderv1.FailedNodesMap, error) { 205 | var ( 206 | result extenderv1.ExtenderFilterResult 207 | nodeList *v1.NodeList 208 | nodeNames *[]string 209 | nodeResult []*v1.Node 210 | args *extenderv1.ExtenderArgs 211 | ) 212 | // 将[]*v1.Node转为map[string]*v1.Node,当调度扩展程序缓存了Node信息,返回的结果只有Node名字。 213 | // fromNodeName用于根据Node名字快速查找对应的Node 214 | fromNodeName := make(map[string]*v1.Node) 215 | for _, n := range nodes { 216 | fromNodeName[n.Name] = n 217 | } 218 | // 如果没有配置filterVerb,说明调度扩展程序不支持Filter,所以直接返回 219 | if h.filterVerb == "" { 220 | return nodes, extenderv1.FailedNodesMap{}, nil 221 | } 222 | 223 | if h.nodeCacheCapable { 224 | // 如果调度扩展程序缓存了Node信息,则参数中只需要设置Node的名字 225 | nodeNameSlice := make([]string, 0, len(nodes)) 226 | for _, node := range nodes { 227 | nodeNameSlice = append(nodeNameSlice, node.Name) 228 | } 229 | nodeNames = &nodeNameSlice 230 | } else { 231 | // 如果调度扩展程序没有缓存Node信息,就只能把全量的Node放在参数中 232 | nodeList = &v1.NodeList{} 233 | for _, node := range nodes { 234 | nodeList.Items = append(nodeList.Items, *node) 235 | } 236 | } 237 | // 构造HTTP请求参数 238 | args = &extenderv1.ExtenderArgs{ 239 | Pod: pod, 240 | Nodes: nodeList, 241 | NodeNames: nodeNames, 242 | } 243 | // 发送HTTP请求 244 | if err := h.send(h.filterVerb, args, &result); err != nil { 245 | return nil, nil, err 246 | } 247 | if result.Error != "" { 248 | return nil, nil, fmt.Errorf(result.Error) 249 | } 250 | // 如果调度扩展程序缓存Node信息并且结果中设置了Node名字,那么前面[]*v1.Node转为map[string]*v1.Node就用上了 251 | if h.nodeCacheCapable && result.NodeNames != nil { 252 | nodeResult = make([]*v1.Node, len(*result.NodeNames)) 253 | // 根据返回结果的Node名字找到Node并输出 254 | for i, nodeName := range *result.NodeNames { 255 | if n, ok := fromNodeName[nodeName]; ok { 256 | nodeResult[i] = n 257 | } else { 258 | return nil, nil, fmt.Errorf( 259 | "extender %q claims a filtered node %q which is not found in the input node list", 260 | h.extenderURL, nodeName) 261 | } 262 | } 263 | } else if result.Nodes != nil { 264 | // 直接从结果中获取Node 265 | nodeResult = make([]*v1.Node, len(result.Nodes.Items)) 266 | for i := range result.Nodes.Items { 267 | nodeResult[i] = &result.Nodes.Items[i] 268 | } 269 | } 270 | 271 | return nodeResult, result.FailedNodes, nil 272 | } 273 | ``` 274 | 275 | ## Prioritize实现 276 | 277 | 不废话了,直接上代码,源码链接: 278 | 279 | ```go 280 | func (h *HTTPExtender) Prioritize(pod *v1.Pod, nodes []*v1.Node) (*extenderv1.HostPriorityList, int64, error) { 281 | var ( 282 | result extenderv1.HostPriorityList 283 | nodeList *v1.NodeList 284 | nodeNames *[]string 285 | args *extenderv1.ExtenderArgs 286 | ) 287 | // 如果没有配置prioritizeVerb,说明调度扩展程序不支持Prioritize,所以直接返回 288 | if h.prioritizeVerb == "" { 289 | result := extenderv1.HostPriorityList{} 290 | for _, node := range nodes { 291 | result = append(result, extenderv1.HostPriority{Host: node.Name, Score: 0}) 292 | } 293 | return &result, 0, nil 294 | } 295 | 296 | // 这部分和Filter()的一样,不重复注释了 297 | if h.nodeCacheCapable { 298 | nodeNameSlice := make([]string, 0, len(nodes)) 299 | for _, node := range nodes { 300 | nodeNameSlice = append(nodeNameSlice, node.Name) 301 | } 302 | nodeNames = &nodeNameSlice 303 | } else { 304 | nodeList = &v1.NodeList{} 305 | for _, node := range nodes { 306 | nodeList.Items = append(nodeList.Items, *node) 307 | } 308 | } 309 | // 构造HTTP请求参数 310 | args = &extenderv1.ExtenderArgs{ 311 | Pod: pod, 312 | Nodes: nodeList, 313 | NodeNames: nodeNames, 314 | } 315 | // 发送HTTP请求 316 | if err := h.send(h.prioritizeVerb, args, &result); err != nil { 317 | return nil, 0, err 318 | } 319 | return &result, h.weight, nil 320 | } 321 | ``` 322 | 323 | # 总结 324 | 325 | 1. Extender是kube-scheduler抽象的调度扩展程序接口,kube-scheduler在调度流程的适当地方调用Extender协助kube-scheduler完成调度; 326 | 2. Extender可以实现过滤、评分、抢占和绑定,主要用来实现Kubernetes无法管理的资源的调度; 327 | 3. HTTPExtender是Extender的一种实现,用于将Extender的接口请求转为HTTP请求发送给调度扩展程序,配置调度扩展程序通过kube-scheduler的配置文件实现; 328 | -------------------------------------------------------------------------------- /kube-scheduler/KubeSchedulerConfiguration.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | kube-scheduler如何配置[调度框架](./Framework.md)和[调度插件](./Plugin.md)中提到的插件(比如使能或禁止哪些插件?)?如何配置[调度队列](./SchedulingQueue.md)的排序函数以及退避时间?本文将详细解析kube-scheduler的配置,在kube-scheduler中,配置也是一种API对象(其实在Kubernetes中都是这种设计,万物皆可API化),它被定义在`k8s.io/kubernetes/pkg/scheduler/apis/config`包中。 10 | 11 | 本文引用源码为kubernetes的release-1.21分支。 12 | 13 | # KubeSchedulerConfiguration 14 | 15 | KubeSchedulerConfiguration从类型名称上可知是kube-scheduler的配置API,源码链接: 16 | 17 | ```go 18 | type KubeSchedulerConfiguration struct { 19 | // K8S所有的API必须有的类型元信息,不用多余解释了。 20 | metav1.TypeMeta 21 | 22 | // 调度Pod的算法的最大并行度,必须大于0,默认为16(在后续章节中会介绍如何设置默认配置)。 23 | // 还记得调度框架文章中提到的parallelize.Until(),比如kube-scheduler采用多协程并发计算分数,最大并行度就是这个参数指定的。 24 | Parallelism int32 25 | 26 | // 调度算法源,包括调度调度算法提供者(Provider)和调度策略两种,后者又分为文件和ConfigMap两种源,笔者在调度插件的文章中简单介绍过。 27 | // 但是AlgorithmSource的方式已经不推荐使用了,取而代之的是KubeSchedulerProfile,下面会看到,所以本文不会对算法源做过多介绍。 28 | AlgorithmSource SchedulerAlgorithmSource 29 | 30 | // LeaderElection选举相关的配置,采用多个kube-scheduler选举leader的方式实现高可用。 31 | // 本文不解析该配置,因为这属于基础组件的配置,从包名子componentbaseconfig也可以看出来,它不专属于kube-scheduler. 32 | // 笔者会单独写一篇文章解析Kubernetes的Leader选举,并介绍如何把Kubernetes的Leader选举用在自己的项目中。 33 | LeaderElection componentbaseconfig.LeaderElectionConfiguration 34 | 35 | // ClientConnection指定了kubeconfig文件和与apiserver通信时要使用的客户端连接设置。 36 | // ClientConnection同样是基础组件配置之一(毕竟需要连接apiserver的组件太多了),本文不做解析。 37 | ClientConnection componentbaseconfig.ClientConnectionConfiguration 38 | // HealthzBindAddress是检查kube-scheduler健康状态使用的IP地址和端口,默认为0.0.0.0:10251 39 | // MetricsBindAddress是获取kube-scheduler监控指标的IP地址和端口,默认为0.0.0.0:10251。 40 | // 这两个地址都是kube-scheduler对外提供http接口服务的地址,可以检测服务健康度以及获取各种监控指标。 41 | HealthzBindAddress string 42 | MetricsBindAddress string 43 | 44 | // 调试相关的配置,属于基础组件配置,本文不做解析。 45 | componentbaseconfig.DebuggingConfiguration 46 | 47 | // 如果集群特别大,每次调度一个Pod都需要遍历所有的Node计算量是不是非常大?PercentageOfNodesToScore就是用来减少计算量的。 48 | // 只要超过PercentageOfNodesToScore指定百分比的Node可行,那么调度器就在这些Node中寻找最优节点,而不会再遍历其他Node。 49 | // 例如:如果集群大小为500个节点,并且此配置的值为30,则调度器一旦找到150个可行Node就会停止查找。 50 | // 所以说,kube-scheduler并不是为每个Pod计算全局最优的Node,而是局部最优。 51 | PercentageOfNodesToScore int32 52 | 53 | // 用于配置调度队列的初始退避时间和最大退避时间,单位是秒。 54 | // 详情参看https://github.com/jindezgm/k8s-src-analysis/tree/master/kube-scheduler/SchedulingQueue.md 55 | PodInitialBackoffSeconds int64 56 | PodMaxBackoffSeconds int64 57 | 58 | // Profiles是kube-scheduler支持的调度配置,每个KubeSchedulerProfile对应一个独立的调度器,并有一个唯一的名字。 59 | // Pod可以指定调度器名字选择调度器,如果未指定任何调度程序名字,则将使用“default-scheduler”配置进行调度。 60 | // 后文专门有一个章节介绍KubeSchedulerProfile,因为KubeSchedulerProfile是本文的重点内容。 61 | Profiles []KubeSchedulerProfile 62 | 63 | // 调度扩展程序的配置列表,每个Extender都包含如何与调度扩展程序进行通信的配置,这些调度扩展程序由所有KubeSchedulerProfile共享。 64 | // 因为Extender是本质就是在kube-scheduler外部扩展调度插件,所以在所有KubeSchedulerProfile共享是必须的。 65 | Extenders []Extender 66 | } 67 | ``` 68 | 69 | 既然定义为API对象,按照Kubernetes的习惯,API对象可以通过Rest接口、文件、ConfigMap等方式访问。KubeSchedulerConfiguration是通过文件的方式进行配置的,那么问题来了,配置文件路径kube-scheduler如何知道?很简单,通过命令行--config设置,如下所示(执行kube-scheduler -h): 70 | 71 | ```base 72 | Usage: 73 | kube-scheduler [flags] 74 | 75 | Misc flags: 76 | 77 | --config string 78 | The path to the configuration file. The following flags can overwrite fields in this file: 79 | --address 80 | --port 81 | --use-legacy-policy-config 82 | --policy-configmap 83 | --policy-config-file 84 | --algorithm-provider 85 | ``` 86 | 87 | 需要注意的是,虽然可以通过文件配置KubeSchedulerConfiguration,但是依然可以通过--address、--port、--use-legacy-policy-config等覆盖配置中的字段。下面是KubeSchedulerConfiguration文件的简单示例(附带笔者注释): 88 | 89 | ```yaml 90 | apiVersion: kubescheduler.config.k8s.io/v1beta1 91 | kind: KubeSchedulerConfiguration 92 | # Leader选举配置 93 | leaderElection: 94 | # 使能Leader选举 95 | leaderElect: true 96 | # apiserver客户端连接配置 97 | clientConnection: 98 | # 指定了kubeconfig文件路径 99 | kubeconfig: /etc/kubernetes/scheduler.conf 100 | profiles: 101 | # 调度器[0]配置,名字是'default-scheduler' 102 | - schedulerName: default-scheduler 103 | # 插件配置 104 | plugins: 105 | # 禁用QueueSort扩展点所有默认插件,使能Test 106 | queueSort: 107 | enabled: 108 | - name: Test 109 | disabled: 110 | - name: "*" 111 | # PreFilter扩展点使能Test 112 | preFilter: 113 | enabled: 114 | - name: Test 115 | # Permit扩展点使能Test 116 | permit: 117 | enabled: 118 | - name: Test 119 | # Reserve扩展点使能Test 120 | reserve: 121 | enabled: 122 | - name: Test 123 | # PostBind扩展点使能Test 124 | postBind: 125 | enabled: 126 | - name: Test 127 | # 插件参数配置 128 | pluginConfig: 129 | # Test插件的参数 130 | - name: Test 131 | args: 132 | abcd: efg 133 | ``` 134 | 135 | 有没有注意到配置文件中apiVersion字段是`kubescheduler.config.k8s.io/v1beta1`,而本文前面介绍的KubeSchedulerConfiguration是在`k8s.io/kubernetes/pkg/scheduler/apis/config`包中,按照apiVersion字段声明的版本,应该是`k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta`包中的类型。都是KubeSchedulerConfiguration类型,二者有什么区别么?其实这样的设计在Kubernetes其他的组件随处可见,`k8s.io/kubernetes/pkg/scheduler/apis/config`中定义的都是内部类型,kube-scheduler使用的也都是内部类型;而`k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta`定义的是v1beta版本的类型,其实使用者可以定义多个版本(比如v1alpha),但是最终还是要转换为内部类型使用,转换函数注册在converter中。说了这么多其实需要读者有一个初步的了解,暂时不需要深入理解,当然,笔者也会有相关的文章介绍这些内容。 136 | 137 | 刨除一些相对不重要的配置参数,笔者总结kube-scheduler的配置主要包括: 138 | 139 | 1. [调度框架](./Framework.md)与[调度插件](./Plugin.md)相关的配置由Profiles(KubeSchedulerProfile)实现; 140 | 2. [调度扩展程序](./Extender.md)相关的配置由Extenders实现; 141 | 3. [调度队列](./SchedulingQueue.md)的退避时间配置由PodInitialBackoffSeconds和PodMaxBackoffSeconds实现; 142 | 4. [调度算法](./ScheduleAlgorithm.md)(插件)最大并行度由Parallelism实现; 143 | 144 | 接下来,笔者就会进一步解析调度插件的配置以及调度扩展程序的配置,至于其他的(基本组件配置除外)要么非常简单要么在其他文章中介绍过了,本文就不在解析。 145 | 146 | ## KubeSchedulerProfile 147 | 148 | 随着群集中的工作负载变得越来越多样化(异构),很自然它们有不同的调度需求。kube-scheduler运行不同的[调度框架](./Framework.md)的[插件](./Plugin.md)配置,将其称为Profile,并关联一个调度器名字。通过设置Pod.Spec.SchedulerName可以选择特定配置来调度Pod。如果未指定调度器名字,则采用默认配置进行调度。 149 | 150 | 集群运行各种工作负载,大致可分为服务(长期运行)和批处理作业(运行后完成)。一些用户可能选择只在集群中运行一类工作负载,因此他们可以提供适合其调度需求的合理配置。但是,用户可以选择在单个群集中运行更多异构工作负载,或者他们可以具有一组固定节点和一组自动缩放的节点,每个节点都需要不同的调度行为。 151 | 152 | Pod可以使用诸如node/pod亲和性,容忍度甚至是pod拓扑之类的功能来影响调度决策。但是有两个问题: 153 | 154 | * 单个kube-scheduler配置以权重分数方式无法适应所有类型,例如,默认配置包括寻求服务高可用性的分数。 155 | * 工作负载的开发者需要了解集群的特性和分数的权重来影响他们的Pod的调度。 156 | 157 | 为了服务于此类异构类型的工作负载,某些集群管理员选择运行多个调度程序,无论这些调度程序是不同的二进制文件还是具有不同配置的kube-scheduler。但是此设置可能会导致多个调度程序之间竞争,因为它们在给定时间可能对集群资源有不同的看法。此外,更多的二进制文件需要更多的管理工作。相反,只有一个kube-scheduler运行多个配置具有运行多个调度器而不会遇到竞争的优点。 158 | 159 | kube-scheduler的配置在v1alpha2版本引入了KubeSchedulerProfile,源码链接: 160 | 161 | ```go 162 | // KubeSchedulerProfile是调度配置,一个KubeSchedulerProfile在kube-scheduler中对应一个调度器(准确的讲应该是一个Framework),并为其定义了唯一名字。 163 | type KubeSchedulerProfile struct { 164 | // SchedulerName是与此配置关联的调度程序的名称,如果SchedulerName与Pod.Spec.SchedulerName匹配,则将使用此配置调度Pod。 165 | SchedulerName string 166 | 167 | // Plugins(下面有注释)为每个扩展点指定使能或禁用的插件集合,使能的插件是除默认插件之外还应启用的插件,禁用的插件是应禁用的任何默认插件。 168 | // 至于默认插件有哪些,笔者在调度插件和调度框架的文章中有提到,本文不赘述。如果没有为扩展点指定使能或禁用的插件,那么将使用该扩展点的默认插件。 169 | // 如果指定了QueueSort插件,那么所有配置必须指定相同的QueueSort插件和PluginConfig(该插件的参数,见下文)。 170 | Plugins *Plugins 171 | 172 | // PluginConfig是每个插件的一组可选的自定义插件参数。未配置插件的配置参数等同于使用该插件的默认配置参数。 173 | // 这个应该比较好理解,每个插件都可能有一些自定的配置,调度框架是不可能抽象所有可能参数的,所以为每个插件提供配置自定义参数的接口。 174 | PluginConfig []PluginConfig 175 | } 176 | 177 | // Plugins包括调度框架的全部扩展点的配置,因为特别简单,笔者就不一一注释了。 178 | type Plugins struct { 179 | // 调度队列排序扩展点的插件配置,下面的就不一一注释了 180 | QueueSort PluginSet 181 | PreFilter PluginSet 182 | Filter PluginSet 183 | PostFilter PluginSet 184 | PreScore PluginSet 185 | Score PluginSet 186 | Reserve PluginSet 187 | Permit PluginSet 188 | PreBind PluginSet 189 | Bind PluginSet 190 | PostBind PluginSet 191 | } 192 | 193 | // PluginSet为扩展点指定使能和禁用的插件,如果数组为nil或者长度为0,则将使用该扩展点的默认插件。 194 | type PluginSet struct { 195 | // Enabled指定除默认插件外还应启用的插件,这些将在默认插件之后以此处指定的顺序调用。 196 | // 那么问题来了,除了默认插件外,其他的插件都有啥?其实这里使能的插件应该都是使用者自己开发的插件。 197 | // 毕竟kube-scheduler不可能集成所有场景的插件,还是需要使用者根据自己需求开发特定的插件。 198 | // 需要注意的是,新开发的插件需要注册到kube-scheduler中,笔者知道的注册方法有两种: 199 | // 1. 直接写在默认的插件配置中,这样就不用使能了; 200 | // 2. 通过NewSchedulerCommand()函数的参数传入; 201 | Enabled []Plugin 202 | // Disabled指定禁用的默认插件,当需要禁用所有默认插件时,提供仅包含一个“*”的数组。 203 | Disabled []Plugin 204 | } 205 | 206 | // Plugin指定插件名称及其权重,权重仅用于Score插件。 207 | type Plugin struct { 208 | // 插件的名字 209 | Name string 210 | // 插件的权重 211 | Weight int32 212 | } 213 | 214 | // PluginConfig指定在初始化时传递给插件的参数,在多个扩展点调用的插件将被初始化一次,Args可以具有任意结构体。 215 | type PluginConfig struct { 216 | // 插件名字,与Plugin.Name匹配 217 | Name string 218 | // Args定义初始化时传递给插件的参数,可以具有任意结构体。这很合理,没有人能够知道未来扩展的插件需要什么样的参数。 219 | Args runtime.Object 220 | } 221 | ``` 222 | 223 | PluginConfig.Args的类型runtime.Object,表示它也应该是一种API,并注册在Scheme中,这样kube-scheduler才能解析成对象传给插件,如下[代码](https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/scheduler/apis/config/register.go#L38)所示: 224 | 225 | ```go 226 | func addKnownTypes(scheme *runtime.Scheme) error { 227 | scheme.AddKnownTypes(SchemeGroupVersion, 228 | &KubeSchedulerConfiguration{}, 229 | &Policy{}, 230 | // 以下是注册各种插件参数的类型,相关定义笔者可以自己看一下,本文不再扩展 231 | &DefaultPreemptionArgs{}, 232 | &InterPodAffinityArgs{}, 233 | &NodeLabelArgs{}, 234 | &NodeResourcesFitArgs{}, 235 | &PodTopologySpreadArgs{}, 236 | &RequestedToCapacityRatioArgs{}, 237 | &ServiceAffinityArgs{}, 238 | &VolumeBindingArgs{}, 239 | &NodeResourcesLeastAllocatedArgs{}, 240 | &NodeResourcesMostAllocatedArgs{}, 241 | &NodeAffinityArgs{}, 242 | ) 243 | scheme.AddKnownTypes(schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}, &Policy{}) 244 | return nil 245 | } 246 | ``` 247 | 248 | ## Extender 249 | 250 | 笔者在[《kube-scheduler调度扩展程序解析》](./Extender.md)中详细解析了Extender,如果对调度扩展程序不了解的读者建议先阅读一下。[HTTPExtender](./Extender.md#HTTPExtender)(调度扩展程序的一种实现,kube-scheduler采用HTTP协议与调度扩展程序交互,当前来看是唯一实现)中有很多成员变量是配置的,比如extenderURL、xxxVerb、weight等,都是通过调度扩展程序配置实现的,源码链接: 251 | 252 | ```go 253 | // HTTPExtender的配置,起这个名字感觉像是通用Extender的配置,没准哪天又冒出来一个GRPCExtender怎么办? 254 | type Extender struct { 255 | // HTTPExtender服务端的URL,为什么是前缀(URLPrefix)?因为它是每个请求最终URL的前缀。 256 | URLPrefix string 257 | // XxxVerb对应HTTPExtender.xxxVerb,具体功能参看解析Extender的文档。 258 | FilterVerb string 259 | PreemptVerb string 260 | PrioritizeVerb string 261 | BindVerb string 262 | 263 | // 权重,因为调度扩展程序可能包含有ScorePlugin 264 | Weight int64 265 | // 是否采用HTTPS与调度扩展程序通信 266 | EnableHTTPS bool 267 | // 如果采用HTTPS通信,创建http.Client就需要用到TLS配置,不需要解释 268 | TLSConfig *ExtenderTLSConfig 269 | // HTTPTimeout指定调用调度扩展程序的超时时间,在过滤阶段如果超时将无法调度Pod,在评分阶段(Prioritize)超时会被忽略。 270 | // 从这个参数可以看出,虽然调度扩展程序通过HTTP协议扩展了调度器,并且不受限于语言,但是存在超时的风险,万不得已笔者还是建议直接扩展调度插件。 271 | HTTPTimeout metav1.Duration 272 | // 对应HTTPExtender.nodeCacheCapable,此处不重复解释了 273 | NodeCacheCapable bool 274 | // 调度扩展程序管理的资源,即哪些资源归调度扩展程序调度,此处是slice,而HTTPExtender是字符串set,简单转换一下就行了 275 | ManagedResources []ExtenderManagedResource 276 | // 对应HTTPExtender.ignorable。 277 | Ignorable bool 278 | } 279 | ``` 280 | 281 | ## SetDefaults_KubeSchedulerConfiguration 282 | 283 | 前面章节中一直在说默认配置,那么默认配置到是怎样的?源码链接: 284 | 285 | ```go 286 | // newDefaultComponentConfig()用于创建默认的KubeSchedulerConfiguration。 287 | func newDefaultComponentConfig() (*kubeschedulerconfig.KubeSchedulerConfiguration, error) { 288 | // 首先创建v1beta1版本的KubeSchedulerConfiguration对象,所以是版本化的配置。 289 | versionedCfg := kubeschedulerconfigv1beta1.KubeSchedulerConfiguration{} 290 | // 关于Debug相关的配置本文不做详细说明,感兴趣的读者可以自己了解 291 | versionedCfg.DebuggingConfiguration = *configv1alpha1.NewRecommendedDebuggingConfiguration() 292 | 293 | // 设置versionedCfg的默认值(Scheme为每个API对象提供了注册设置默认值函数的接口,并通过Default()接口调用) 294 | kubeschedulerscheme.Scheme.Default(&versionedCfg) 295 | // 将v1beta1版本的KubeSchedulerConfiguration转换为KubeSchedulerConfiguration 296 | // 因为kube-scheudler使用的是内部类型,而不是版本化的类型。 297 | cfg := kubeschedulerconfig.KubeSchedulerConfiguration{} 298 | if err := kubeschedulerscheme.Scheme.Convert(&versionedCfg, &cfg, nil); err != nil { 299 | return nil, err 300 | } 301 | return &cfg, nil 302 | } 303 | ``` 304 | 305 | newDefaultComponentConfig()感觉做点事,又感觉什么也没做!毕竟只是调用了注册的设置默认值函数,那么v1beta1.KubeSchedulerConfiguration的设置默认值的函数又是哪个?源码链接: 306 | 307 | ```go 308 | // SetDefaults_KubeSchedulerConfiguration()用于设置KubeSchedulerConfiguration的默认值。 309 | func SetDefaults_KubeSchedulerConfiguration(obj *v1beta1.KubeSchedulerConfiguration) { 310 | // 最大并行度默认值是16 311 | if obj.Parallelism == nil { 312 | obj.Parallelism = pointer.Int32Ptr(16) 313 | } 314 | 315 | // newDefaultComponentConfig()函数可知obj.Profiles是空的。 316 | // 沿着newDefaultComponentConfig()思路obj.Profiles追加了一个未被初始化的v1beta1.KubeSchedulerProfile对象。 317 | if len(obj.Profiles) == 0 { 318 | obj.Profiles = append(obj.Profiles, v1beta1.KubeSchedulerProfile{}) 319 | } 320 | // 因为上面代码追加了一个没有初始化话的v1beta1.KubeSchedulerProfile对象,所以需要为这个配置设置一个名字。 321 | // 这个名字就是默认调度器的名字DefaultSchedulerName("default-scheduler") 322 | if len(obj.Profiles) == 1 && obj.Profiles[0].SchedulerName == nil { 323 | obj.Profiles[0].SchedulerName = pointer.StringPtr(v1.DefaultSchedulerName) 324 | } 325 | 326 | // HealthzBindAddress和MetricsBindAddress不是本文的重点,所以相关设置默认值的代码此处省略 327 | ... 328 | 329 | // 默认的PercentageOfNodesToScore是0(config.DefaultPercentageOfNodesToScore) 330 | // 如果为0,调度算法会根据集群Node数量计算一个自适应的比例。 331 | if obj.PercentageOfNodesToScore == nil { 332 | percentageOfNodesToScore := int32(config.DefaultPercentageOfNodesToScore) 333 | obj.PercentageOfNodesToScore = &percentageOfNodesToScore 334 | } 335 | 336 | // 基础组件配置的默认值此处省略 337 | ... 338 | 339 | // 默认的初识退避时间是1秒 340 | if obj.PodInitialBackoffSeconds == nil { 341 | val := int64(1) 342 | obj.PodInitialBackoffSeconds = &val 343 | } 344 | 345 | // 默认的最大退避时间是10秒 346 | if obj.PodMaxBackoffSeconds == nil { 347 | val := int64(10) 348 | obj.PodMaxBackoffSeconds = &val 349 | } 350 | } 351 | ``` 352 | 353 | 那么问题来了,有了设置默认值的函数,是如何注册到Scheme的呢?API对象的设置默认值的函数是通过code-generator自动生成的,源码链接: 354 | 355 | ```go 356 | func RegisterDefaults(scheme *runtime.Scheme) error { 357 | ... 358 | // 将SetObjectDefaults_KubeSchedulerConfiguration注册为v1beta1.KubeSchedulerConfiguration{}的设置默认值的函数 359 | scheme.AddTypeDefaultingFunc(&v1beta1.KubeSchedulerConfiguration{}, func(obj interface{}) { 360 | SetObjectDefaults_KubeSchedulerConfiguration(obj.(*v1beta1.KubeSchedulerConfiguration)) 361 | }) 362 | ... 363 | return nil 364 | } 365 | 366 | // SetObjectDefaults_KubeSchedulerConfiguration最终调用了SetDefaults_KubeSchedulerConfiguration 367 | func SetObjectDefaults_KubeSchedulerConfiguration(in *v1beta1.KubeSchedulerConfiguration) { 368 | SetDefaults_KubeSchedulerConfiguration(in) 369 | } 370 | ``` 371 | 372 | 虽然知道是如何注册KubeSchedulerConfiguration的设置默认值函数,但是kube-scheduler是在什么时候注册的呢?源码链接: 373 | 374 | ```go 375 | // 在引入v1beta1包的时候就注册了addDefaultingFuncs()函数 376 | func init() { 377 | localSchemeBuilder.Register(addDefaultingFuncs) 378 | } 379 | ``` 380 | 381 | addDefaultingFuncs()函数定义在 382 | 383 | ```go 384 | // 太简单了,不用解释了吧。 385 | func addDefaultingFuncs(scheme *runtime.Scheme) error { 386 | return RegisterDefaults(scheme) 387 | } 388 | ``` 389 | 390 | 经过一系列的源码解析,有没有发现v1beta1.KubeSchedulerConfiguration相关的代码组织在不同的包中,笔者整理如下: 391 | 392 | 1. v1beta1.KubeSchedulerConfiguration的类型被定义在`k8s.io/kube-scheduler/config/v1beta1` 393 | 2. v1beta1.KubeSchedulerConfiguration的deepcopy、defaults、conversion等被被生成在`k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta` 394 | 395 | # 总结 396 | 397 | 1. kube-scheduler通过KubeSchedulerProfile实现多个调度器配置,每个调度器都有一个名字,默认的调度器名字是"default-scheduler",可以通过Pod.Spec.SchedulerName为Pod选择调度器; 398 | 2. 每个调度器配置(KubeSchedulerProfile)通过Plugins配置每个扩展点(QueueSort、PreFilter、Filter等),每个扩展点可以通过PluginSet使能/禁止指定的插件,每个插件通过Plugin配置名字和权重(只在Score扩展点使用); 399 | 3. 调度插件的自定义参数通过PluginConfig配置; 400 | 4. 使能配置是在默认插件基础上增加的插件,禁用配置是需要禁用指定的默认插件; 401 | 5. 所有的KubeSchedulerProfile必须指定相同的QueueSort插件,至于为什么,[Configurator](./Configurator.md)可以找到答案; 402 | 403 | 404 | 405 | 406 | -------------------------------------------------------------------------------- /kube-scheduler/Plug-In.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/kube-scheduler/Plug-In.png -------------------------------------------------------------------------------- /kube-scheduler/PodNominator.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 笔者在[《SchedulingQueue》](./SchedulingQueue.md)的文章中提到了PodNominator,因为调度队列继承了PodNominator,但是当时为了避免不必要的内容扩展,并没有对PodNominator做任何说明。本文将对PodNominator做较全面的解析,从它的定义、实现直到在kube-scheduler中的应用。 10 | 11 | 本文采用的Kubenetes源码的release-1.20分支。 12 | 13 | # PodNominator的定义 14 | 15 | 首先需要了解PodNominator到底是干啥的,英文翻译就是Pod提名器,那什么又是提名呢?这个可以从Pod的API定义的部分注释得到结果。源码链接: 16 | 17 | ```go 18 | // nominatedNodeName is set only when this pod preempts other pods on the node, but it cannot be 19 | // scheduled right away as preemption victims receive their graceful termination periods. 20 | // This field does not guarantee that the pod will be scheduled on this node. Scheduler may decide 21 | // to place the pod elsewhere if other nodes become available sooner. Scheduler may also decide to 22 | // give the resources on this node to a higher priority pod that is created after preemption. 23 | // As a result, this field may be different than PodSpec.nodeName when the pod is 24 | // scheduled. 25 | // +optional 26 | NominatedNodeName string `json:"nominatedNodeName,omitempty" protobuf:"bytes,11,opt,name=nominatedNodeName"` 27 | ``` 28 | 29 | 简单总结源码注释就是当Pod抢占Node上其他Pod的时候会设置Pod.Status.NominatedNodeName为Node的名字。调度器不会立刻将Pod调度到Node上,因为需要等到被抢占的Pod优雅退出。到这里就可以了,其他内容属于调度相关的,我们只需要知道提名到底是干啥的就可以了。 30 | 31 | 现在知道提名就是可以将Pod调度到提名的Node上,但是需要等被抢占的Pod退出后腾出资源才能执行调度。而PodNominator就是记录哪些Pod获得Node提名,那么问题来了,Pod.Status.NominatedNodeName不是已经记录了么,还要PodNominator干什么呢?笔者先不回答这个问题,在总结的章节中会给出答案。 32 | 33 | 先来看看PodNominator的定义,源码链接: 34 | 35 | ```go 36 | type PodNominator interface { 37 | // 提名pod调度到nodeName的Node上 38 | AddNominatedPod(pod *v1.Pod, nodeName string) 39 | // 删除pod提名的nodeName 40 | DeleteNominatedPodIfExists(pod *v1.Pod) 41 | // PodNominator处理pod更新事件 42 | UpdateNominatedPod(oldPod, newPod *v1.Pod) 43 | // 获取提名到nodeName上的所有Pod,这个接口是不是感觉到PodNominator存在的意义了? 44 | // 毕竟PodNominator有统计功能,否则就需要遍历所有的Pod.Status.NominatedNodeName才能统计出来 45 | NominatedPodsForNode(nodeName string) []*v1.Pod 46 | } 47 | ``` 48 | 49 | 从PodNominator的定义来看非常简单,感觉用map就可以实现了,事实上也确实如此。 50 | 51 | # PodNominator的实现 52 | 53 | PodNominator的实现在调度队列中,这其中的用意值得研究一下。咱们先看代码实现,在研究为什么在调度队列中实现。源码链接: 54 | 55 | ```go 56 | // 确实非常简单,用map加读写锁实现的 57 | type nominatedPodMap struct { 58 | // nominatedPods的key是提名的Node的名字,value是所有的Pod,这就是为NominatedPodsForNode()接口专门设计的 59 | nominatedPods map[string][]*v1.Pod 60 | // nominatedPodToNode的key是Pod的UID,value是提名Node的名字,这是为增、删、改接口设计的 61 | // 为什么调度队列用NS+NAME,而这里用UID,其中可能有版本更新造成的历史原因,关键要看是否有用名字访问的需求。 62 | // 当然,当前版本调度队列的接口没有直接用NS+NAME访问Pod,但是不排除以前是有这个设计考虑的。 63 | // 以上只是笔者猜测,仅供参考,有哪位小伙伴有更靠谱的答案欢迎在留言区回复。 64 | nominatedPodToNode map[ktypes.UID]string 65 | 66 | sync.RWMutex 67 | } 68 | 69 | // nominatedPodMap一共就三个核心函数,add、delete、和UpdateNominatedPod 70 | func (npm *nominatedPodMap) add(p *v1.Pod, nodeName string) { 71 | // 避免Pod已经存在先执行删除,确保同一个Pod不会存在两个实例,这个主要是为了nominatedPods考虑的,毕竟它的value是slice,没有去重能力。 72 | npm.delete(p) 73 | 74 | // NominatedNodeName()函数就是获取p.Status.NominatedNodeName, 75 | // 那么下面的代码的意思就是nodeName和p.Status.NominatedNodeName优先用nodeName,如果二者都没有指定则返回 76 | // 这里需要知道的是nodeName代表着最新的状态,所以需要优先,这一点在PodNominator应用章节会进一步说明 77 | nnn := nodeName 78 | if len(nnn) == 0 { 79 | nnn = NominatedNodeName(p) 80 | if len(nnn) == 0 { 81 | return 82 | } 83 | } 84 | // 下面的代码没什么难度,就是把Pod放到两个map中 85 | npm.nominatedPodToNode[p.UID] = nnn 86 | for _, np := range npm.nominatedPods[nnn] { 87 | if np.UID == p.UID { 88 | klog.V(4).Infof("Pod %v/%v already exists in the nominated map!", p.Namespace, p.Name) 89 | return 90 | } 91 | } 92 | npm.nominatedPods[nnn] = append(npm.nominatedPods[nnn], p) 93 | } 94 | 95 | func (npm *nominatedPodMap) delete(p *v1.Pod) { 96 | // 如果Pod不存在就返回 97 | nnn, ok := npm.nominatedPodToNode[p.UID] 98 | if !ok { 99 | return 100 | } 101 | // 然后从两个map中删除 102 | for i, np := range npm.nominatedPods[nnn] { 103 | if np.UID == p.UID { 104 | npm.nominatedPods[nnn] = append(npm.nominatedPods[nnn][:i], npm.nominatedPods[nnn][i+1:]...) 105 | if len(npm.nominatedPods[nnn]) == 0 { 106 | delete(npm.nominatedPods, nnn) 107 | } 108 | break 109 | } 110 | } 111 | delete(npm.nominatedPodToNode, p.UID) 112 | } 113 | 114 | func (npm *nominatedPodMap) UpdateNominatedPod(oldPod, newPod *v1.Pod) { 115 | npm.Lock() 116 | defer npm.Unlock() 117 | // 首选需要知道一个知识点,kube-scheduler什么时候会调用UpdateNominatedPod()?这个问题貌似应该是PodNominator应用章节的内容。 118 | // 为了便于理解下面的代码,笔者需要提前剧透一下,答案是在调度队列的更新接口中,感兴趣的同学可以回看《kube-scheduler的SchedulingQueue解析》的源码注释 119 | // 而调度队列的更新是kube-scheduler在watch apiserver的Pod的时候触发调用的,所以此时默认是没有提名Node的 120 | nodeName := "" 121 | // 有一些情况,在Pod刚好提名了Node之后收到了Pod的更新事件并且新Pod.Status.NominatedNodeName="",此时需要保留提名的Node。 122 | // 以下几种情况更新事件是不会保留提名的Node 123 | // 1.设置Status.NominatedNodeName:表现为NominatedNodeName(oldPod) == "" && NominatedNodeName(newPod) != "" 124 | // 2.更新Status.NominatedNodeName:表现为NominatedNodeName(oldPod) != "" && NominatedNodeName(newPod) != "" 125 | // 3.删除Status.NominatedNodeName:表现为NominatedNodeName(oldPod) != "" && NominatedNodeName(newPod) == "" 126 | if NominatedNodeName(oldPod) == "" && NominatedNodeName(newPod) == "" { 127 | if nnn, ok := npm.nominatedPodToNode[oldPod.UID]; ok { 128 | // 这是唯一一种情况保留提名 129 | nodeName = nnn 130 | } 131 | } 132 | // 无论提名的Node名字是否修改都需要更新,因为需要确保Pod的指针也被更新 133 | npm.delete(oldPod) 134 | npm.add(newPod, nodeName) 135 | } 136 | ``` 137 | 138 | 除了更新保留原有提名Node部分稍微有点复杂,整个PodNominator的实现可以说简单到不能再简单,甚至可以毫不夸张的说,在看到PodNominator的定义的时候就可以想象得到它的实现了。虽然PodNominator看似非常简单,但是很多复杂的功能就是又这些简单的模块组建而成。PodNominator是kube-scheduler实现抢占调度的关键所在,此时是不是感觉他就没那么简单了呢?现在笔者就带着大家看看PodNominator在kube-scheduler是如何应用,进而实现抢占调度的。 139 | 140 | # PodNominator应用 141 | 142 | 在kube-scheduler中,有两个类型直接继承了PodNominator,它们分别是SchedulingQueue和PreemptHandle。前者笔者在[《SchedulingQueue》](./SchedulingQueue.md)文章中已经提到了,但是直接忽略了,所以本文算是调度队列的一个延续;PreemptHandle是抢占句柄,本文不会过多介绍,还是老套路,留给后续文章解析,但是从类型名字可以知道是用来实现抢占的。所以说PodNominator与抢占调度是有关系的。 143 | 144 | 虽然说SchedulingQueue和PreemptHandle都继承了PodNominator,但是他们都指向了同一个对象(这也是golang语言的特点,指针继承),所以kube-scheduler中只有一个PodNominator对象。这也是nominatedPodMap加锁的一个原因,因为有多协程并发访问的需求。 145 | 146 | ## PodNominator在调度队列中的应用 147 | 148 | 调度队列有三种情况会调用PodNominator.AddNominatedPod(): 149 | 150 | 1. PriorityQueue.Add():源码链接,此处的目的是将添加的Pod如果提名了Node,那就添加到PodNominator中。笔者认为正常的逻辑可能不会出现这种情况,因为一个新建的Pod何来提名?还记得SharedInformer的ResyncPeriod么?SharedInformer会定时全量同步一次,此时的事件是Add,以上仅是笔者的猜测。 151 | 2. PriorityQueue.AddUnschedulableIfNotPresent():源码链接,此处的目的是恢复Pod的提名状态,因为该函数会将Pod加入到unschedulableQ或者backoffQ,均为不可调度状态,原有的提名需要删除,恢复到Pod.Status.NominatedNodeName。 152 | 3. PriorityQueue.Update():源码链接,此处的目的与PriorityQueue.Add()相同,因为更新的时候如果Pod不在任何子队列就当Add处理。 153 | 调度队列在PriorityQueue.Update()函数中如果Pod已经在队列中存在,会调用PodNominator。UpdateNominatedPod(),相应的代码笔者不再拷贝了,读者可以通过上面的连接找到。毕竟Pod已经更新了,相应的状态也需要更细难道PodNominator中,就是更新Pod的指针。同理,在PriorityQueue.Delete()函数中调用了PodNominator.DeleteNominatedPodIfExists(),笔者就不再解释了。 154 | 155 | ## PodNominator在调度器中的应用 156 | 157 | 当kube-scheduler需要通过抢占的方式为Pod提名某个Node时,此时的Pod仍然处于未调度状态,因为Pod需要等到Node上被抢占的Pod退出。所以此时对于Pod而言是调度失败的,也就是PodNominator.AddNominatedPod()出现在recordSchedulingFailure()函数中的原因。代码链接: 158 | 159 | ```go 160 | func (sched *Scheduler) recordSchedulingFailure(fwk framework.Framework, podInfo *framework.QueuedPodInfo, err error, reason string, nominatedNode string) { 161 | // sched.Error是Pod调度出错后的处理,即便是抢占成功并提名,依然是资源不满足错误,因为等待被抢占Pod退出 162 | // 这个函数会调用PriorityQueue.AddUnschedulableIfNoPresent()函数将Pod放入不可调度子队列 163 | sched.Error(podInfo, err) 164 | 165 | // Update the scheduling queue with the nominated pod information. Without 166 | // this, there would be a race condition between the next scheduling cycle 167 | // and the time the scheduler receives a Pod Update for the nominated pod. 168 | // Here we check for nil only for tests. 169 | if sched.SchedulingQueue != nil { 170 | sched.SchedulingQueue.AddNominatedPod(podInfo.Pod, nominatedNode) 171 | } 172 | 173 | ...... 174 | } 175 | ``` 176 | 177 | 而在调度器中调用recordSchedulingFailure()函数传入有效Node名字(非"")的地方只有,表示抢占成功,其他的地方都是传入空的字符串,表示调度失败。本文不对调度器做过多分析,因为会有专门的文章分析,感兴趣的同学可以自行分析源码,当然也可以等待笔者的相关文章发布。笔者此处简单描述一下调度器抢占的原理:当没有任何一个Node满足Pod的需求的时候,调度器开始抢占Pod优先级低的Pod,如果抢占成功则提名被抢占Pod所在的Node,然后再将Pod放入不可调度队列,等待被抢占Pod退出。 178 | 179 | 当然,放入不可调度队列的Pod过一段时间还会被重新放入activeQ,此时如果有满足Pod需求的Node,kube-scheduler会将Pod调度到该Node上,然后清除Pod提名的Node。代码链接: 180 | 181 | ```go 182 | // 还记得《kube-scheduler的Cache解析》对于assume的解释么?如果忘记了请复习一遍 183 | func (sched *Scheduler) assume(assumed *v1.Pod, host string) error { 184 | // 设置Pod调度的Node 185 | assumed.Spec.NodeName = host 186 | // Cache中假定Pod已经调度到了Node上 187 | if err := sched.SchedulerCache.AssumePod(assumed); err != nil { 188 | klog.Errorf("scheduler cache AssumePod failed: %v", err) 189 | return err 190 | } 191 | // 已经假定Pod调度到了Node上,应该移除以前提名的Node了(如果有提名的Node) 192 | if sched.SchedulingQueue != nil { 193 | sched.SchedulingQueue.DeleteNominatedPodIfExists(assumed) 194 | } 195 | 196 | return nil 197 | } 198 | ``` 199 | 200 | Pod通过PodNominator提名Node就可以么?下一轮调度新的Pod如何确保不会被重复抢占(Pod1抢占了Pod0等待的过程中,Pod2优先级不高于Pod1但高于Pod0的情况下不抢占Pod0)?这就是PodNominator.NominatedPodsForNode()的作用了。调度器在遍历Node时都会把这部分资源累加到Node上,这样就避免重复抢占的问题,这也是kube-scheduler里面比较常见的一个名词"Reserve"(预留)。因为这部分代码相对比较复杂,笔者此处不做注释。 201 | 202 | 以上就是PodNominator在kube-scheduler中的应用,如果感觉知识点有点零散不系统,那么请看下文的总结。 203 | 204 | # 总结 205 | 206 | 1. PodNominator是kube-scheduler为了实现抢占调度定义的一种“提名”管理器,他记录了所有抢占成功但需要等被抢占Pod退出的Pod; 207 | 2. 通过PodNominator.NominatedPodsForNode(),kube-scheduler获取提名到指定Node上的所有Pod,在计算调度的时候这部分Pod申请的资源视为Node预留给抢占Pod的资源; 208 | 3. 当没有Node满足Pod的需求时,kube-scheduler开始执行抢占调度,如果抢占成功则利用PodNominator提名被抢占Pod所在的Node为Pod运行的Node; 209 | 4. 提名成功不代表已调度,所以此时Pod仍然是不可调度状态,放在调度队列的unschedulableQ子队列中; 210 | 5. 如果Pod从unschedulableQ迁移到activeQ,并且正好有Node满足Pod的需求,则Pod被调度到该Node上,并且删除以前提名的Node; 211 | 6. 此时再回来品一下“提名”,就是调度器预先在Node上为Pod预留一部分当前正在被别的Pod占用资源。此时再来看本文开始提出的问题,PodNominator有什么作用?我想笔者应该不用再做多余的解释了。 212 | 7. 最后,还有一个问题,为什么将PodNominator放在调度队列中实现?如果是笔者也会这么做,笔者的原因很简单:Pod虽然提名了Node,但是依然是未调度状态,所以放在调度队列中实现最合适。至于作者是不是这么考虑的就不知道了。 213 | -------------------------------------------------------------------------------- /kube-scheduler/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ![kube-scheduler](./kube-scheduler.png) 8 | 9 | 1. 利用SharedIndedInformer过滤未调度的Pod放入[调度队列](./SchedulingQueue.md) 10 | 2. 利用SharedIndexInformer过滤已调度的Pod更新[调度缓存](./Cache.md) 11 | 3. 从[调度队列](./SchedulingQueue.md)取出一个待调度的Pod,通过Pod.Spec.SchedulerName获取[调度框架](./Framework.md) 12 | 4. [调度框架](./Framework.md)是配置好的[调度插件](./Plugin.md)集合,既可以通过扩展[调度插件](./Plugin.md)的方式扩展调度能力,也可以通过[调度扩展器](./Extender.md) 13 | 5. [调度算法](./ScheduleAlgorithm.md)利用[调度缓存](./Cache.md)的快照以及输入的[调度框架](./Framework.md),为Pod选择最优的节点 14 | 6. 如果[调度算法](./ScheduleAlgorithm.md)执行失败,将Pod放入[调度队列](./SchedulingQueue.md)的不可调度自队列;如果[调度算法](./ScheduleAlgorithm.md)执行成功,通知[调度缓存](./Cache.md)假定调度Pod后异步绑定,绑定成功执行2 15 | 7. 其实[调度算法](./ScheduleAlgorithm.md)并不是执行失败就将Pod放入不可调度队列,而是通过[调度插件](./Plugin.md)执行抢占调度,抢占成功的Pod会被[提名](./PodNominator.md)并等待被强占调度Pod退出,只有抢占失败的Pod才会放入不可调度队列 16 | 8. 而且Pod假定调度完成后,不是立刻执行绑定,而是需要经过[调度插件](./Plugin.md)的ReservePlugin和PermitPlugin,如果PermitPlugin返回等待,则Pod需要[等待](./WaitingPods.md)直到批准或超时 17 | 9. 向SharedIndedInformer注册了Pod、Node、Service、CSINode、PV、PVC、StorageClass[事件处理函数](./EventHandlers.md) 18 | 10. 以上调度流程通过[调度器](./Scheduler.md)对象实现,而[调度器](./Scheduler.md)是通过[配置器](./Configurator.md)构造的,[配置器](./Configurator.md)的配置主要来自[配置API](./KubeSchedulerConfiguration.md) 19 | -------------------------------------------------------------------------------- /kube-scheduler/SchedulingQueue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/kube-scheduler/SchedulingQueue.png -------------------------------------------------------------------------------- /kube-scheduler/WaitingPods.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 前言 8 | 9 | 本文的等待Pod不是[调度队列](./SchedulingQueue.md)中等待调度的Pod,在调度队列中的Pod类型是PodInfo。本文的等待Pod是在[调度插件](./Plugin.md)中PermitPlugin返回等待的Pod,虽然当前没有PermitPlugin的实现,但这不影响我们理解kube-scheduler如何处理多个插件同时返回等待的情况。毕竟有任何插件返回拒绝,Pod就会返回调度队列,全部返回批准,Pod就会进入绑定周期,所以kube-scheduler需要管理至少一个PermitPlugin返回等待并且其他PermitPlugin都返回批准的Pod 10 | 11 | 本文引用kubernetes源码release-1.21分支。 12 | 13 | # WaitingPod 14 | 15 | WaitingPod是一个接口类型,源码链接: 16 | 17 | ```go 18 | type WaitingPod interface { 19 | // 获取等待Pod的API对象,很好理解,一个等待Pod应该继承v1.Pod 20 | GetPod() *v1.Pod 21 | // 获取哪些PermitPlugin插件需要Pod等待。 22 | GetPendingPlugins() []string 23 | // 名字为pluginName的插件批准了Pod,如果这是最后一个批准的插件,那么该Pod就可以进入绑定周期了 24 | Allow(pluginName string) 25 | // 拒绝等待的Pod,Pod需要返回调度队列 26 | Reject(msg string) 27 | } 28 | ``` 29 | 30 | ## WaitingPod实现 31 | 32 | WaitingPod的实现是waitingPod,源码链接: 33 | 34 | ```go 35 | type waitingPod struct { 36 | // Pod的API对象,WaitingPod.GetPod()直接返回'pod'就可以了。 37 | pod *v1.Pod 38 | // key是要求Pod等待的插件的名字,WaitingPod.GetPendingPlugins()就是把map的key转为slice。 39 | // value是定时器,即等待截止时间,至于超时后做什么见下文的构造函数 40 | pendingPlugins map[string]*time.Timer 41 | // 用于阻塞等待WaitingPod结果的协程,同时可以将结果返回给等待协程。所有的插件都批准或者任何待超时都会向s输出结果。 42 | s chan *framework.Status 43 | mu sync.RWMutex 44 | } 45 | ``` 46 | 47 | 来看看WaitingPod的构造函数,源码链接: 48 | 49 | ```go 50 | // newWaitingPod()是WaitingPod的构造函数。 51 | func newWaitingPod(pod *v1.Pod, pluginsMaxWaitTime map[string]time.Duration) *waitingPod { 52 | wp := &waitingPod{ 53 | pod: pod, 54 | // 缓冲设置为1非常好理解,它的效果等同于sync.Cond,生产者不会被阻塞,同时还能保证输出结果不丢失 55 | s: make(chan *framework.Status, 1), 56 | } 57 | 58 | wp.pendingPlugins = make(map[string]*time.Timer, len(pluginsMaxWaitTime)) 59 | 60 | wp.mu.Lock() 61 | defer wp.mu.Unlock() 62 | for k, v := range pluginsMaxWaitTime { 63 | // 根据插件返回的等待时间设置定时器,等待超时就拒绝等待的Pod 64 | plugin, waitTime := k, v 65 | wp.pendingPlugins[plugin] = time.AfterFunc(waitTime, func() { 66 | msg := fmt.Sprintf("rejected due to timeout after waiting %v at plugin %v", 67 | waitTime, plugin) 68 | wp.Reject(plugin, msg) 69 | }) 70 | } 71 | 72 | return wp 73 | } 74 | ``` 75 | 76 | 从waitingPod定义的成员变量很容易知道GetPod()和GetPendingPlugins()的实现,没有任何知识点。本文只解析一下Allow()和Reject()的实现,源码链接:。 77 | 78 | ```go 79 | // Allow()实现了WaitingPod.Allow()接口。 80 | func (w *waitingPod) Allow(pluginName string) { 81 | w.mu.Lock() 82 | defer w.mu.Unlock() 83 | // 如果指定插件存在,则停止计时器并删除它,因为该插件已经批准Pod了。 84 | if timer, exist := w.pendingPlugins[pluginName]; exist { 85 | timer.Stop() 86 | delete(w.pendingPlugins, pluginName) 87 | } 88 | 89 | // 如果还有插件需要等待则直接返回,即等待Pod的协程依然被阻塞 90 | if len(w.pendingPlugins) != 0 { 91 | return 92 | } 93 | 94 | // 输出等待结果,Success表示所有插件都批准了,即便此时没有协程接收s的结果(即获取该Pod等待结果)也没有问题,因为s的缓冲为1所以不会丢失结果。 95 | // 既然缓冲都是1了,为什么还有default这个case?因为Allow可能会与某个插件超时同时发生。 96 | // 也就是说Allow()和Reject()可能存在并发调用的可能,这会造成后调用者被阻塞。 97 | select { 98 | case w.s <- framework.NewStatus(framework.Success, ""): 99 | default: 100 | } 101 | } 102 | 103 | // Reject()实现了WaitingPod.Reject()接口。 104 | func (w *waitingPod) Reject(pluginName, msg string) { 105 | w.mu.RLock() 106 | defer w.mu.RUnlock() 107 | // 停止所有的插件定时器,因为这个Pod已经被拒绝了 108 | for _, timer := range w.pendingPlugins { 109 | timer.Stop() 110 | } 111 | 112 | // 输出等待结果,Unschedulable表示Pod不可调度,需要返回调度队列。 113 | select { 114 | case w.s <- framework.NewStatus(framework.Unschedulable, msg).WithFailedPlugin(pluginName): 115 | default: 116 | } 117 | } 118 | ``` 119 | 120 | # waitingPodsMap 121 | 122 | kube-scheduler中WaitingPod可能会很多,立刻能想到的方法就是用一个map管理起来,再加一个锁保护一下。嗯!这就是waitingPodsMap,。 123 | 124 | ```go 125 | // 注意:waitingPodsMap用Pod的UID做键,而不是NS+Name 126 | type waitingPodsMap struct { 127 | pods map[types.UID]*waitingPod 128 | mu sync.RWMutex 129 | } 130 | 131 | // 下面的代码是在没有任何注释的必要,放在这里省的读者点连接进去看了,内容不多,也占不了多少版面。 132 | func (m *waitingPodsMap) add(wp *waitingPod) { 133 | m.mu.Lock() 134 | defer m.mu.Unlock() 135 | m.pods[wp.GetPod().UID] = wp 136 | } 137 | 138 | func (m *waitingPodsMap) remove(uid types.UID) { 139 | m.mu.Lock() 140 | defer m.mu.Unlock() 141 | delete(m.pods, uid) 142 | } 143 | func (m *waitingPodsMap) get(uid types.UID) *waitingPod { 144 | m.mu.RLock() 145 | defer m.mu.RUnlock() 146 | return m.pods[uid] 147 | } 148 | 149 | func (m *waitingPodsMap) iterate(callback func(framework.WaitingPod)) { 150 | m.mu.RLock() 151 | defer m.mu.RUnlock() 152 | for _, v := range m.pods { 153 | callback(v) 154 | } 155 | } 156 | ``` 157 | 158 | # 总结 159 | 160 | 1. 只有WaitingPod的实现有点内容,waitingPodsMap基本可以忽略不计; 161 | 2. waitingPod用map\[string\]*time.Timer实现了多个PermitPlugin的等待,所有的PermitPlugin批准才能通过,任何time.Timer超时都会拒绝; 162 | 3. waitingPod通过一个结果(状态值)chan来阻塞获取Pod等待结果的协程并返回等待结果,其中chan的缓冲大小为1,既保证了生产者不会阻塞,同时保证了结果不丢失; 163 | -------------------------------------------------------------------------------- /kube-scheduler/kube-scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/kube-scheduler/kube-scheduler.png -------------------------------------------------------------------------------- /kube-scheduler/scheduling-framework-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/kube-scheduler/scheduling-framework-extensions.png -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之kubespray 2 | 3 | - [新建](cluster.md) 4 | - [扩容](scale.md) 5 | - [缩容](remove-node.md) 6 | - [升级](upgrade-cluster.md) 7 | - [删除](reset.md) 8 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/cluster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindezgm/k8s-src-analysis/74dc01a0024f54236d63ea853883710ae6156317/kubernetes-sigs/kubespary/cluster.jpg -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/cluster.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之新建集群 2 | 3 | ![新建集群](./cluster.jpg) 4 | 5 | 上图为kubespray:v2.18.0安装一个kubernetes集群的流程图(网络插件为clico),其中主轴上每个节点代表一个阶段,而且节点内容为需要执行该阶段内容的及节点范围,其中: 6 | 7 | - local为本机,即执行ansible-playbook命令的节点 8 | - bastion为堡垒机 9 | - k8s_cluster为kubernetes所有节点,包括kube_control_plane、worker和calico_rr节点 10 | - kube_control_plane为控制平面节点 11 | - etcd为etcd节点 12 | - calico_rr为calico rr节点 13 | - all是所有节点 14 | - `[i]`表示节点改分组的第i个节点,比如bastion[0]为第1个堡垒机 15 | 16 | 安装一个kubernetes的流程如下: 17 | 18 | 1. [校验ansible版本](./ansible_version.md),因为kubespray对于ansible的版本有一定的要求。 19 | 2. [堡垒机ssh配置](bastion-ssh-config/README.md),前提条件是配置了堡垒机,而且需要注意的是,堡垒机只有第一个节点有用。 20 | 3. [引导kubernetes与etcd节点](kubernetes/bootstrap-os/READMD.md),使得这些节点可以运行ansible模块,引导的主要工作为: 21 | 1. 配置包管理器(比如配置yum源),以安装必要的包 22 | 2. 安装Python 23 | 3. 安装Ansible的包管理器模块必要的包 24 | 4. 设置节点名字(hostname) 25 | 4. 搜集所有节点的信息。 26 | 5. 在kubernetes与etcd节点上执行[预安装](./kubernetes/preinstall/README.md)、[安装容器引擎](./container-engine/README.md)以及[下载](./download/README.md)必要的模块与镜像,因为安装kubernetes的组件有的是二进制,有的是镜像,所以在执行[下载](./download/README.md)之前需要提前安装容器引擎。 27 | 6. [安装etcd](./etcd/README.md) 28 | 7. 为每个kubernetes节点[生成etcd证书](./etcd/README.md#) 29 | 8. 在kubernetes节点[安装kubelet](./kubernetes/node/README.md) 30 | 9. [安装控制平面](./kubernetes/control-plane/README.md) 31 | 10. 在kubernetes节点[安装网络插件](./network_plugin/README.md) 32 | 11. [安装calico rr](./network_plugin/calico/rr/README.md) 33 | 12. 安装apps,比如ingress、rbd_provisioner、helm等。 34 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/container-engine/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之安装容器引擎 2 | 3 | 1. [安装runc](./runc/README.md) 4 | 2. [安装crictl](./crictl/README.md) 5 | 3. [安装nerdctl](./nerdctl/README.md) 6 | 4. [安装containerd](./containerd/README.md) -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/container-engine/containerd/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 通过部署学习Kubernetes之安装[containerd](https://github.com/containerd/) 3 | 4 | ## 前言 5 | 6 | 如果不知道为什么安装containerd,请先阅读[Kubernetes的容器运行时](https://kubernetes.io/docs/setup/production-environment/container-runtimes/)。 7 | 8 | 本文引用源码为kubespray:v2.18.0版本,因为kubespray支持多种类型操作系统,本文只针对CentOS做解析,其他发行版读者按需自行分析。 9 | 10 | ## 安装[containerd](https://github.com/containerd/) 11 | 12 | 源码链接: 13 | 14 | ```yaml 15 | --- 16 | # 检查操作系统发行版是否可以安装containerd 17 | - name: Fail containerd setup if distribution is not supported 18 | fail: 19 | msg: "{{ ansible_distribution }} is not supported by containerd." 20 | when: 21 | # 如果选择的操作系统发行版不在如下列表,需要自行添加到,否则无法安装 22 | - ansible_distribution not in ["CentOS", "OracleLinux", "RedHat", "Ubuntu", "Debian", "Fedora", "AlmaLinux", "Rocky", "Amazon", "Flatcar", "Flatcar Container Linux by Kinvolk", "Suse", "openSUSE Leap", "openSUSE Tumbleweed"] 23 | 24 | # 通过包管理器删除已安装的contrainerd,比如yum remove containerd。 25 | # kubespray以前的版本是通过包管理器安装containerd,当前版本是直接下载containerd的二进制包。 26 | # 安装新的containerd需要确保节点上没有其他方式安装的containerd 27 | - name: containerd | Remove any package manager controlled containerd package 28 | package: 29 | name: "{{ containerd_package }}" 30 | state: absent 31 | when: 32 | - not (is_ostree or (ansible_distribution == "Flatcar Container Linux by Kinvolk") or (ansible_distribution == "Flatcar")) 33 | 34 | # 删除containerd的yum源配置,当前版本已经不在使用包管理器安装containerd 35 | - name: containerd | Remove containerd repository 36 | file: 37 | path: "{{ yum_repo_dir }}/containerd.repo" 38 | state: absent 39 | when: 40 | - ansible_os_family in ['RedHat'] 41 | 42 | # 与CentOS无关 43 | - name: containerd | Remove containerd repository 44 | apt_repository: 45 | repo: "{{ item }}" 46 | state: absent 47 | with_items: "{{ containerd_repo_info.repos }}" 48 | when: ansible_pkg_mgr == 'apt' 49 | 50 | # 下载containerd二进制文件 51 | - name: containerd | Download containerd 52 | include_tasks: "../../../download/tasks/download_file.yml" 53 | vars: 54 | # 下载配置后文有解析 55 | download: "{{ download_defaults | combine(downloads.containerd) }}" 56 | 57 | # 解压缩containerd 58 | - name: containerd | Unpack containerd archive 59 | unarchive: 60 | src: "{{ downloads.containerd.dest }}" 61 | dest: "{{ containerd_bin_dir }}" 62 | mode: 0755 63 | remote_src: yes 64 | extra_opts: 65 | - --strip-components=1 66 | notify: restart containerd 67 | 68 | # containerd的运行目录可以配置,默认的运行目录是/usr/bin,如果配置的运行目录不是/usr/bin 69 | # 需要将安装在/usr/bin目录下的containerd程序删除 70 | - name: containerd | Remove orphaned binary 71 | file: 72 | path: "/usr/bin/{{ item }}" 73 | state: absent 74 | when: containerd_bin_dir != "/usr/bin" 75 | ignore_errors: true # noqa ignore-errors 76 | with_items: 77 | - containerd 78 | - containerd-shim 79 | - containerd-shim-runc-v1 80 | - containerd-shim-runc-v2 81 | - ctr 82 | 83 | # 生成containerd的服务 84 | - name: containerd | Generate systemd service for containerd 85 | template: 86 | src: containerd.service.j2 87 | dest: /etc/systemd/system/containerd.service 88 | mode: 0644 89 | notify: restart containerd 90 | 91 | # 确保containerd的目录存在 92 | - name: containerd | Ensure containerd directories exist 93 | file: 94 | dest: "{{ item }}" 95 | state: directory 96 | mode: 0755 97 | owner: root 98 | group: root 99 | with_items: 100 | # containerd服务路径,默认为/etc/systemd/system/containerd.service.d 101 | - "{{ containerd_systemd_dir }}" 102 | # containerd配置目录,默认为/etc/containerd 103 | - "{{ containerd_cfg_dir }}" 104 | # containerd存储镜像的目录,默认为/var/lib/containerd 105 | - "{{ containerd_storage_dir }}" 106 | # containerd的容器目录,默认为/run/containerd 107 | - "{{ containerd_state_dir }}" 108 | 109 | # http/https代理相关的本文不关注,默认http/https代理关闭即可 110 | - name: containerd | Write containerd proxy drop-in 111 | template: 112 | src: http-proxy.conf.j2 113 | dest: "{{ containerd_systemd_dir }}/http-proxy.conf" 114 | mode: 0644 115 | notify: restart containerd 116 | when: http_proxy is defined or https_proxy is defined 117 | 118 | # 复制containerd配置文件,一般生成配置的方法是containerd config default > /etc/containerd/config.toml 119 | # 然后再默认参数的基础上修改部分自定义参数 120 | - name: containerd | Copy containerd config file 121 | template: 122 | src: config.toml.j2 123 | dest: "{{ containerd_cfg_dir }}/config.toml" 124 | owner: "root" 125 | mode: 0640 126 | # 为了保证新的配置生效,需要重启containerd服务 127 | notify: restart containerd 128 | 129 | # you can sometimes end up in a state where everything is installed 130 | # but containerd was not started / enabled 131 | # 132 | - name: containerd | Flush handlers 133 | meta: flush_handlers 134 | 135 | # 确保containerd服务启动 136 | - name: containerd | Ensure containerd is started and enabled 137 | service: 138 | name: containerd 139 | enabled: yes 140 | state: started 141 | ``` 142 | 143 | ## 总结 144 | 145 | 安装containerd的流程如下: 146 | 147 | 1. 校验操作系统的发行版是否在指定的范围内; 148 | 2. 删除通过包管理器安装的containerd 149 | 3. 下载containerd; 150 | 4. 生成containerd服务; 151 | 5. 确保containerd服务运行; 152 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/container-engine/runc/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之安装[runc](https://github.com/opencontainers/runc) 2 | 3 | ## 前言 4 | 5 | [runc](https://github.com/opencontainers/runc)是一个CLI工具,[OCI](https://github.com/opencontainers)的一种实现,用于在Linux上生成和运行容器。containerd通过runc运行容器,所以在[安装containerd](../containerd/README.md)之前,需要安装runc。 6 | 7 | 本文引用源码为kubespray:v2.18.0版本,因为kubespray支持多种类型操作系统,本文只针对CentOS做解析,其他发行版读者按需自行分析。 8 | 9 | ## 安装[runc](https://github.com/opencontainers/runc) 10 | 11 | 安装runc就是下载runc的二进制文件到运行目录即可,而且没有任何配置文件,就是这么简单。源码链接: 12 | 13 | ```yaml 14 | --- 15 | # 与CentOS无关 16 | - name: runc | set is_ostree 17 | set_fact: 18 | is_ostree: "{{ ostree.stat.exists }}" 19 | 20 | # 通过包管理器删除runc,比如yum remove runc。 21 | # 因为以前的版本是通过包管理器安装runc,当前版本是直接下载二进制文件。 22 | # 此处是避免与其他方式安装的runc冲突,所以先删除通过包管理器安装的runc。 23 | - name: runc | Uninstall runc package managed by package manager 24 | package: 25 | name: "{{ runc_package_name }}" 26 | state: absent 27 | when: 28 | - not (is_ostree or (ansible_distribution == "Flatcar Container Linux by Kinvolk") or (ansible_distribution == "Flatcar")) 29 | 30 | # 下载runc二进制文件 31 | - name: runc | Download runc binary 32 | include_tasks: "../../../download/tasks/download_file.yml" 33 | vars: 34 | # 下载runc的配置后文有解析 35 | download: "{{ download_defaults | combine(downloads.runc) }}" 36 | 37 | # 将下载的runc二进制文件拷贝到运行目录(比如/usr/bin),并重命名成runc 38 | - name: Copy runc binary from download dir 39 | copy: 40 | src: "{{ downloads.runc.dest }}" 41 | dest: "{{ runc_bin_dir }}/runc" 42 | mode: 0755 43 | remote_src: true 44 | 45 | # 因为kubespray默认安装的runc目录为/usr/bin(绝大部分runc的安装路径),但是可以通过配置修改。 46 | # 如果/usr/bin目录下已经安装了runc,而配置的runc安装目录不是/usr/bin则需要删除已经安装的runc 47 | - name: runc | Remove orphaned binary 48 | file: 49 | path: /usr/bin/runc 50 | state: absent 51 | when: runc_bin_dir != "/usr/bin" 52 | ignore_errors: true # noqa ignore-errors 53 | ``` 54 | 55 | 接下来再看看runc的下载配置,源码链接: 56 | 57 | ```yaml 58 | downloads: 59 | runc: 60 | # 下载文件 61 | file: true 62 | # 只有容器引擎是containerd时才需要下载runc 63 | enabled: "{{ container_manager == 'containerd' }}" 64 | version: "{{ runc_version }}" 65 | # runc下载到指定的目录,默认为/tmp/releases 66 | dest: "{{ local_release_dir }}/runc" 67 | # runc二进制文件的校验和 68 | sha256: "{{ runc_binary_checksum }}" 69 | # runc下载url,默认为https://github.com/opencontainers/runc/releases/download/{{ runc_version }}/runc.{{ image_arch }} 70 | url: "{{ runc_download_url }}" 71 | # 不需要解压 72 | unarchive: false 73 | # 用户为root 74 | owner: "root" 75 | # 所有用户都可以读和运行,只有root可以写 76 | mode: "0755" 77 | # kubernetes所有节点都需要下载runc 78 | groups: 79 | - k8s_cluster 80 | ``` 81 | 82 | ## 总结 83 | 84 | 安装runc的流程如下: 85 | 86 | 1. 删除通过包管理器安装的runc 87 | 2. 下载runc 88 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/docs/downloads.md: -------------------------------------------------------------------------------- 1 | # 下载二进制文件和容器 2 | 3 | Kubespray支持多种下载/上传模式,默认模式为: 4 | 5 | * 每个节点自己下载二进制文件和容器镜像,即`download_run_once: False`; 6 | * 对于Kubernetes应用,拉取镜像策略是`k8s_image_pull_policy: IfNotPresent`。 7 | * 对于系统管理的容器,如kubelet或etcd,拉取镜像策略是`download_always_pull: False`,即只有需要的repo:tag或sha256哈希值与本地不同才拉取镜像。 8 | 9 | 还有一种“拉一次,推多次”模式: 10 | 11 | * 设置`download_run_once: True`,容器镜像和二进制文件kubespray只下载一次,然后将它们推送到集群所有节点。默认委托第一个控制平面节点执行下载。 12 | * 设置`download_localhost: True`不在委托第一个控制平面节点,变为自己下载,这在集群节点无法访问外部地址时非常有用。这需要在Ansible节点上安装并运行容器运行时,并且当前用户要么在docker组中,要么可以执行无密码sudo,以便能够使用容器运行时。注意:即使`download_localhost`为False,文件仍然会从委托的下载节点复制到Ansible节点(本地主机),然后从Ansible节点分发到所有集群节点。 13 | 14 | 注意:当`download_run_once`为true和`download_localhost`为false时,所有下载都将在委托节点(即第一个控制平面节点)上完成,包括该节点上不需要的容器镜像的下载。因此,该节点上所需的存储可能会比`download_run_once`为false时更多,因为所有镜像都将下载到该节点上容器运行时的存储中,而不仅仅是该节点所需的镜像。 15 | 16 | 关于缓存: 17 | 18 | * 当`download_run_once`是True,所有下载的文件都会缓存在本地`download_cache_dir`,默认是`/tmp/kubespray_cache`。 在随后的下载过程中,此本地缓存将成为节点下载文件和镜像的源,最大限度地减少公网带宽使用并缩短下载时间。预计在ansible节点上将使用大约800MB的磁盘空间用于缓存。kubernetes节点上的镜像缓存所需的磁盘空间与最大镜像所需的磁盘空间差不多,目前略小于150MB。 19 | * 默认情况下,如果`download_run_once`为False,kubespray不会将下载的镜像和文件从委托节点拷贝到本地缓存,也不会使用该缓存作为这些节点的下载源。如果您有容器镜像和文件的完整缓存,并且不需要下载任何内容,此时想要使用缓存,需要设置`download_force_cache`为True。 20 | * 为了节省磁盘空间,缓存镜像在使用后默认将从远程节点中删除(因为镜像在传输过程中需要压缩和解压缩,此处删除的是传输到远程节点的镜像压缩文件),设置`download_keep_remote_cache`可以保留文件。这在开发kubespray时很有用,因为这可以减少下载镜像的时间。因此,远程节点上镜像所需的存储空间将从150MB增加到大约550MB,这是目前所有所需容器镜像的总大小。 21 | 22 | 容器镜像和二进制文件通过一些变量描述,例如foo_version,foo_download_url,foo_checksum用于描述二进制文件,foo_image_repo, foo_image_tag或foo_digest_checksum(可选)用于描述容器。 23 | 24 | 容器镜像可以通过它的repo和tag来定义,例如:`andyshinn/dnsmasq:2.72`,或者通过repo和sha256:`andyshinn/dnsmasq@sha256:7c883354f6ea9876d176fe1d30132515478b2859d6fc0cbf9223ffdc09168193`。 25 | 26 | 注意,必须同时指定SHA256和镜像tag并相互对应。上面给定的示例由以下变量表示: 27 | 28 | ```yaml 29 | dnsmasq_digest_checksum: 7c883354f6ea9876d176fe1d30132515478b2859d6fc0cbf9223ffdc09168193 30 | dnsmasq_image_repo: andyshinn/dnsmasq 31 | dnsmasq_image_tag: '2.72' 32 | ``` 33 | 34 | 在download role默认值中可以找到变量的完整列表,还允许为二进制文件和容器镜像指定自定义url和本地存储库。 35 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/docs/offline-environment.md: -------------------------------------------------------------------------------- 1 | # 离线环境 2 | 3 | 如果服务器无法访问互联网(例如在有安全限制的本地部署时),需要: 4 | 5 | * 一个HTTP反向代理/缓存/镜像,用于提供静态文件(zip和二进制文件)服务 6 | * 一个内部Yum/Deb源 7 | * 一个提供kubespray所有容器镜像的内部仓库,容器镜像列表取决于具体的设置 8 | * [可选]一个用于kubespray所需python包的内部PyPi服务器(仅当操作系统不提供`requirements.txt`列出的所有python包/版本时才需要) 9 | * [可选]一个内部Helm仓库(仅在`helm_enabled=true`时才需要) 10 | 11 | ## 配置Inventory 12 | 13 | 一旦所有组件都可以从内网访问,接下来调整inventory中的以下变量以匹配离线环境: 14 | 15 | ```yaml 16 | # Registry overrides 17 | kube_image_repo: "{{ registry_host }}" 18 | gcr_image_repo: "{{ registry_host }}" 19 | docker_image_repo: "{{ registry_host }}" 20 | quay_image_repo: "{{ registry_host }}" 21 | 22 | kubeadm_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubeadm" 23 | kubectl_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubectl" 24 | kubelet_download_url: "{{ files_repo }}/kubernetes/{{ kube_version }}/kubelet" 25 | # etcd is optional if you **DON'T** use etcd_deployment=host 26 | etcd_download_url: "{{ files_repo }}/kubernetes/etcd/etcd-{{ etcd_version }}-linux-amd64.tar.gz" 27 | cni_download_url: "{{ files_repo }}/kubernetes/cni/cni-plugins-linux-{{ image_arch }}-{{ cni_version }}.tgz" 28 | crictl_download_url: "{{ files_repo }}/kubernetes/cri-tools/crictl-{{ crictl_version }}-{{ ansible_system | lower }}-{{ image_arch }}.tar.gz" 29 | # If using Calico 30 | calicoctl_download_url: "{{ files_repo }}/kubernetes/calico/{{ calico_ctl_version }}/calicoctl-linux-{{ image_arch }}" 31 | # If using Calico with kdd 32 | calico_crds_download_url: "{{ files_repo }}/kubernetes/calico/{{ calico_version }}.tar.gz" 33 | 34 | # CentOS/Redhat/AlmaLinux/Rocky Linux 35 | ## Docker / Containerd 36 | docker_rh_repo_base_url: "{{ yum_repo }}/docker-ce/$releasever/$basearch" 37 | docker_rh_repo_gpgkey: "{{ yum_repo }}/docker-ce/gpg" 38 | 39 | # Fedora 40 | ## Docker 41 | docker_fedora_repo_base_url: "{{ yum_repo }}/docker-ce/{{ ansible_distribution_major_version }}/{{ ansible_architecture }}" 42 | docker_fedora_repo_gpgkey: "{{ yum_repo }}/docker-ce/gpg" 43 | ## Containerd 44 | containerd_fedora_repo_base_url: "{{ yum_repo }}/containerd" 45 | containerd_fedora_repo_gpgkey: "{{ yum_repo }}/docker-ce/gpg" 46 | 47 | # Debian 48 | ## Docker 49 | docker_debian_repo_base_url: "{{ debian_repo }}/docker-ce" 50 | docker_debian_repo_gpgkey: "{{ debian_repo }}/docker-ce/gpg" 51 | ## Containerd 52 | containerd_debian_repo_base_url: "{{ ubuntu_repo }}/containerd" 53 | containerd_debian_repo_gpgkey: "{{ ubuntu_repo }}/containerd/gpg" 54 | containerd_debian_repo_repokey: 'YOURREPOKEY' 55 | 56 | # Ubuntu 57 | ## Docker 58 | docker_ubuntu_repo_base_url: "{{ ubuntu_repo }}/docker-ce" 59 | docker_ubuntu_repo_gpgkey: "{{ ubuntu_repo }}/docker-ce/gpg" 60 | ## Containerd 61 | containerd_ubuntu_repo_base_url: "{{ ubuntu_repo }}/containerd" 62 | containerd_ubuntu_repo_gpgkey: "{{ ubuntu_repo }}/containerd/gpg" 63 | containerd_ubuntu_repo_repokey: 'YOURREPOKEY' 64 | ``` 65 | 66 | 对于特定于操作系统只需定义与您的操作系统匹配的设置,如果使用上述设置,则需要在inventory中定义以下变量: 67 | 68 | * `registry_host`: 容器镜像仓库,如果`download/role/defaults`中定义的容器镜像使用不相同的仓库,则需要覆盖所有的*_image_repo。如果想让工作更轻松,请使用相同的镜像仓库,这样无需覆盖其他任何内容。 69 | * `files_repo`: 能够提供上述文件的HTTP网络服务器或反向代理。路径并不重要,可以将它们存储在任何地方,只要可以被kubespray访问。建议*_version在路径中使用,这样就无需在每次kubespray升级这些组件之一时修改此设置。 70 | * yum_repo/debian_repo/ubuntu_repo: 操作系统相关的包源,应该指向的内部源。 71 | 72 | ## 安装Kubespray Python包 73 | 74 | ### 推荐方式:Kubespray容器镜像 75 | 76 | 最简单的方法是使用kubespray容器镜像,因为所有需要的包都安装在镜像中,只需将容器镜像复制到私有仓库中即可! 77 | 78 | ### 手动安装 79 | 80 | 查看`requirements.txt`文件并检查您的操作系统是否提供了所有开箱即用的软件包(使用操作系统软件包管理器)。对于那些确实的包,需要使用具有Internet访问权限的代理或在内网中设置一个PyPi服务器来托管这些包。 81 | 82 | 如果使用HTTP(S)代理下载python包: 83 | 84 | ```bash 85 | sudo pip install --proxy=https://[username:password@]proxyserver:port -r requirements.txt 86 | ``` 87 | 88 | 使用内部PyPi服务器: 89 | 90 | ```bash 91 | # If you host all required packages 92 | pip install -i https://pypiserver/pypi -r requirements.txt 93 | 94 | # If you only need the ones missing from the OS package manager 95 | pip install -i https://pypiserver/pypi package_you_miss 96 | ``` 97 | 98 | ## 像往常一样运行Kubespray 99 | 100 | 一旦所有工件都到位并且您的inventory正确设置,您可以使用常规cluster.yaml命令运行kubespray: 101 | 102 | ```bash 103 | ansible-playbook -i inventory/my_airgap_cluster/hosts.yaml -b cluster.yml 104 | ``` 105 | 106 | 如果使用[Kubespray容器镜像](#推荐方式:Kubespray容器镜像),可以将inventory挂载到容器内: 107 | 108 | ```bash 109 | docker run --rm -it -v path_to_inventory/my_airgap_cluster:inventory/my_airgap_cluster myprivateregisry.com/kubespray/kubespray:v2.14.0 ansible-playbook -i inventory/my_airgap_cluster/hosts.yaml -b cluster.yml 110 | ``` 111 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/kubernetes/preinstall/0010-swapoff.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之关闭交换空间 2 | 3 | ## 前言 4 | 5 | Kubernetes从1.8版本之后必须关闭系统[交换空间](https://www.linux.com/news/all-about-linux-swap-space/),否则将不能工作(kubelet)。本文将通过kubespray源码分析如何关闭系统交换空间,更重要的是分析为什么要关闭交换空间。 6 | 7 | 本文引用源码为kubespray:v2.18.0版本,操作系统类型只针对CentOS做解析,其他发行版读者按需自行分析。 8 | 9 | ## 关闭系统[交换空间](https://www.linux.com/news/all-about-linux-swap-space/) 10 | 11 | 关闭系统[交换空间](https://www.linux.com/news/all-about-linux-swap-space/)是kubespray的一个[role](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html), 源码链接: 12 | 13 | ```yaml 14 | --- 15 | # 卸载(unmount)swap类型设备,同时删除/etc/fstab中配置的交换设备/文件 16 | # 这个操作是非常必要的,虽然后面会通过swapoff -a禁止交换,但没有这个操作,系统重启后交换空间还是会开启的 17 | - name: Remove swapfile from /etc/fstab 18 | mount: 19 | name: "{{ item }}" 20 | fstype: swap 21 | state: absent 22 | with_items: 23 | - swap 24 | - none 25 | 26 | # swapon -s可以列举正在使用的交换设备/文件的信息,如果开启了系统交换空间,swapon变量就会被赋值 27 | # swapon变量就可用来判断是否开启了系统交换空间,如果开启了,需要关闭它 28 | - name: check swap 29 | command: /sbin/swapon -s 30 | register: swapon 31 | changed_when: no 32 | 33 | # swapoff -a将/etc/fstab文件中所有设置为swap的设备禁止交换,关于swapoff后文有详细说明 34 | - name: Disable swap 35 | command: /sbin/swapoff -a 36 | when: 37 | # swapon是swapon -s的输出值,这个条件说明swapon -s有输出,也就是说明当前有使用的交换设备/文件 38 | - swapon.stdout 39 | # 开启swap时kubelet默认是报错的,如果非要强制开启swap,则需要配置kubelet,如何配置笔者会在部署kubelet的文章中说明。 40 | - kubelet_fail_swap_on | default(True) 41 | ignore_errors: "{{ ansible_check_mode }}" # noqa ignore-errors 42 | 43 | # Fedora不在本文解析范围 44 | - name: Disable swapOnZram for Fedora 45 | command: touch /etc/systemd/zram-generator.conf 46 | when: 47 | - swapon.stdout 48 | - ansible_distribution in ['Fedora'] 49 | - kubelet_fail_swap_on | default(True) 50 | ``` 51 | 52 | [swapoff](https://linux.die.net/man/8/swapoff)在指定的设备和文件上禁用swap,当使用-a参数时,在所有已知的交换设备和文件(如/proc/swaps或[/etc/fstab](https://www.redhat.com/sysadmin/etc-fstab)中的配置的)上禁用交换。 53 | 54 | ## 总结 55 | 56 | 关闭系统[交换空间](https://www.linux.com/news/all-about-linux-swap-space/)其实很简单:1)卸载交换设备并删除[/etc/fstab](https://www.redhat.com/sysadmin/etc-fstab)中的相关配置;2)通过swapoff -a在已知的交换设备/文件禁止swap。 57 | 58 | 那么问题来了,Kubernetes为什么要禁止交换空间呢?关于这个问题,很早就有人[开贴讨论](https://serverfault.com/questions/881517/why-disable-swap-on-kubernetes),github上也有相关的[issue](https://github.com/kubernetes/kubernetes/issues/53533)。笔者结合大家的讨论以及个人的理解,总结如下: 59 | 60 | 1. swap是让操作系统为应用程序提供了超出物理内存大小(比如10倍于物理内存)的虚拟内存,这在物理内存非常稀有的以前非常有用,当然现在依然非常有用。但是Kubernetes的思想是容器的CPU/内存应该固定在资源限制(容器资源的limit)范围内,所以根本不需要交换,交换反而会降低性能。 61 | 2. 有不少人认为关闭swap是Kubernetes团队的懒惰行为,一刀切的行为并不可取,非常典型的案例就是:应用虽然需要40GB的内存,但是大部分数据都很少访问,只有很少的数据是频繁访问的,开启swap可以让应用在16GB内存的主机中运行;关闭swap就必须在64GB的主机中运行,这会增加成本。 62 | 3. 这就是以前运行比较正常的服务,到了Kubernetes上后会OOM的原因,因为没有swap提供更大的虚拟内存。 63 | 4. 笔者更倾向于认为支持swap当前还不是Kubernetes的重点,但允许用户通过配置开启swap(参看kubelet的配置),当然后果需要自己承担,毕竟官方不建议开启。 64 | 65 | 因为对相关内容学习时间较短,以上总结可能不准确也不全面,随着笔者更深入的理解,会逐渐完善。 66 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/kubernetes/preinstall/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之Kubernets预安装 2 | 3 | 预安装是在安装Kubernetes之前对操作系统的配置以及ansible参数校验的过程,主要功能如下: 4 | 5 | 1. [swapoff](./0010-swapoff.md):关闭操作系统交换空间。 6 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/network_plugin/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之网络插件 2 | 3 | 1. [安装calico](./calico/README.md) 4 | 2. [安装cni](./cni/README.md) 5 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/network_plugin/cni/README.md: -------------------------------------------------------------------------------- 1 | # 通过部署学习Kubernetes之安装cni 2 | 3 | ## 前言 4 | 5 | 关于CNI建议先阅读[Kubernetes网络插件](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)和[CNI规范](https://github.com/containernetworking/cni/blob/master/SPEC.md),对CNI有一定的了解后,再阅读本文会比较容易理解。 6 | 7 | 本文引用源码为kubespray:v2.18.0版本,因为kubespray支持多种类型操作系统,本文只针对CentOS做解析,其他发行版读者按需自行分析。 8 | 9 | ## 安装CNI 10 | 11 | CNI是容器网络接口,属于规范或协议类,理论上规范或者协议不存在安装一说,我们从代码上看一下安装CNI到底安装了什么?源码链接: 12 | 13 | ```yaml 14 | --- 15 | # 确保/opt/cni/bin文件夹存在,因为这是CNI插件二进制文件存放的目录。 16 | # 此处有个问题,为什么CNI二进制文件目录是hardcode在代码中,而不是使用变量? 17 | # 首先,/opt/cni/bin不是CNI规定的; 18 | # 其次,CNI二进制文件目录是kubelet传给容器运行时(比如containerd); 19 | # 再者,kubelet的CNI二进制文件目录默认值是/opt/cni/bin,可以通过--cni-bin-dir命令行参数修改 20 | # 最后,kubespray没有为CNI二进制文件目录设置变量,直接使用了kubelet的默认值,说明/opt/cni/bin得到的大家的共识 21 | - name: CNI | make sure /opt/cni/bin exists 22 | file: 23 | path: /opt/cni/bin 24 | state: directory 25 | mode: 0755 26 | owner: kube 27 | recurse: true 28 | 29 | # 拷贝CNI插件,CNI插件在执行下载阶段的时候就被下载到{{ local_release_dir }}目录(默认为/tmp/releases)。 30 | # 此时只需要将下载的压缩包解压到/opt/cni/bin目录即可。 31 | - name: CNI | Copy cni plugins 32 | unarchive: 33 | src: "{{ local_release_dir }}/cni-plugins-linux-{{ image_arch }}-{{ cni_version }}.tgz" 34 | dest: "/opt/cni/bin" 35 | mode: 0755 36 | remote_src: yes 37 | ``` 38 | 39 | ## 总结 40 | 41 | 1. CNI虽然是容器网络接口,但是CNI项目不仅维护接口规范,同时维护了一些[CNI插件](https://github.com/containernetworking/plugins),这些插件不仅可以作为实现CNI的参考,同时实现了绝大部分插件都必须要实现的部分功能(比如IP地址管理),这样就免去了开发网络插件的重复工作。 42 | 2. 安装CNI其实就是安装CNI项目维护的CNI插件。 43 | -------------------------------------------------------------------------------- /kubernetes-sigs/kubespary/offline-install.md: -------------------------------------------------------------------------------- 1 | # Kubespray离线安装Kubernetes的几种方法 2 | 3 | ## kubespray推荐的方法 4 | 5 | [kubespray离线安装kubernetes](./docs/offline-environment.md)需要在内网搭建文件服务器、镜像仓库、包源以及按需的PyPi服务器和Helm仓库,本质上与在线安装没什么区别,只是将下载文件、镜像、包的地址覆盖为内部地址。这种方法比较正规,适用于绝大部分场景。 6 | 7 | ## 使用cache 8 | 9 | 这是利用kubespray的[缓存机制](./docs/downloads#关于缓存)实现的离线安装方法。首先需要在一个可以访问互联网的环境配置`download_run_once=true`部署一个最小集群,从而在ansible节点获得所需的所有文件和镜像的缓存。然后再将这些缓存文件拷贝到内网ansible主机的指定目录,设置`download_force_cache=true`将所需文件、镜像分发到节点并安装。 10 | 11 | 该方法有如下缺点: 12 | 13 | 1. 缓存中没有需要的包,所以节点依赖的包需要单独安装; 14 | 2. 所有文件都是通过ansible节点分发到集群所有节点,ansible节点带宽有限,部署性能远低于[方法1](#kubespray推荐的方法); 15 | 16 | 使用cache的方法适用于安装小规模集群,比如演示集群、简单的测试集群等,单独为这些集群搭建文件服务器、镜像仓库、包源成本比较高。 17 | 18 | ## 制作镜像 19 | 20 | 这种方法适用于云厂商亦或是将kubernetes安装在虚拟机上的场景,将kubespray需要的所有文件、镜像下载到虚拟机镜像中(包也提前安装),然后用该镜像创建的虚拟机安装kubernetes集群。这种方法比[第一种方法](#kubespray推荐的方法)的部署速度还快,因为不需要下载任何文件或者镜像,非常适合部署较大规模的集群。但是该方法也存在如下缺点: 21 | 22 | 1. 按照节点角色单独制作镜像管理成本较高,制作统一的镜像镜像又比较大(因为需要提前下载所有的文件和镜像); 23 | 2. 需要下载的文件、镜像、包与具体的配置有关,静态的镜像显得不够灵活,满配的镜像又会比较大; 24 | 25 | 以上问题配合[第一种方法](#kubespray推荐的方法)也能达到适当的改良,即制作几个较大粒度且较为通用的虚拟机镜像,在安装过程中缺失的文件、镜像、包可以离线下载。需要注意的是,这种改良只适用于快速部署大规模的kubernetes集群。 26 | --------------------------------------------------------------------------------