├── .gitignore ├── README.md ├── apimachinery └── meta.md └── client-go ├── 1.store-indexer-cache.md ├── 2.queue-fifo-delta_fifo.md ├── 3.listwatch-reflector-controller.md ├── 4.informer.md ├── 5.lister.md ├── 6.sharedInformerFactory.md ├── 7.rateLimiter-workqueue.md ├── 8.customize-controller.md ├── 9.scheme-clientset-codegen.md ├── README.md ├── controller-runtime.md ├── how-to-create-a-kubernetes-custom-controller-using-client-go.md ├── image ├── 2019-01-25-23-48-04.png ├── 2019-01-25-23-49-26.png ├── 2019-02-18-11-09-56.png └── 2019-03-06-18-42-40.png ├── kubernetes-deep-dive-code-generation-customresources.md └── 开发笔记.md /.gitignore: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-dev-docs 2 | 3 | Kubernetes 源码解析和实践 -------------------------------------------------------------------------------- /apimachinery/meta.md: -------------------------------------------------------------------------------- 1 | 2 | ``` go 3 | // 来源于 k8s.io/client-go/tools/cache/index.go 4 | // MetaNamespaceIndexFunc is a default index function that indexes based on an object's namespace 5 | func MetaNamespaceIndexFunc(obj interface{}) ([]string, error) { 6 | meta, err := meta.Accessor(obj) 7 | if err != nil { 8 | return []string{""}, fmt.Errorf("object has no meta: %v", err) 9 | } 10 | return []string{meta.GetNamespace()}, nil 11 | } 12 | ``` -------------------------------------------------------------------------------- /client-go/1.store-indexer-cache.md: -------------------------------------------------------------------------------- 1 | 2 | # Kubernetes 对象缓存和索引 3 | 4 | 5 | 6 | - [Kubernetes 对象缓存和索引](#kubernetes-对象缓存和索引) 7 | - [对象缓存 Store](#对象缓存-store) 8 | - [对象索引 Indexer](#对象索引-indexer) 9 | - [为对象生成索引值列表的 IndexFunc 和 IndexFunc 集合 Indexers](#为对象生成索引值列表的-indexfunc-和-indexfunc-集合-indexers) 10 | - [索引缓存 Index 和 Indices](#索引缓存-index-和-indices) 11 | - [为对象生成唯一标识 Key 的 KeyFunc](#为对象生成唯一标识-key-的-keyfunc) 12 | - [可并发访问的索引缓存 ThreadSafeStore](#可并发访问的索引缓存-threadsafestore) 13 | - [Add/Update() 方法](#addupdate-方法) 14 | - [updateIndices() 方法](#updateindices-方法) 15 | - [Delete() 方法](#delete-方法) 16 | - [Replace() 方法](#replace-方法) 17 | - [其它方法介绍](#其它方法介绍) 18 | - [使用 ThreadSafeStore 实现 Store/Indexer 接口的 cache](#使用-threadsafestore-实现-storeindexer-接口的-cache) 19 | - [使用 cache 的 DeltaFIFO 和 Informer](#使用-cache-的-deltafifo-和-informer) 20 | 21 | 22 | 23 | `Store` 是 K-V 类型的内存对象缓存。 24 | 25 | `Indexer` 在 `Store` 的基础上添加了对象索引,从而实现快速查找对象的功能。 26 | 27 | `IndexFunc` 类型的函数可以为对象生成索引列表。 28 | 29 | `cache` 实现了 `Store` 和 `Indexer` 接口,函数 `NewIndexer()` 和 `NewStore()` 返回 `cache` 类型的对象。 30 | 31 | `cache` 使用比较广泛,如各种 `Informer` 和 `DeltaFIFO` 用它做对象缓存和索引。 32 | 33 | ## 对象缓存 Store 34 | 35 | Store 是 KV 类型的对象缓存: 36 | 37 | ``` go 38 | // 来源于 k8s.io/client-go/tools/cache/store.go 39 | type Store interface { 40 | Add(obj interface{}) error 41 | Update(obj interface{}) error 42 | Delete(obj interface{}) error 43 | List() []interface{} 44 | ListKeys() []string 45 | Get(obj interface{}) (item interface{}, exists bool, err error) 46 | GetByKey(key string) (item interface{}, exists bool, err error) 47 | 48 | // 使用传入的对象列表替换 Store 中的对象 49 | Replace([]interface{}, string) error 50 | Resync() error 51 | } 52 | ``` 53 | `NewStore()` 函数返回一个实现该接口的 `struct cache` 类型对象(见后文分析)。 54 | 55 | `Queue` 接口是 `Store` 的超集,所以实现 `Queue` 接口的 `FIFO` 类型、`DeltaFIFO` 类型也实现了 `Store`接口。(详见: [2.queue-fifo-delta_fifo.md](./2.queue-fifo-delta_fifo.md)) 56 | 57 | ## 对象索引 Indexer 58 | 59 | Indexer 是在 Store 的基础上,添加了索引功能,方便快速获取(一批)对象。 60 | 61 | ``` go 62 | // 来源于 k8s.io/client-go/tools/cache/index.go 63 | type Indexer interface { 64 | // Index 实现了 Store 的接口 65 | Store 66 | // 返回注册的、名为 indexName 的索引函数 67 | Index(indexName string, obj interface{}) ([]interface{}, error) 68 | IndexKeys(indexName, indexKey string) ([]string, error) 69 | ListIndexFuncValues(indexName string) []string 70 | ByIndex(indexName, indexKey string) ([]interface{}, error) 71 | GetIndexers() Indexers 72 | 73 | AddIndexers(newIndexers Indexers) error 74 | } 75 | ``` 76 | 77 | `NewIndexer()` 函数返回一个实现该接口的 `struct cache` 类型对象(见后文分析)。 78 | 79 | ## 为对象生成索引值列表的 IndexFunc 和 IndexFunc 集合 Indexers 80 | 81 | 对象的索引是一个**字符串列表**,由 `IndexFunc` 类型的函数生成,所以索引值和对象是**一对多**关系。 82 | 83 | ``` go 84 | // 来源于 k8s.io/client-go/tools/cache/index.go 85 | type IndexFunc func(obj interface{}) ([]string, error) 86 | ``` 87 | 88 | client-go package 提供了名为 `NamespaceIndex` 的 IndexFunc 类型函数 `MetaNamespaceIndexFunc`,它提取对象的 `Namespace` 作为索引: 89 | 90 | ``` go 91 | // 来源于 k8s.io/client-go/tools/cache/index.go 92 | func MetaNamespaceIndexFunc(obj interface{}) ([]string, error) { 93 | meta, err := meta.Accessor(obj) 94 | if err != nil { 95 | return []string{""}, fmt.Errorf("object has no meta: %v", err) 96 | } 97 | return []string{meta.GetNamespace()}, nil 98 | } 99 | ``` 100 | 101 | 多个命名的 IndexFunc 类型函数用 `Indexers` 表示: 102 | 103 | ``` go 104 | // 来源于 k8s.io/client-go/tools/cache/index.go 105 | // map[索引函数名称]索引函数 106 | type Indexers map[string]IndexFunc 107 | ``` 108 | 109 | Indexer 接口的 `AddIndexers()` 方法为对象添加索引函数,从而为对象生成**不同类型**的索引,满足各种查找需求。 110 | 111 | 类似于 IndexFunc 为对象生成索引值列表,`KeyFunc` 函数(见后文)为对象生成一个唯一的标识字符串,称为对象 Key。 112 | 113 | ## 索引缓存 Index 和 Indices 114 | 115 | 前面说过,索引值和对象是一对多映射关系: 116 | 117 | + `Index`:保存索引匹配的对象集合(用它的唯一表示 Key 表示); 118 | + `Indices`:保存某个索引函数生成的所有索引的对象集合; 119 | 120 | ``` go 121 | // 来源于 k8s.io/client-go/tools/cache/index.go 122 | // map[索引字符串]set{对象 Key 集合} 123 | type Index map[string]sets.String 124 | 125 | // 对象的索引。map[索引函数名称]Index 126 | type Indices map[string]Index 127 | ``` 128 | 后文分析 cache 类型时将用到 Index 和 Indices。 129 | 130 | ## 为对象生成唯一标识 Key 的 KeyFunc 131 | 132 | ``` go 133 | // 来源于 k8s.io/client-go/tools/cache/store.go 134 | type KeyFunc func(obj interface{}) (string, error) 135 | ``` 136 | 137 | client-go package 提供了两个 KeyFunc 类型函数 `MetaNamespaceKeyFunc` 和 `DeletionHandlingMetaNamespaceKeyFunc` (实际用的最多): 138 | + `MetaNamespaceKeyFunc`:提取对象的 `/` 或 `` 作为 Key; 139 | + `DeletionHandlingMetaNamespaceKeyFunc`:先检查对象是不是 `DeletedFinalStateUnknown` 类型(见后文),如果是,直接返回该对象内的 Key 字段,否则调用 `MetaNamespaceKeyFunc` 函数生成 Key; 140 | 141 | ``` go 142 | // 来源于 k8s.io/client-go/tools/cache/store.go 143 | func MetaNamespaceKeyFunc(obj interface{}) (string, error) { 144 | // 如果对象是字符串,则直接使用它作为 Key 145 | if key, ok := obj.(ExplicitKey); ok { 146 | return string(key), nil 147 | } 148 | // 否则提取对象的 Meta 信息 149 | meta, err := meta.Accessor(obj) 150 | if err != nil { 151 | return "", fmt.Errorf("object has no meta: %v", err) 152 | } 153 | // 如果对象有 Namespace 则用 `/` 作为 key 154 | // 否则用 `` 作为 key 155 | if len(meta.GetNamespace()) > 0 { 156 | return meta.GetNamespace() + "/" + meta.GetName(), nil 157 | } 158 | return meta.GetName(), nil 159 | } 160 | 161 | // 来源于 k8s.io/client-go/tools/cache/controller.go 162 | func DeletionHandlingMetaNamespaceKeyFunc(obj interface{}) (string, error) { 163 | // DeletedFinalStateUnknown 封装了删除对象 Key 和对象自身的类型,由 DeltaFIFO.Replace() 方法产生 164 | if d, ok := obj.(DeletedFinalStateUnknown); ok { 165 | return d.Key, nil 166 | } 167 | return MetaNamespaceKeyFunc(obj) 168 | } 169 | ``` 170 | 171 | 与 `MetaNamespaceKeyFunc()` 功能相反的是 `SplitMetaNamespaceKey()` 函数,它将传入的 Key 分解,返回对象所在的命名空间和对象名称。 172 | 173 | 使用 KeyFunc 比较多的场景是创建 Store、Index 和 DeltaFIFO,如: 174 | 175 | ``` go 176 | clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) 177 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, clientState) 178 | ``` 179 | 180 | 因为 DeltaFIFO 接收的是从 apiserver List/Watch 的 K8S 资源对象,所以可以用 MetaNamespaceKeyFunc 提取它的 NS 和 Name。 181 | 182 | 但是 NewIndexer() 返回的 clientState 一般是 Reflector 从 DeltaFIFO 弹出的 Deltas,它是一个 Delta 的列表,而 Delta.Object 可能是 K8S 资源对象,也可能是 DeletedFinalStateUnknown 类型对象。所以 NewIndexer() 使用能区分这两种类型的 DeletionHandlingMetaNamespaceKeyFunc KeyFunc。 183 | 184 | ## 可并发访问的索引缓存 ThreadSafeStore 185 | 186 | `ThreadSafeStore` 通过锁机制,实现多 goroutine 可以并发访问的、带有索引功能(`Indexer` 接口)的对象缓存(`Store` 接口)。 187 | 188 | `ThreadSafeStore` 本身没有实现 `Indexer` 和 `Store` 接口,但包含同名方法,后文会介绍,`struct cache` 类型内部使用 `ThreadSafeStore` 实现了 `Indexer` 和 `Store` 接口。 189 | 190 | ``` go 191 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 192 | type ThreadSafeStore interface { 193 | // 下面这些方法和 Store 接口方法同名,差别在于多了唯一标识对象的 key 参数 194 | // 相比 Store 接口,缺少了 GetByKey() 方法 195 | Add(key string, obj interface{}) 196 | Update(key string, obj interface{}) 197 | Delete(key string) 198 | List() []interface{} 199 | ListKeys() []string 200 | Get(key string) (item interface{}, exists bool) 201 | 202 | Replace(map[string]interface{}, string) 203 | Resync() error 204 | 205 | // 下面这些是 Indexer 定义的接口方法 206 | Index(indexName string, obj interface{}) ([]interface{}, error) 207 | IndexKeys(indexName, indexKey string) ([]string, error) 208 | ListIndexFuncValues(name string) []string 209 | ByIndex(indexName, indexKey string) ([]interface{}, error) 210 | GetIndexers() Indexers 211 | AddIndexers(newIndexers Indexers) error 212 | } 213 | ``` 214 | 215 | `NewThreadSafeStore()` 函数返回一个实现该接口的对象,类型是 `threadSafeMap`: 216 | 217 | ``` go 218 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 219 | func NewThreadSafeStore(indexers Indexers, indices Indices) ThreadSafeStore { 220 | return &threadSafeMap{ 221 | items: map[string]interface{}{}, 222 | indexers: indexers, 223 | indices: indices, 224 | } 225 | } 226 | ``` 227 | 228 | `threadSafeMap` 使用 map 缓存所有对象: 229 | 230 | ``` go 231 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 232 | // threadSafeMap implements ThreadSafeStore 233 | type threadSafeMap struct { 234 | lock sync.RWMutex 235 | // 对象缓存。使用对象的 Key 作为 map key; 236 | items map[string]interface{} 237 | // 命名的索引函数集合。map[索引函数名称]索引函数 238 | indexers Indexers 239 | // 对象的索引缓存。map[索引函数名称][索引字符串]set{对象 Key 集合} 240 | indices Indices 241 | } 242 | ``` 243 | 244 | ### Add/Update() 方法 245 | 246 | ``` go 247 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 248 | func (c *threadSafeMap) Add(key string, obj interface{}) { 249 | c.lock.Lock() 250 | defer c.lock.Unlock() 251 | oldObject := c.items[key] 252 | c.items[key] = obj 253 | c.updateIndices(oldObject, obj, key) 254 | } 255 | 256 | func (c *threadSafeMap) Update(key string, obj interface{}) { 257 | c.lock.Lock() 258 | defer c.lock.Unlock() 259 | oldObject := c.items[key] 260 | c.items[key] = obj 261 | c.updateIndices(oldObject, obj, key) 262 | } 263 | ``` 264 | 265 | 当 `Add()/Update()` 一个对象时,先将它加到缓存,然后用 `updateIndices()` 方法更新索引。 266 | 267 | ### updateIndices() 方法 268 | 269 | `updateIndices()` 方法使用 `c.indexers` 中的索引函数,为对象创建**多种类型**索引值列表,然后将这些索引及对象的 Key 更新到索引缓存中(`c.indices`)。 270 | 271 | ``` go 272 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 273 | func (c *threadSafeMap) updateIndices(oldObj interface{}, newObj interface{}, key string) { 274 | // 从索引中移除老的 obj 275 | if oldObj != nil { 276 | c.deleteFromIndices(oldObj, key) 277 | } 278 | // 遍历 c.indexers 中的索引函数,为对象生成不同类型的索引列表 279 | for name, indexFunc := range c.indexers { 280 | indexValues, err := indexFunc(newObj) 281 | if err != nil { 282 | panic(fmt.Errorf("unable to calculate an index entry for key %q on index %q: %v", key, name, err)) 283 | } 284 | // 获取当前索引函数创建的索引 285 | index := c.indices[name] 286 | if index == nil { 287 | index = Index{} 288 | c.indices[name] = index 289 | } 290 | // 将所有索引值和对象 Key 更新到索引缓存 c.indices 中 291 | for _, indexValue := range indexValues { 292 | set := index[indexValue] 293 | if set == nil { 294 | set = sets.String{} 295 | index[indexValue] = set 296 | } 297 | set.Insert(key) 298 | } 299 | } 300 | } 301 | ``` 302 | 303 | ### Delete() 方法 304 | 305 | `Delete()` 方法先后从索引缓存和对象缓存中删除对象。`deleteFromIndices()` 方法遍历 `c.indexers` 中的索引函数,为对象计算索引值列表,然后再从索引缓存和对象缓存中删除该对象: 306 | 307 | ``` go 308 | // 来源于 k8s.io/client-go/tools/cache/thread_safe_store.go 309 | func (c *threadSafeMap) Delete(key string) { 310 | c.lock.Lock() 311 | defer c.lock.Unlock() 312 | if obj, exists := c.items[key]; exists { 313 | // 从索引缓存中删除对象 314 | c.deleteFromIndices(obj, key) 315 | // 从对象缓存中删除对象 316 | delete(c.items, key) 317 | } 318 | } 319 | ``` 320 | 321 | 注意:一个索引值可能匹配多个对象,所以不能直接删除索引缓存中索引值对应的对象集合。 322 | 323 | ### Replace() 方法 324 | 325 | `Replace()` 方法使用传入的对象列表替换内部缓存,然后重建索引: 326 | 327 | ``` go 328 | func (c *threadSafeMap) Replace(items map[string]interface{}, resourceVersion string) { 329 | c.lock.Lock() 330 | defer c.lock.Unlock() 331 | c.items = items 332 | 333 | // rebuild any index 334 | c.indices = Indices{} 335 | for key, item := range c.items { 336 | c.updateIndices(nil, item, key) 337 | } 338 | } 339 | ``` 340 | 341 | ### 其它方法介绍 342 | 343 | 1. `Index(indexName string, obj interface{}) ([]interface{}, error)` 344 | 345 | indexName 为索引函数名称(下同)。使用对应的索引函数为对象生成索引值列表,然后查询索引缓存,返回匹配这些索引值的对象列表(去重)。 346 | 347 | 2. `ByIndex(indexName, indexKey string) ([]interface{}, error)` 348 | 349 | indexKey 为索引值,查询索引缓存,返回它匹配的**对象列表**; 350 | 351 | 3. `IndexKeys(indexName, indexKey string) ([]string, error)` 352 | 353 | indexKey 为索引值,查询索引缓存,返回它匹配的**对象 Key 列表**; 354 | 355 | 4. `ListIndexFuncValues(indexName string) []string` 356 | 357 | 查询索引缓存,返回 indexName 对应的索引函数创建的所有索引包含的对象 Key 列表; 358 | 359 | 5. `GetIndexers() Indexers` 360 | 361 | 返回命名的索引函数集合 `c.indexers` 362 | 363 | 6. `AddIndexers(newIndexers Indexers) error` 364 | 365 | 将 newIndexers 中的命名函数添加到索引函数集合 c.indexers 中。 366 | 367 | 必须**在添加任何对象前调用该方法**,否则会出错返回。 368 | 369 | newIndexers 中的命名函数不能与 c.indexers 中已有的函数重名,否则出错返回。 370 | 371 | 7. `Resync() error` 372 | 373 | 直接返回。因为 Add/Update/Delete/Replace 方法都会同时更新缓存和索引,两者时刻是同步的。 374 | 375 | ## 使用 ThreadSafeStore 实现 Store/Indexer 接口的 cache 376 | 377 | 在分析 `ThreadSafeStore` 接口时提到过,它的方法如 `Add/Update/Delete/Get()` 都**需要传入**对象的 Key。 378 | 而 `cache` 则封装了 `ThreadSafeStore` 和 `KeyFunc`,后者为添加到 `ThreadSafeStore` 的对象生成 Key: 379 | 380 | ``` go 381 | // 来源于 k8s.io/client-go/tools/cache/store.go 382 | type cache struct { 383 | // cacheStorage bears the burden of thread safety for the cache 384 | cacheStorage ThreadSafeStore 385 | // keyFunc is used to make the key for objects stored in and retrieved from items, and 386 | // should be deterministic. 387 | keyFunc KeyFunc 388 | } 389 | ``` 390 | 391 | `cache` 实现了 `Indexer` 和 `Store` 接口,函数 `NewIndexer()` 和 `NewStore()` 均返回该类型的对象: 392 | 393 | ``` go 394 | // 来源于 k8s.io/client-go/tools/cache/store.go 395 | func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer { 396 | return &cache{ 397 | cacheStorage: NewThreadSafeStore(indexers, Indices{}), 398 | keyFunc: keyFunc, 399 | } 400 | } 401 | 402 | // 来源于 k8s.io/client-go/tools/cache/store.go 403 | func NewStore(keyFunc KeyFunc) Store { 404 | return &cache{ 405 | cacheStorage: NewThreadSafeStore(Indexers{}, Indices{}), 406 | keyFunc: keyFunc, 407 | } 408 | } 409 | ``` 410 | 411 | ## 使用 cache 的 DeltaFIFO 和 Informer 412 | 413 | 后文会介绍,DeltaFIFO 使用 NewStore() 或 NewIndexer() 返回的 cache 作为 knownObjects 对象缓存,而且传给这两个函数的 KeyFunc 一般是 `DeletionHandlingMetaNamespaceKeyFunc`: 414 | 415 | 1. `NewInformer` 函数: 416 | 417 | ``` go 418 | // 来源于 k8s.io/client-go/tools/cache/controller.go 419 | func NewInformer( 420 | lw ListerWatcher, 421 | objType runtime.Object, 422 | resyncPeriod time.Duration, 423 | h ResourceEventHandler, 424 | ) (Store, Controller) { 425 | // This will hold the client state, as we know it. 426 | clientState := NewStore(DeletionHandlingMetaNamespaceKeyFunc) 427 | 428 | return clientState, newInformer(lw, objType, resyncPeriod, h, clientState) 429 | } 430 | ``` 431 | 432 | 2. `NewIndexerInformer` 函数: 433 | 434 | ``` go 435 | // 来源于 k8s.io/client-go/tools/cache/controller.go 436 | func NewIndexerInformer( 437 | lw ListerWatcher, 438 | objType runtime.Object, 439 | resyncPeriod time.Duration, 440 | h ResourceEventHandler, 441 | indexers Indexers, 442 | ) (Indexer, Controller) { 443 | // This will hold the client state, as we know it. 444 | clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) 445 | 446 | return clientState, newInformer(lw, objType, resyncPeriod, h, clientState) 447 | } 448 | ``` 449 | 450 | 3. `NewSharedIndexInformer` 函数: 451 | 452 | ``` go 453 | // 来源于 k8s.io/client-go/tools/cache/shared_informer.go 454 | func NewSharedIndexInformer(lw ListerWatcher, objType runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers Indexers) SharedIndexInformer { 455 | realClock := &clock.RealClock{} 456 | sharedIndexInformer := &sharedIndexInformer{ 457 | processor: &sharedProcessor{clock: realClock}, 458 | 459 | indexer: NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers), 460 | 461 | listerWatcher: lw, 462 | objectType: objType, 463 | resyncCheckPeriod: defaultEventHandlerResyncPeriod, 464 | defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod, 465 | cacheMutationDetector: NewCacheMutationDetector(fmt.Sprintf("%T", objType)), 466 | clock: realClock, 467 | } 468 | return sharedIndexInformer 469 | } 470 | ``` -------------------------------------------------------------------------------- /client-go/2.queue-fifo-delta_fifo.md: -------------------------------------------------------------------------------- 1 | # Kubernetes 事件队列 2 | 3 | 4 | 5 | - [Kubernetes 事件队列](#kubernetes-事件队列) 6 | - [Queue 定义了队列接口](#queue-定义了队列接口) 7 | - [FIFO 是先入先出的队列](#fifo-是先入先出的队列) 8 | - [Add() 方法](#add-方法) 9 | - [Update() 方法](#update-方法) 10 | - [`Delete()` 方法](#delete-方法) 11 | - [Pop() 方法](#pop-方法) 12 | - [Replace() 方法](#replace-方法) 13 | - [HasSyncd() 方法](#hassyncd-方法) 14 | - [Resync() 方法](#resync-方法) 15 | - [DeltaFIFO 是记录对象历史事件的队列](#deltafifo-是记录对象历史事件的队列) 16 | - [DeltaFIFO 的生产者和消费者](#deltafifo-的生产者和消费者) 17 | - [记录对象事件的 Delta、Deltas 和 DeletedFinalStateUnknown 类型](#记录对象事件的-deltadeltas-和-deletedfinalstateunknown-类型) 18 | - [Add() 和 Update() 方法](#add-和-update-方法) 19 | - [queueActionLocked() 方法](#queueactionlocked-方法) 20 | - [Delete() 方法](#delete-方法) 21 | - [Get/GetByKey/List/ListKeys() 方法](#getgetbykeylistlistkeys-方法) 22 | - [Replace() 方法](#replace-方法-1) 23 | - [Resync() 方法](#resync-方法-1) 24 | - [Pop() 方法](#pop-方法-1) 25 | - [HasSyncd() 方法](#hassyncd-方法-1) 26 | - [DeltaFIFO 和 knownObjects 对象缓存的同步](#deltafifo-和-knownobjects-对象缓存的同步) 27 | 28 | 29 | 30 | `Queue` 接口是在 `Store` 的基础上添加了 `Pop()` 方法。 31 | 32 | `FIFO` 和 `DeltaFIFO` 类型(非接口)实现了 `Queue` 接口。 33 | 34 | `DeltaFIFO` 是 Kubernetes 中非常重要的数据结构,用于保存对象的变化事件。 35 | 36 | ## Queue 定义了队列接口 37 | 38 | `Queue` 是在对象缓存的基础上,添加了 `Pop()` 方法,这样既能缓存对象、按照 Key 查找对象、也能按序(Add 的顺序)弹出对象: 39 | 40 | ``` go 41 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 42 | type Queue interface { 43 | // 对象缓存接口 44 | Store 45 | 46 | // 弹出队列中的一个对象,如果队列为空,则一直阻塞。返回处理后的对象,以及处理结果。 47 | Pop(PopProcessFunc) (interface{}, error) 48 | 49 | AddIfNotPresent(interface{}) error 50 | 51 | // 当队列中第一批对象都弹出后,返回 true。 52 | HasSynced() bool 53 | Close() 54 | } 55 | ``` 56 | 57 | ## FIFO 是先入先出的队列 58 | 59 | FIFO (struct 类型,非接口)实现了 Queue 接口,只缓存对象的**一个最新值**,例如,队列中对象 A 的值为 a1,在被弹出前,进行两次更新,值分别为 a2, a3,则只会弹出一次且值为 a3。 60 | 61 | FIFO 适用的情况: 62 | 63 | 1. 你希望最多一个 worker 处理某个对象(对象在队列中是唯一的); 64 | 2. 当 worker 处理该对象时,对象值是最新的; 65 | 3. 你不需要处理删除的对象(删除的对象不会被弹出); 66 | 67 | FIFO 类型定义如下: 68 | 69 | ``` go 70 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 71 | type FIFO struct { 72 | lock sync.RWMutex 73 | cond sync.Cond 74 | 75 | // 对象缓存,用于快速查询。map key 为对象的 Key,该 Key 由 keyFunc 函数生成 76 | items map[string]interface{} 77 | 78 | // 对象弹出(Pop)顺序队列,队列中各对象 Key 是**唯一**的 79 | queue []string 80 | 81 | // 首先调用 Delete/Add/Update 或 Replace() 添加的第一批对象都 Pop 后为 true 82 | populated bool 83 | 84 | // Replace() 添加的第一批对象的数目 85 | initialPopulationCount int 86 | 87 | // 根据对象生成它的标识 Key 的函数 88 | keyFunc KeyFunc 89 | 90 | closed bool 91 | closedLock sync.Mutex 92 | } 93 | ``` 94 | 95 | 函数 `NewFIFO()` 返回 FIFO 类型对象,传入的 `KeyFunc` (一般是 `DeletionHandlingMetaNamespaceKeyFunc`) 用于生成对象 Key: 96 | 97 | ``` go 98 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 99 | func NewFIFO(keyFunc KeyFunc) *FIFO { 100 | f := &FIFO{ 101 | // 初始化对象缓存 102 | items: map[string]interface{}{}, 103 | // 初始化对象 Key 队列 104 | queue: []string{}, 105 | keyFunc: keyFunc, 106 | } 107 | f.cond.L = &f.lock 108 | return f 109 | } 110 | ``` 111 | 112 | ### Add() 方法 113 | 114 | 将对象更新到缓存(f.items),如果缓存中没有该对象,则将它加到弹出队列(f.queue),这样可以保证只会弹出对象一次,且弹出的是最新值。 115 | 116 | ``` go 117 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 118 | func (f *FIFO) Add(obj interface{}) error { 119 | id, err := f.keyFunc(obj) 120 | if err != nil { 121 | return KeyError{obj, err} 122 | } 123 | f.lock.Lock() 124 | defer f.lock.Unlock() 125 | f.populated = true 126 | 127 | // 缓存中没有该对象,则将它的 key 加到队列 f.queue 中 128 | if _, exists := f.items[id]; !exists { 129 | f.queue = append(f.queue, id) 130 | } 131 | // 更新对象缓存 132 | f.items[id] = obj 133 | f.cond.Broadcast() 134 | return nil 135 | } 136 | ``` 137 | 138 | 什么情况下缓存中没有该对象呢? 139 | 140 | 1. 第一次向 FIFO Add/Update 该对象; 141 | 2. 或者调用 FIFO 的 Delete 方法删除该对象; 142 | 3. 或者,该对象被 Pop 处理了; 143 | 4. 或者,调用 Replace 方法,用新的一组对象替换当前缓存 f.items 和队列 f.queue; 144 | 145 | ### Update() 方法 146 | 147 | 通过 `Add()` 方法实现: 148 | 149 | ``` go 150 | func (f *FIFO) Update(obj interface{}) error { 151 | return f.Add(obj) 152 | } 153 | ``` 154 | 155 | ### `Delete()` 方法 156 | 157 | 从缓存中删除对象: 158 | 159 | ``` go 160 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 161 | func (f *FIFO) Delete(obj interface{}) error { 162 | id, err := f.keyFunc(obj) 163 | if err != nil { 164 | return KeyError{obj, err} 165 | } 166 | f.lock.Lock() 167 | defer f.lock.Unlock() 168 | f.populated = true 169 | // 从缓存中删除对象,注意 f.queue 中还可能有对象的 Key 170 | delete(f.items, id) 171 | return err 172 | } 173 | ``` 174 | 175 | 注意:**没有**从弹出队列(f.queue) 中删除该对象 Key,后续弹出过程中会**忽略**这种已删除对象的 Key,继续弹出下一个对象。 176 | 177 | ### Pop() 方法 178 | 179 | 从弹出队列(f.queue)弹出一个对象,并调用用户注册的回调函数进行处理,返回处理后的对象和出错信息。 180 | 181 | 如果队列为空,则一直阻塞。 182 | 183 | 处理函数执行失败时应该返回 `ErrRequeue` 类型的错误,这时该对象会被**重新加回** FIFO,后续可以再次被弹出处理。 184 | 185 | `Pop()` 是在对 FIFO 加锁的情况下调用处理函数的,所以可以在多个 goroutine 中**并发调用** 该方法。 186 | 187 | ``` go 188 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 189 | func (f *FIFO) Pop(process PopProcessFunc) (interface{}, error) { 190 | f.lock.Lock() 191 | defer f.lock.Unlock() 192 | for { 193 | for len(f.queue) == 0 { 194 | if f.IsClosed() { 195 | return nil, FIFOClosedError 196 | } 197 | // 如果队列未关闭,但为空,则阻塞等待 198 | f.cond.Wait() 199 | } 200 | // 先从 queue 弹出对象 id 201 | id := f.queue[0] 202 | f.queue = f.queue[1:] 203 | if f.initialPopulationCount > 0 { 204 | f.initialPopulationCount-- 205 | } 206 | // 从缓存中获取对象 207 | item, ok := f.items[id] 208 | if !ok { 209 | // 前面提到,当 Add/Update 对象,在 Pop 前又 Delete 了该对象,就会出现 queue 中有 id,而 items 中无对象的情况 210 | // 由于对象已经被删除,所以跳过,Pop 下一个对象 211 | continue 212 | } 213 | // 从缓存中删除对象 214 | delete(f.items, id) 215 | // 调用处理函数,该函数处于 f.lock 锁保护中,可以并发执行 216 | err := process(item) 217 | // 如果处理 item 失败,应该返回 ErrRequeue 类型错误,再将对象加回队列 218 | if e, ok := err.(ErrRequeue); ok { 219 | f.addIfNotPresent(id, item) 220 | err = e.Err 221 | } 222 | return item, err 223 | } 224 | } 225 | ``` 226 | 227 | ### Replace() 方法 228 | 229 | 用传入的一组对象替换对象缓存 f.items 和弹出队列 f.queue: 230 | 231 | ``` go 232 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 233 | func (f *FIFO) Replace(list []interface{}, resourceVersion string) error { 234 | items := make(map[string]interface{}, len(list)) 235 | for _, item := range list { 236 | key, err := f.keyFunc(item) 237 | if err != nil { 238 | return KeyError{item, err} 239 | } 240 | items[key] = item 241 | } 242 | 243 | f.lock.Lock() 244 | defer f.lock.Unlock() 245 | 246 | if !f.populated { 247 | f.populated = true 248 | f.initialPopulationCount = len(items) 249 | } 250 | 251 | f.items = items 252 | f.queue = f.queue[:0] 253 | for id := range items { 254 | f.queue = append(f.queue, id) 255 | } 256 | if len(f.queue) > 0 { 257 | f.cond.Broadcast() 258 | } 259 | return nil 260 | } 261 | ``` 262 | 263 | ### HasSyncd() 方法 264 | 265 | 参考后文对 DeltaFIFO 的 `HasSyncd()` 方法分析。 266 | 267 | ### Resync() 方法 268 | 269 | 将对象缓存 `f.items` 中的对象都更新到弹出队列 `f.queue` 中: 270 | 271 | ``` go 272 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 273 | func (f *FIFO) Resync() error { 274 | f.lock.Lock() 275 | defer f.lock.Unlock() 276 | 277 | inQueue := sets.NewString() 278 | for _, id := range f.queue { 279 | inQueue.Insert(id) 280 | } 281 | for id := range f.items { 282 | if !inQueue.Has(id) { 283 | f.queue = append(f.queue, id) 284 | } 285 | } 286 | if len(f.queue) > 0 { 287 | f.cond.Broadcast() 288 | } 289 | return nil 290 | } 291 | ``` 292 | 293 | **FIXME!!!**:对象加入和弹出时都会同时更新 f.items 和 f.queue,按说是完全一致的,所以 `Resync()` 方法是多余的? 294 | 295 | ## DeltaFIFO 是记录对象历史事件的队列 296 | 297 | `DeltaFIFO` 与 `FIFO` 类型的区别: 298 | 299 | 1. DeltaFIFO 缓存对象的事件列表,而 FIFO 缓存对象的最新值; 300 | 2. FIFO 内部维护了一个对象缓存(f.items),而 DeltaFIFO 需要和一个外部维护的、包含所有对象的缓存(knownObjects) 结合使用: 301 | + Delete():用 knownObjects 检查待删除的对象是否存在,如果不存在则直接返回,否则生成 Deleted 事件; 302 | + Replace():用传入的队列列表更新 DeltaFIFO,检查 knownObjects,为不在传入的对象列表中的对象生成 Deleted 事件; 303 | + Resync():将 knownObjects 中的对象同步到 DeltaFIFO 中,并生成 Sync 事件; 304 | 3. Delete/Replace/Resync() 方法不会从 DeltaFIFO 删除/替换对象,而是生成对应的事件。DeltaFIFO 的消费者需要将他们从 knownObjects 删除(见后文); 305 | 4. DeltaFIFO 的 Pop/Get() 方法,返回的不是对象最新值,而是对象事件列表。 306 | 307 | DeltaFIFO 适用的情况: 308 | 309 | 1. 你希望最多一个 worker 处理某个对象的事件(对象在队列中是唯一的,); 310 | 2. 当 worker 处理该对象时,可以获得自上次弹出该对象以来的所有事件,如 Add/Updat/Delete(FIFO 只缓存和弹出对象的最新值); 311 | 3. 你可以处理删除对象的事件(FIFO 不弹出被删除的对象); 312 | 4. 你想周期处理所有的对象(Reflector 周期调用 Resync() 方法,将 knownObjects 中对象同步到 DeltaFIFO); 313 | 314 | DeltaFIFO 是一个生产者-消费者队列,生产者是 [Reflector](3.reflector.md),消费者是 [controller/sharedInformer/sharedIndexInformer](4.controller-informer.md)。 315 | 316 | 函数 `NewDeltaFIFO()` 返回一个 `DeltaFIFO` 类型对象: 317 | 318 | ``` go 319 | // 来源于 k8s.io/client-go/tools/cache/delta_fifo.go 320 | func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO { 321 | f := &DeltaFIFO{ 322 | // 对象事件缓存,Key 为对象 Key,Value 为该对象的事件列表类型 Deltas; 323 | items: map[string]Deltas{}, 324 | // 对象弹出队列,缓存的是对象 Key,后续 Pop 方法按序弹出; 325 | queue: []string{}, 326 | // 生成对象标识 Key 的函数,一般是预定义的 MetaNamespaceKeyFunc 函数; 327 | keyFunc: keyFunc, 328 | // 关联的外部对象缓存 329 | knownObjects: knownObjects, 330 | } 331 | f.cond.L = &f.lock 332 | return f 333 | } 334 | ``` 335 | 336 | + `knownObjects` 是外部的对象缓存,DelaFIFO **不对它进行更新**,只用它来查找对象。 337 | + `keyFunc` 一般是预定义的 `MetaNamespaceKeyFunc` 函数,即提取对象的 Namespace/Name 作为标识 Key。 338 | 339 | 例如 NewIndexerInformer() 创建 knownObjects 和 DeltaFIFO 的过程如下: 340 | 341 | ``` go 342 | // 来源于:k8s.io/client-go/tools/cache/controller.go 343 | ... 344 | clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) 345 | ... 346 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, clientState) 347 | ``` 348 | 349 | DeltaFIFO 的消费者根据从 DeltaFIFO 弹出的 Delta 对象对 knownObjects 缓存(上面的 clientState )进行更新,从而保证 FIFO 缓存和该缓存的一致性。 350 | 351 | ### DeltaFIFO 的生产者和消费者 352 | 353 | [后续文章会介绍](4.controller-informer.md),各种 `Informer`(如 `Informer、IndexInformer、SharedInformer、SharedIndexInformer`)的初始化函数依次创建 354 | `knownObjects` 缓存、`DeltaFIO` 和 [`controller`](4.controller-informer.md)。`controller` 再将 `DeltaFIFO` 传给 [Reflector](3.reflector.md), 355 | 356 | **Reflector 的 `ListAndWatch()` 方法是 DeltaFIFO 的生产者**: 357 | 1. List etcd 中(通过 kube-apiserver,下同)特定类型的所有对象,然后调用 DeltaFIFO 的 `Replace()` 方法,将它们完整同步到 DeltaFIFO; 358 | 2. 根据配置的 Resync 时间,**周期调用** DeltaFIFO 的 `Resync()` 方法(见后文),将 knownObjects 中的对象更新到 DeltaFIFO 中; 359 | 3. 阻塞式 Watch etcd 中特对类型的对象变化,根据事件的类型分别调用 DeltaFIFO 的 Add/Update/Delete()方法,将对象更新到 DeltaFIFO; 360 | 361 | Watch etcd 会**周期性的**超时(5min ~ 10min),这时 `ListAndWatch()` 出错返回,Reflector 会等待一段时间再执行它,从而实现**周期的将 `etcd` 中特定类型的全部对象**同步到 `DeltaFIFO`。 362 | 363 | **`controller` 是 `DeltaFIFO` 的消费者,它用 DeltaFIFO 弹出的对象更新 `knownObjects` 缓存**,然后调用注册的 OnUpdate/OnAdd/OnDelete 回调函数。 364 | 详情参考 [Reflector](3.reflector.md) 和 [controller 和 Informer](4.controller-informer.md) 文档。 365 | 366 | ### 记录对象事件的 Delta、Deltas 和 DeletedFinalStateUnknown 类型 367 | 368 | DeltaFIFO 使用 Delta 类型记录对象的事件类型和发生**事件后**的对象值: 369 | 370 | ``` go 371 | type Delta struct { 372 | // DeltaType 可能是:Added、Deleted、Updated、Sync 373 | Type DeltaType 374 | Object interface{} 375 | } 376 | ``` 377 | 378 | DeltaFIFO Watch apiserver 过程中可能因网络等问题出现丢事件的情况,如果丢失了 Delete 事件,则后续 Reflector 重复执行 `ListAndWatch()` 方法从 apiserver 获取的对象集合 set1 会出现与 knownObjects 对象集合 set2 不一致的情况。 379 | 为了保证两者一致,DeltaFIFO 的 `Replace()` 方法将位于 set1 但不在 set2 中的对象用 `DeletedFinalStateUnknown` 类型对象封装,再保存到 Delta Object 中。 380 | 381 | ``` go 382 | type DeletedFinalStateUnknown struct { 383 | // 对象的 Key 384 | Key string 385 | // knownObjects 缓存中的对象值 386 | Obj interface{} 387 | } 388 | ``` 389 | 390 | `Replace()` 方法是**唯一**产生 `DeletedFinalStateUnknown` 类型对象的方法。 391 | 392 | ### Add() 和 Update() 方法 393 | 394 | ``` go 395 | // 来源于 k8s.io/client-go/tools/cache/delta_fifo.go 396 | func (f *DeltaFIFO) Add(obj interface{}) error { 397 | f.lock.Lock() 398 | defer f.lock.Unlock() 399 | f.populated = true 400 | // Added 类型事件; 401 | return f.queueActionLocked(Added, obj) 402 | } 403 | ``` 404 | 405 | `Update()` 方法和 `Add()` 方法类似,差别在于产生的是 `Updated` 类型 Delta 事件; 406 | 407 | ### queueActionLocked() 方法 408 | 409 | 将对象的事件存入事件队列 f.items,如果事件队列中没有该对象则还将对象(Key)加入弹出队列(f.queue),另外它还做如下操作: 410 | 411 | 1. 如果事件类型为 Sync,且对象的事件列表中最后一个事件类型为 Deleted,则直接返回(没有必要再同步一个已删除的对象,一般是 Reflector 周期调用 Rsync() 方法产生的 Sync 事件); 412 | 2. 合并连续重复的 Deleted 事件为一个; 413 | 414 | ``` go 415 | func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error { 416 | id, err := f.KeyOf(obj) 417 | if err != nil { 418 | return KeyError{obj, err} 419 | } 420 | 421 | // FIXME!!! 感觉这个逻辑不太对。产生 Sync 事件有两种情形: 422 | // 1. 周期的 Rsync(), 这时处理逻辑 OK; 423 | // 2. Reflector 执行 ListAndWatch() LIST etcd 获取特定类型的全部对象; 424 | if actionType == Sync && f.willObjectBeDeletedLocked(id) { 425 | return nil 426 | } 427 | // 将对象保存到 Delta 中 428 | newDeltas := append(f.items[id], Delta{actionType, obj}) 429 | newDeltas = dedupDeltas(newDeltas) 430 | 431 | if len(newDeltas) > 0 { 432 | if _, exists := f.items[id]; !exists { 433 | f.queue = append(f.queue, id) 434 | } 435 | f.items[id] = newDeltas 436 | f.cond.Broadcast() 437 | } else { 438 | delete(f.items, id) 439 | } 440 | } 441 | ``` 442 | 443 | ### Delete() 方法 444 | 445 | 如果 `f.knownObjects` 对象缓存和事件队列 `f.items` 中均没有待删除的对象,则**直接返回**,否则为对象生成 `Deleted` 事件(非`DeletedFinalStateUnknown` 类型)。 446 | 447 | FIFO 的 Delete() 方法将对象从缓存中删除,而 DeltaFIFO 的 Delete() 方法**不将对象从事件缓存 f.items 和弹出队列 f.queue 删除**,而是后续弹出时,将它从 f.items、f.queue **和 f.knownObjects** 中删除。 448 | 449 | ### Get/GetByKey/List/ListKeys() 方法 450 | 451 | 查询事件缓存 f.items,返回对象的事件列表 Deltas 或 Key 列表; 452 | 453 | ### Replace() 方法 454 | 455 | `Replace(list []interface{}, resourceVersion string)` 456 | 457 | 1. 为 list 中的每个对象生成 `Sync` 事件; 458 | 2. 遍历 f.knownObjects 中对象,对不在传入的 list 中的对象,用 `DeletedFinalStateUnknown` 类型对象封装,再保存到 Deleted 类型的 Delta.Object 中; 459 | 460 | Reflector 的 `ListAndWatch()` 方法因 Watch 超时而周期调用 `Replace()` 方法,从而周期地将 etcd 中特定类型的所有对象同步到 DeltaFIFO 中。 461 | `controller` 用 DeltaFIFO 弹出的对象更新 `knownObjects` 缓存,详情参考 [Reflector](3.reflector.md) 和 [controller 和 Informer](4.controller-informer.md) 文档。 462 | 463 | `Replace()` 方法是**唯一**产生 `DeletedFinalStateUnknown` 类型对象的方法。 464 | 465 | ### Resync() 方法 466 | 467 | 遍历 knownObjects 中的对象: 468 | 469 | 1. 如果该对象位于事件缓存 f.items 中,则跳过; 470 | 2. 否则,生成 Sync 事件; 471 | 472 | 前文我们提到 DeltaFIFO 的使用场景之一是:**“你想周期处理所有的对象”**,而这是通过周期将 knownObjects 中的对象同步到 DeltaFIFO 来实现的。 473 | 474 | Reflector 的 `ListAndWatch()` 方法周期执行 DeltaFIFO 的 Resync() 方法,将 knownObjects 中的对象同步到 DeltaFIFO(产生 Sync 事件),从而有机会再次调用注册的 `OnUpdate()` 处理函数。 475 | 476 | **只有 Replace() 和 Rsync() 方法才产生 Sync 事件**。 477 | 478 | ### Pop() 方法 479 | 480 | Pop(process PopProcessFunc) 481 | 482 | 1. 如果弹出队列 f.queue 为空,则**阻塞等待**; 483 | 2. 每次弹出队列头部对象的事件列表(Deltas 类型),然后将该对象的事件列表从缓存(f.items)中删除; 484 | 3. 调用配置的回调函数 PopProcessFunc(传入事件列表 Deltas); 485 | 486 | 如果函数 PopProcessFunc() 执行失败,应该调用 `AddIfNotPresent()` 方法将 Deltas 重新加回 DeltaFIFO,这样后续可以再次被弹出处理,防止丢事件。(controler 已实现该逻辑) 487 | 488 | 注意,Pop() 方法是在加锁的情况下调用 PopProcessFunc 的,所以在多个 goroutine 并发调用 Pop() 的情况下,它们实际是**串行**执行的。 489 | 490 | ### HasSyncd() 方法 491 | 492 | 创建 DealtaFIFO 后,如果首先调用的是 `Replace()` 方法,则 `f.populated` 被设置为 `true`,`f.initialPopulationCount` 被设置为传入的对象数量。当这**第一批**对象都被弹出完毕时(包含弹出前被删除的对象),`HasSynced()` 方法返回 `true`: 493 | 494 | ``` go 495 | // 来源于 k8s.io/client-go/tools/cache/fifo.go 496 | func (f *DeltaFIFO) HasSynced() bool { 497 | f.lock.Lock() 498 | defer f.lock.Unlock() 499 | return f.populated && f.initialPopulationCount == 0 500 | } 501 | ``` 502 | 503 | 另外,如果在调用 `Replace()` 方法前,**首先**调用了 `Add/Update/Delete/AddIfNotPresent()` 方法,则 `HasSynced()` 方法也会返回 `true`。 504 | 505 | 第一批对象弹出完毕后,后续不管是否再次调用 Replace()或其它方法,HasSynced() 方法**总是返回 true** 506 | 507 | ### DeltaFIFO 和 knownObjects 对象缓存的同步 508 | 509 | 1. Reflector 从 etcd List 出特定类型的所有对象,调用 DeltaFIFO 的 Replace() 方法为各对象生成 Sync 事件,此时 knownObjects 对象缓存为空; 510 | 2. controller 从 DeltaFIFO 弹出对象事件列表 Deltas,遍历 Deltas,根据 Delta 的事件类型更新 knownObjects,从而实现 DeltaFIFO 和 knownObjects 缓存中的对象一致: 511 | 512 | controller 每次**启动**时,因 knownObjects 为空且事件类型为 Sync,所以会为同步来的所有对象: 513 | 514 | 1. 调用 knownObjects 的 **Add() 方法**,将它们加入到对象缓存; 515 | 2. 调用注册的 OnAdd() 回调函数。所以**第一次对象同步时, controller 也会调用用户注册的 OnAdd() 回调函数**。 516 | 517 | ``` go 518 | // 来源于:k8s.io/client-go/tools/cache/controller.go 519 | for _, d := range obj.(Deltas) { 520 | switch d.Type { 521 | // Replace() 方法生成的 Sync 事件涉及到的对象, 522 | case Sync, Added, Updated: 523 | // clientState 即为 knownObjects 对象缓存 524 | if old, exists, err := clientState.Get(d.Object); err == nil && exists { 525 | if err := clientState.Update(d.Object); err != nil { 526 | return err 527 | } 528 | h.OnUpdate(old, d.Object) 529 | } else { 530 | if err := clientState.Add(d.Object); err != nil { 531 | return err 532 | } 533 | h.OnAdd(d.Object) 534 | } 535 | case Deleted: 536 | // 先从缓存中删除,再调用处理函数 537 | if err := clientState.Delete(d.Object); err != nil { 538 | return err 539 | } 540 | h.OnDelete(d.Object) 541 | } 542 | } 543 | ``` 544 | 545 | 3. 但是,Reflector 的 Watch 可能会出现**丢失事件**的情况(如 ListAndWatch 出错返回后,Reflector 会 Sleep 一段时间再执行它,期间 etcd 的对象变化事件丢失),这样再次 List 到的对象集合 set1 与 knownObjects 缓存中的对象集合 set2 不一致。如何解决这个问题呢? 546 | 547 | 答案在于:List 到对象集合后,DeltaFIFO 调用的 `Replace()` 方法将位于 set1 但不在 set2 中的对象用 `DeletedFinalStateUnknown` 类型对象封装,再保存到 Delta.Object 中。而上面 handlerObject() 的参数即为 Delta.Object。 548 | 549 | 4. ListAndWatch() 方法起一个 goroutine,周期调用 Resync() 方法,将 knownObjects 中的对象更新到 DeltaFIFO。为何要这么做呢? 550 | 551 | 前文我们提到 DeltaFIFO 的使用场景之一是:**“你想周期处理所有的对象”**,但对象一旦从 DeltaFIFO 中弹出,如果没有产生新的 Watch 事件,就不会对它再调用注册的回调函数。Reflector 的 `ListAndWatch()` 方法会周期执行 DeltaFIFO 的 Resync() 方法,目的就是**为对象产生新的 Sync 事件**,从而有机会再次调用注册的 `OnUpdate()` 处理函数。因此 Resync 时,如果对象已经在 f.items,则后续由机会被弹出,所以不需要为它生成 Sync 事件。 552 | 553 | 后续文章会介绍,`controller` 一般是在 `Informer` 中使用的,`controller` 调用的 `OnUpdate()` 函数会调用用户创建 `Informer` 时传入的 `ResourceEventHandler` 中的 `OnUpdate()` 函数。所以用户的 `OnUpdate()` 函数可能会因 DeltaFIFO 的周期 Resync() 而被调用,它应该检查传入的 old、new 对象是否发生变化,未变化时直接返回: 554 | 555 | ``` go 556 | // 来源于 https://github.com/kubernetes/sample-controller/blob/master/controller.go#L131 557 | deployInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 558 | // 第一次从 etcd 同步的对象会生成 Added 事件,调用该函数 559 | // 后续 Added 事件就代表的确有**新创建**的资源对象; 560 | AddFunc: controller.handleDeploy, 561 | UpdateFunc: func(old, new interface{}) { 562 | // 由于 Reflector 周期调用 DeltaFIFO 的 Rsync() 方法,`controller` 会调用注册的 OnUpdate 回调函数,所以需要判断新旧对象是否相同,如果相同则不需处理 563 | // 也可以用 564 | newDepl := new.(*extensionsv1beta1.Deployment) 565 | oldDepl := old.(*extensionsv1beta1.Deployment) 566 | if newDepl.ResourceVersion == oldDepl.ResourceVersion { 567 | // Periodic resync will send update events for all known Deployments. 568 | // Two different versions of the same Deployment will always have different RVs. 569 | return 570 | } 571 | controller.handleDeploy(new) 572 | }, 573 | DeleteFunc: controller.handleDeploy, 574 | }) 575 | ``` 576 | 577 | 另外,前面的第 2 条提到过,`controller` 刚启动时,knownObjects 为空,会为从 etcd 同步来的特定类型的所有对象生成 Added 事件,进而调用上面注册的 AddFunc。 -------------------------------------------------------------------------------- /client-go/3.listwatch-reflector-controller.md: -------------------------------------------------------------------------------- 1 | # kubernetes 事件反射器 2 | 3 | 4 | 5 | - [kubernetes 事件反射器](#kubernetes-事件反射器) 6 | - [ListWatcher 接口](#listwatcher-接口) 7 | - [实现 ListWatcher 接口的 ListWatch](#实现-listwatcher-接口的-listwatch) 8 | - [使用 ListWatch 的 Informer](#使用-listwatch-的-informer) 9 | - [Reflector](#reflector) 10 | - [Reflector 类型定义](#reflector-类型定义) 11 | - [创建 Reflector 对象的函数](#创建-reflector-对象的函数) 12 | - [Run() 方法](#run-方法) 13 | - [ListAndWatch() 方法](#listandwatch-方法) 14 | - [使用 Reflector 的 controller](#使用-reflector-的-controller) 15 | - [Run() 方法](#run-方法-1) 16 | - [processLoop() 方法](#processloop-方法) 17 | - [使用 controller 的 Informer](#使用-controller-的-informer) 18 | - [HasSynced() 方法](#hassynced-方法) 19 | - [自定义 Controller 使用 HasSynced() 方法的场景](#自定义-controller-使用-hassynced-方法的场景) 20 | 21 | 22 | 23 | `Reflector` 是 Kubernetes 的事件反射器,`controller`、`Informer` 使用它 List 和 Watch apiserver 中特定类型资源对象变化,并更新到 DeltaFIFO 和对象缓存中,这样客户端后续可以从本地缓存获取对象,而不需要每次都和 apiserver 交互。 24 | 25 | 在介绍 Relfector 前,先介绍 Reflector 使用的 `ListerWatcher` 接口。 26 | 27 | ## ListWatcher 接口 28 | 29 | ListWatcher 接口定义了 List 和 Watch 特定资源类型的方法: 30 | 31 | 1. List(): 从 apiserver 获取一批**特定类型**的对象; 32 | 2. Watch(): 从上面 List() 获取的 ResourceVersion 开始,通过 apiserver Watch etcd 中对象的变化; 33 | 34 | ListWatcher 需要通过 RESTClient 的 Get 方法和 apiserver 通信,Getter 接口定义了该方法。 35 | 36 | ``` go 37 | // 来源于 k8s.io/client-go/tools/cache/listwatch.go` 38 | type ListerWatcher interface { 39 | // List() 返回对象列表;从这些对象中提取 ResourceVersion,用户后续的 Watch() 方法参数; 40 | List(options metav1.ListOptions) (runtime.Object, error) 41 | // Watch() 方法从指定的版本(options 中指定)开始 Watch。 42 | Watch(options metav1.ListOptions) (watch.Interface, error) 43 | } 44 | 45 | // ListFunc knows how to list resources 46 | type ListFunc func(options metav1.ListOptions) (runtime.Object, error) 47 | 48 | // WatchFunc knows how to watch resources 49 | type WatchFunc func(options metav1.ListOptions) (watch.Interface, error) 50 | 51 | // Getter interface knows how to access Get method from RESTClient. 52 | type Getter interface { 53 | Get() *restclient.Request 54 | } 55 | ``` 56 | 57 | ### 实现 ListWatcher 接口的 ListWatch 58 | 59 | ListWatch 类型实现了 `ListWatcher` 接口,被 `NewReflector()` 函数和各 K8S 内置资源对象的 `Informer` 创建函数(如 `NewFilteredDeploymentInformer()`)使用: 60 | 61 | ``` go 62 | // 来源于 k8s.io/client-go/tools/cache/listwatch.go 63 | type ListWatch struct { 64 | ListFunc ListFunc 65 | WatchFunc WatchFunc 66 | DisableChunking bool 67 | } 68 | ``` 69 | 70 | 函数 `NewListWatchFromClient()` 和 `NewFilteredListWatchFromClient()` 返回 ListWatch 对象。 71 | 72 | 传入的 Getter 参数是**已经配置**的 K8S **特定 API Group/Version 的 RESTClient**,这里的“已经配置” 指创建 RESTClient 的 rest.Config 已经配置了 GroupVersion、APIPath、NegotiatedSerializer 等参数: 73 | 74 | 对于 K8S 内置对象,已经配置的 RESTClient 位于 `k8s.io/client-go/kubernetes///_client.go` 文件中,如 75 | [ExtensionsV1beta1Client](vendor/k8s.io/client-go/kubernetes/typed/extensions/v1beta1/extensions_client.go) 76 | 77 | 对于自定义类型对象,已经配置的 RESTClient 位于 `pkg/client/clientset/versioned/typed///_client.go` 文件中。 78 | 79 | 这个已配置的 RESTClient 适用于特定 API Group/Version 下的**所有**资源类型,在发送 Get() 请求时,还需要给`Resource()` 方法传入具体的**资源类型名称**,如 `Resource("deployments")`,这样才能唯一确定资源类型:`////namespaces//`,如 `/apis/apps/v1beta1/namespaces/default/deployment/my-nginx-111`。 80 | 81 | ``` go 82 | // 来源于 k8s.io/client-go/tools/cache/listwatch.go 83 | func NewListWatchFromClient(c Getter, resource string, namespace string, fieldSelector fields.Selector) *ListWatch { 84 | optionsModifier := func(options *metav1.ListOptions) { 85 | options.FieldSelector = fieldSelector.String() 86 | } 87 | return NewFilteredListWatchFromClient(c, resource, namespace, optionsModifier) 88 | } 89 | 90 | // 调用 RESTClient 的 Get() 方法 List 和 Watch 对应的资源类型对象 91 | func NewFilteredListWatchFromClient(c Getter, resource string, namespace string, optionsModifier func(options *metav1.ListOptions)) *ListWatch { 92 | listFunc := func(options metav1.ListOptions) (runtime.Object, error) { 93 | optionsModifier(&options) 94 | // c 是某一个 API Group/Version 的 RESTClient 95 | return c.Get(). 96 | Namespace(namespace). 97 | Resource(resource). // 指定某一个资源类型名称,如 "deployments" 98 | VersionedParams(&options, metav1.ParameterCodec). 99 | Do(). 100 | Get() 101 | } 102 | // 实际调用 WatchFunc 函数时,options 中包含上次 ListFunc 放回的对象列表的 ResourceVersion 值 103 | watchFunc := func(options metav1.ListOptions) (watch.Interface, error) { 104 | options.Watch = true 105 | optionsModifier(&options) 106 | return c.Get(). 107 | Namespace(namespace). 108 | Resource(resource). 109 | VersionedParams(&options, metav1.ParameterCodec). 110 | Watch() 111 | } 112 | return &ListWatch{ListFunc: listFunc, WatchFunc: watchFunc} 113 | } 114 | ``` 115 | 116 | ListWatch 的 `List()` 和 `Watch()` 方法是直接调用内部的 `ListFunc()` 或 `WatchFunc()` 函数来实现的,比较直接和简单,故不再分析。 117 | 118 | ### 使用 ListWatch 的 Informer 119 | 120 | 后文会介绍,各资源类型都有自己特定的 Informer(codegen 工具自动生成),如 [DeploymentInformer](k8s.io/client-go/informers/extensions/v1beta1/deployment.go),它们使用自己资源类型的 ClientSet 来初始化 ListWatch,只返回对应类型的对象: 121 | 122 | ``` go 123 | // 来源于 k8s.io/client-go/informers/extensions/v1beta1/deployment.go 124 | func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 125 | return cache.NewSharedIndexInformer( 126 | // 使用特定资源类型的 RESTClient 创建 ListWatch 127 | &cache.ListWatch{ 128 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 129 | if tweakListOptions != nil { 130 | tweakListOptions(&options) 131 | } 132 | return client.ExtensionsV1beta1().Deployments(namespace).List(options) 133 | }, 134 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 135 | if tweakListOptions != nil { 136 | tweakListOptions(&options) 137 | } 138 | return client.ExtensionsV1beta1().Deployments(namespace).Watch(options) 139 | }, 140 | }, 141 | &extensionsv1beta1.Deployment{}, 142 | resyncPeriod, 143 | indexers, 144 | ) 145 | } 146 | ``` 147 | 148 | ## Reflector 149 | 150 | 在解析完 ListWatch 垫后,我们终于可以开始解析 Reflector 的实现了! 151 | 152 | ### Reflector 类型定义 153 | 154 | Reflector 使用 ListerWatcher 从 apiserver 同步 expectedType 类型的对象及其事件,缓存到 store(DeltaFIFO 类型)中。 155 | 156 | ``` go 157 | // 来源于:k8s.io/client-go/tools/cache/reflector.go 158 | type Reflector struct { 159 | // Reflector 的名称,缺省值是 file:line 160 | name string 161 | 162 | // metrics tracks basic metric information about the reflector 163 | metrics *reflectorMetrics 164 | 165 | // 一个 Reflector 指定监控一种对象类型,该 field 指定对象类型 166 | expectedType reflect.Type 167 | 168 | // store 用于缓存对象变化事件,一般是 DeltaFIFO,可以参考 NewInformer/NewIndexerInformer 函数 169 | store Store 170 | 171 | // listerWatcher 用于 List 和 Watch 资源对象 172 | listerWatcher ListerWatcher 173 | 174 | // 当 ListAndWatch() 方法出错(超时)返回后,等待 period 时间后再次执行 ListAndWatch() 方法 175 | period time.Duration 176 | 177 | // Reflector 周期调用 store 的 Resync() 方法, 178 | // 对于 DeltaFIFO 而言,是将 knownObjects 对象缓存中的所有对象同步到 DeltaFIFO 中 179 | resyncPeriod time.Duration 180 | 181 | // 在执行 resync 时额外的判断函数 182 | ShouldResync func() bool 183 | 184 | // clock allows tests to manipulate time 185 | clock clock.Clock 186 | 187 | // lastSyncResourceVersion is the resource version token last 188 | // observed when doing a sync with the underlying store 189 | // it is thread safe, but not synchronized with the underlying store 190 | lastSyncResourceVersion string 191 | 192 | // lastSyncResourceVersionMutex guards read/write access to lastSyncResourceVersion 193 | lastSyncResourceVersionMutex sync.RWMutex 194 | } 195 | ``` 196 | 197 | ### 创建 Reflector 对象的函数 198 | 199 | 函数 `NewNamespaceKeyedIndexerAndReflector()`、`NewReflector()`、`NewNamedReflector()` 返回 Reflector 对象,其中 `NewNamedReflector()` 是其它两个方法的基础: 200 | 201 | ``` go 202 | // 来源于:k8s.io/client-go/tools/cache/reflector.go 203 | var internalPackages = []string{"client-go/tools/cache/"} 204 | 205 | func NewNamespaceKeyedIndexerAndReflector(lw ListerWatcher, expectedType interface{}, resyncPeriod time.Duration) (indexer Indexer, reflector *Reflector) { 206 | // 使用 Namespace 作为 IndexFunc 和 KeyFunc 207 | indexer = NewIndexer(MetaNamespaceKeyFunc, Indexers{"namespace": MetaNamespaceIndexFunc}) 208 | reflector = NewReflector(lw, expectedType, indexer, resyncPeriod) 209 | return indexer, reflector 210 | } 211 | 212 | func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { 213 | // 使用 Reflector 所在的目录文件创建 Reflector 名称,格式为 file:linenum 214 | return NewNamedReflector(naming.GetNameFromCallsite(internalPackages...), lw, expectedType, store, resyncPeriod) 215 | } 216 | 217 | func NewNamedReflector(name string, lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { 218 | reflectorSuffix := atomic.AddInt64(&reflectorDisambiguator, 1) 219 | r := &Reflector{ 220 | name: name, 221 | // we need this to be unique per process (some names are still the same) but obvious who it belongs to 222 | metrics: newReflectorMetrics(makeValidPrometheusMetricLabel(fmt.Sprintf("reflector_"+name+"_%d", reflectorSuffix))), 223 | listerWatcher: lw, 224 | store: store, 225 | expectedType: reflect.TypeOf(expectedType), 226 | period: time.Second, // Run() 方法 wait.Until() 的重新执行时间间隔 227 | resyncPeriod: resyncPeriod, // 周期调用 store 的 Resync() 方法的时间间隔 228 | clock: &clock.RealClock{}, 229 | } 230 | return r 231 | } 232 | ``` 233 | 234 | Relectore 对象一般是 `Informer` 的 `controller` 创建的,例如(具体参考:[4.controller-informer.md](4.controller-informer.md)): 235 | 236 | ``` go 237 | // 来源于:k8s.io/client-go/tools/cache/controller.go 238 | func (c *controller) Run(stopCh <-chan struct{}) { 239 | ... 240 | // 使用 controller 的 Config 参数创建 Reflector 241 | r := NewReflector( 242 | c.config.ListerWatcher, 243 | c.config.ObjectType, 244 | c.config.Queue, // DeltaFIFO 245 | c.config.FullResyncPeriod, 246 | ) 247 | r.ShouldResync = c.config.ShouldResync 248 | r.clock = c.clock 249 | ... 250 | // 运行 Reflector 的 Run 方法 251 | wg.StartWithChannel(stopCh, r.Run) 252 | ... 253 | } 254 | ``` 255 | 256 | ### Run() 方法 257 | 258 | Run() 方法一直运行 `ListAndWatch()` 方法,如果出错,则等待 r.period 时间后再次执行 `ListAndWatch()` 方法方法,所以 **Run() 方法是不会返回的**,直到 stopCh 被关闭。 259 | 260 | ``` go 261 | // 来源于:k8s.io/client-go/tools/cache/reflector.go 262 | func (r *Reflector) Run(stopCh <-chan struct{}) { 263 | klog.V(3).Infof("Starting reflector %v (%s) from %s", r.expectedType, r.resyncPeriod, r.name) 264 | wait.Until(func() { 265 | if err := r.ListAndWatch(stopCh); err != nil { 266 | utilruntime.HandleError(err) 267 | } 268 | }, r.period, stopCh) 269 | } 270 | ``` 271 | 272 | ### ListAndWatch() 方法 273 | 274 | 该方法是 Refelector 的核心方法,实现了: 275 | 276 | 1. 从 apiserver List 资源类型的所有对象(ResourceVersion 为 0); 277 | 2. 从对象列表中获取该类型对象的 resourceVersion; 278 | 3. 调用内部 Store(DeltaFIFO)的 Replace() 方法,将 List 的对象更新到内部的 DeltaFIFO 中(生成 Sync 事件,或 DeletedFinalStateUnknown 类型的 Deleted 事件); 279 | 4. 周期(resyncPeriod)调用 DeltaFIFO 的 Resycn() 方法,将 knownObjects 缓存中的对象同步到 DeltaFIFO 中(SYNC 事件),从而实现**周期处理所有**资源类型对象的功能; 280 | 5. 从 List 获取的 resourceVersion 开始阻塞式 Watch apiserver,根据收到的事件类型更新 DeltaFIFO; 281 | 282 | ``` go 283 | // 来源于:k8s.io/client-go/tools/cache/reflector.go 284 | 285 | // Watch 的最小 timeout 时间,实际是 [minWatchTimeout, 2*minWatchTimeout] 之间的随机值 286 | var minWatchTimeout = 5 * time.Minute 287 | 288 | func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error { 289 | ... 290 | // ResourceVersion: "0" 表示 etcd 中对象的当前值,这里列出 etcd 中当前该类型的所有对象 291 | options := metav1.ListOptions{ResourceVersion: "0"} 292 | ... 293 | list, err := r.listerWatcher.List(options) 294 | ... 295 | listMetaInterface, err := meta.ListAccessor(list) 296 | ... 297 | // 从对象列表中获取该类型对象的 resourceVersion,后续 Watch 的时候使用 298 | resourceVersion = listMetaInterface.GetResourceVersion() 299 | ... 300 | items, err := meta.ExtractList(list) 301 | ... 302 | // 将 items 同步到 r.store 即 DeltaFIFO 中,使用的是 DeltaFIFO 的 Replace() 方法 303 | if err := r.syncWith(items, resourceVersion); err != nil { 304 | return fmt.Errorf("%s: Unable to sync list result: %v", r.name, err) 305 | } 306 | // 缓存 resourceVersion 307 | r.setLastSyncResourceVersion(resourceVersion) 308 | ... 309 | 310 | go func() { 311 | resyncCh, cleanup := r.resyncChan() 312 | defer func() { 313 | cleanup() // Call the last one written into cleanup 314 | }() 315 | // 周期调用 r.store 的 Resync() 方法,实现将 knownObjects 对象缓存中的对象同步到 DeltaFIFO 中 316 | for { 317 | select { 318 | case <-resyncCh: 319 | case <-stopCh: 320 | return 321 | case <-cancelCh: 322 | return 323 | } 324 | if r.ShouldResync == nil || r.ShouldResync() { 325 | klog.V(4).Infof("%s: forcing resync", r.name) 326 | if err := r.store.Resync(); err != nil { 327 | resyncerrc <- err 328 | return 329 | } 330 | } 331 | cleanup() 332 | resyncCh, cleanup = r.resyncChan() 333 | } 334 | }() 335 | 336 | for { 337 | ... 338 | // Watch 会在 timeoutSeconds 后超时,这时 r.watchHandler() 出错,ListAndWatch() 方法出错返回 339 | // Reflecter 会等待 r.period 事件后重新执行 ListAndWatch() 方法; 340 | timeoutSeconds := int64(minWatchTimeout.Seconds() * (rand.Float64() + 1.0)) 341 | options = metav1.ListOptions{ 342 | ResourceVersion: resourceVersion, 343 | TimeoutSeconds: &timeoutSeconds, 344 | } 345 | w, err := r.listerWatcher.Watch(options) 346 | ... 347 | // 阻塞处理 Watch 事件 348 | if err := r.watchHandler(w, &resourceVersion, resyncerrc, stopCh); err != nil { 349 | if err != errorStopRequested { 350 | klog.Warningf("%s: watch of %v ended with: %v", r.name, r.expectedType, err) 351 | } 352 | return nil 353 | } 354 | } 355 | } 356 | 357 | // 用 items 中的对象替换 r.store 358 | func (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error { 359 | found := make([]interface{}, 0, len(items)) 360 | for _, item := range items { 361 | found = append(found, item) 362 | } 363 | return r.store.Replace(found, resourceVersion) 364 | } 365 | 366 | // 根据 Watch 到的事件类型,调用 r.store 的方法,将对象更新到 r.store 中 367 | func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error { 368 | ... 369 | loop: 370 | for { 371 | select { 372 | ... 373 | case event, ok := <-w.ResultChan(): 374 | ... 375 | newResourceVersion := meta.GetResourceVersion() 376 | switch event.Type { 377 | case watch.Added: 378 | err := r.store.Add(event.Object) 379 | if err != nil { 380 | utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err)) 381 | } 382 | case watch.Modified: 383 | err := r.store.Update(event.Object) 384 | if err != nil { 385 | utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err)) 386 | } 387 | case watch.Deleted: 388 | err := r.store.Delete(event.Object) 389 | if err != nil { 390 | utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err)) 391 | } 392 | default: 393 | utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) 394 | } 395 | *resourceVersion = newResourceVersion 396 | r.setLastSyncResourceVersion(newResourceVersion) 397 | eventCount++ 398 | } 399 | } 400 | ... 401 | } 402 | ``` 403 | 404 | ## 使用 Reflector 的 controller 405 | 406 | Controller 封装了 Reflector,Reflector 使用 ListerWatcher 从 apiserver 获取对象列表和事件,存到 DeltaFIFO 中,Controller 持续弹出 Reflector 的 DeltaFIFO,用弹出的 Delta 更新它的 ClientState 缓存,并调用 Informer 设置的 OnAdd/OnUpdate/OnDelete 回调函数。 407 | 408 | ``` go 409 | // 来源于:k8s.io/client-go/tools/cache/controller.go 410 | type Config struct { 411 | // 缓存 ObjectType 类型对象的队列,同时也被 Reflector 使用。 412 | // 后续 NewInformer、NewIndexerInformer 创建该配置时,实际创建的是 DeltaFIFO 类型的 Queue。 413 | Queue 414 | 415 | // Controller 为 ObjectType 创建 Reflector 使用的 ListerWatcher; 416 | ListerWatcher 417 | 418 | // 对于对象的每个事件,调用该处理函数 419 | Process ProcessFunc 420 | 421 | // 该 Controller 关注和管理的对象类型 422 | ObjectType runtime.Object 423 | 424 | // 周期调用 Queue 的 Resync() 方法的周期 425 | FullResyncPeriod time.Duration 426 | 427 | // 而外判断是否需要 Resync() 的函数(一般是 nil) 428 | ShouldResync ShouldResyncFunc 429 | 430 | // 如果 Process 处理弹出的对象失败,是否将对象重新加到 Queue 中(一般是 false) 431 | RetryOnError bool 432 | } 433 | 434 | // controller 是 Controller 的实现,但是没有创建它的 New 方法 435 | // 所以 controller 实际是由 NewInformer、NewIndexerInformer 来创建和使用的 436 | type controller struct { 437 | config Config 438 | reflector *Reflector 439 | reflectorMutex sync.RWMutex 440 | clock clock.Clock 441 | } 442 | ``` 443 | 444 | ### Run() 方法 445 | 446 | ``` go 447 | func (c *controller) Run(stopCh <-chan struct{}) { 448 | defer utilruntime.HandleCrash() 449 | go func() { 450 | <-stopCh 451 | // controller 运行结束时关闭队列 452 | c.config.Queue.Close() 453 | }() 454 | 455 | // 根据 Controller 的配置,初始化监控 ObjectType 类型对象的 Reflector 456 | // 初始化 Refector 时,传入了 Controller 的 Queue(DeltaFIFO),所以 Refector 会同步更新该 Queue; 457 | r := NewReflector( 458 | c.config.ListerWatcher, 459 | c.config.ObjectType, 460 | c.config.Queue, 461 | c.config.FullResyncPeriod, 462 | ) 463 | r.ShouldResync = c.config.ShouldResync 464 | r.clock = c.clock 465 | 466 | c.reflectorMutex.Lock() 467 | c.reflector = r 468 | c.reflectorMutex.Unlock() 469 | 470 | var wg wait.Group 471 | defer wg.Wait() 472 | 473 | // 在另一个 goroutine 里启动 Reflector 474 | wg.StartWithChannel(stopCh, r.Run) 475 | 476 | // 阻塞执行 c.processLoop,该 Loop 是个死循环,只是在出错时才返回值,然后等待 1s 后重新执行 477 | wait.Until(c.processLoop, time.Second, stopCh) 478 | } 479 | ``` 480 | ### processLoop() 方法 481 | 482 | 从 DeltaFIFO 中弹出 Deltas 事件,然后调用配置的 PopProcessFunc 函数,该函数: 483 | 484 | 1. 使用弹出的对象更新 DeltaFIFO 使用的 knownObjests 对象缓存(controller 创建的 clientState); 485 | 2. 调用用户注册的回调函数; 486 | 487 | DelteFIFO 的 Pop() 方法是在**加锁的情况下**执行 PopProcessFunc 的,所以即使多个 goroutine 并发调用 Pop() 方法,它们也是**串行执行**的,所以**不会出现多个 goroutine 同时处理一个资源对象的情况**。 488 | 489 | ``` go 490 | func (c *controller) processLoop() { 491 | for { 492 | obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) 493 | if err != nil { 494 | if err == FIFOClosedError { 495 | return 496 | } 497 | if c.config.RetryOnError { 498 | // This is the safe way to re-enqueue. 499 | c.config.Queue.AddIfNotPresent(obj) 500 | } 501 | } 502 | } 503 | } 504 | ``` 505 | 506 | c.config.RetryOnError 一般为 false(参考后续的 NewInformer() 函数),所以当 PopProcessFunc 执行出错时并不会将对象添加到 DeltaFIFO 中。 507 | 508 | ## 使用 controller 的 Informer 509 | 510 | NewInformer()、NewIndexInformer() 函数使用 controller 来 List/Watch 特定资源类型的对象,缓存到本地,并调用用户提供的回调函数(保存在 ResourceEventHandler 中)。 511 | 512 | ``` go 513 | // 来源于:k8s.io/client-go/tools/cache/controller.go 514 | func NewInformer( 515 | lw ListerWatcher, 516 | objType runtime.Object, 517 | resyncPeriod time.Duration, 518 | h ResourceEventHandler, 519 | ) (Store, Controller) { 520 | // Store 是一个存储对象的内存数据库,它使用 KeyFunc 函数获取对象的唯一访问 key。 521 | // NewStore 实际返回的是一个 ThreadSafe 类型 522 | clientState := NewStore(DeletionHandlingMetaNamespaceKeyFunc) 523 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, clientState) 524 | 525 | cfg := &Config{ 526 | Queue: fifo, 527 | ListerWatcher: lw, 528 | ObjectType: objType, 529 | FullResyncPeriod: resyncPeriod, 530 | // 不将执行失败 Process 失败的对象放回 Queue 531 | RetryOnError: false, 532 | 533 | // DeltaFIFO 的 Pop() 方法是在内部加锁的情况下执行 Process 函数,所以对 clientState 的更新和调用 OnUpdate/OnAdd/OnDelted 处理函数是串行的。 534 | Process: func(obj interface{}) error { 535 | // from oldest to newest 536 | for _, d := range obj.(Deltas) { 537 | switch d.Type { 538 | case Sync, Added, Updated: 539 | // 根据 clientState 中是否有该对象决定调用 OnUpdate() 还是 OnAdd() 处理函数 540 | // 所以,Controller 刚启动时,由于 clientState 为空,会为所有 list 到的对象调用 OnAdd() 处理函数; 541 | if old, exists, err := clientState.Get(d.Object); err == nil && exists { 542 | if err := clientState.Update(d.Object); err != nil { 543 | return err 544 | } 545 | h.OnUpdate(old, d.Object) 546 | } else { 547 | if err := clientState.Add(d.Object); err != nil { 548 | return err 549 | } 550 | h.OnAdd(d.Object) 551 | } 552 | case Deleted: 553 | // 删除的时候,是先将对象从 clientState 中删除,再调用用户处理函数 554 | if err := clientState.Delete(d.Object); err != nil { 555 | return err 556 | } 557 | // d.Object 可能是原生资源类型对象,也可能是 DeletedFinalStateUnknown 类型对象,所以 OnDeleted()函数需要能区分和处理 558 | h.OnDelete(d.Object) 559 | } 560 | } 561 | return nil 562 | }, 563 | } 564 | return clientState, New(cfg) 565 | } 566 | ``` 567 | 568 | ### HasSynced() 方法 569 | 570 | 调用 DeltaFIFO 的 HasSynced() 方法。 571 | 572 | 当 processLoop() 弹出 DeltaFIFO 中第一批 Reflector List 到的对象并处理结束后,该方法返回 true,而且后续一直返回 true; 573 | 574 | ``` go 575 | func (c *controller) HasSynced() bool { 576 | return c.config.Queue.HasSynced() 577 | } 578 | ``` 579 | 580 | sharedInformer/sharedIndexInformer 的 HasSynced() 方法实际是调用 controller 的 HasSynced() 方法,而且该方法的签名与 InformerSynced 函数类型一致: 581 | 582 | ``` go 583 | // 来源:k8s.io/client-go/tools/cache/shared_informer.go 584 | func (s *sharedIndexInformer) HasSynced() bool { 585 | s.startedLock.Lock() 586 | defer s.startedLock.Unlock() 587 | 588 | if s.controller == nil { 589 | return false 590 | } 591 | return s.controller.HasSynced() 592 | } 593 | 594 | type InformerSynced func() bool 595 | ``` 596 | 597 | ### 自定义 Controller 使用 HasSynced() 方法的场景 598 | 599 | 在开发 K8S Controller 的时候,一个惯例是调用 cache.WaitForCacheSync,等待所有 Informer Cache 都同步后才启动消费 workqueue 的 worker: 600 | 601 | ``` go 602 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 603 | 604 | // 自定义的 Controller 605 | type Controller struct { 606 | ... 607 | deploymentsLister appslisters.DeploymentLister 608 | deploymentsSynced cache.InformerSynced // InformerSynced 函数类型 609 | ... 610 | } 611 | 612 | func NewController( 613 | kubeclientset kubernetes.Interface, 614 | sampleclientset clientset.Interface, 615 | deploymentInformer appsinformers.DeploymentInformer, 616 | fooInformer informers.FooInformer) *Controller { 617 | ... 618 | controller := &Controller{ 619 | kubeclientset: kubeclientset, 620 | sampleclientset: sampleclientset, 621 | deploymentsLister: deploymentInformer.Lister(), 622 | deploymentsSynced: deploymentInformer.Informer().HasSynced, // Infomer 的 HasSynced() 方法 623 | } 624 | ... 625 | } 626 | 627 | // 等待所有类型的 Informer 的 HasSynced() 方法返回为 true 时再启动 workers 628 | func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { 629 | ... 630 | // Wait for the caches to be synced before starting workers 631 | klog.Info("Waiting for informer caches to sync") 632 | if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok { 633 | return fmt.Errorf("failed to wait for caches to sync") 634 | } 635 | 636 | klog.Info("Starting workers") 637 | // Launch two workers to process Foo resources 638 | for i := 0; i < threadiness; i++ { 639 | go wait.Until(c.runWorker, time.Second, stopCh) 640 | } 641 | ... 642 | } 643 | ``` 644 | 645 | 为何要等各 informer 的 HasSynced() 返回为 true 时才开始启动 worker 呢? 646 | 647 | 因为 HasSynced() 返回 true 时表明 Reflecter List 的第一批对象都从 DeltaFIFO 弹出,并由 controller **更新到 clientState 缓存中,这样 worker 才能通过通过对象名称(Key)从 Lister Get 到对象**。否则对象可能还在 DeltaFIFO 中且没有同步到 clientState 缓存中,这样 worker 通过对象名称从 Lister 中 Get 不到对象。 -------------------------------------------------------------------------------- /client-go/4.informer.md: -------------------------------------------------------------------------------- 1 | # Informer 2 | 3 | 4 | 5 | - [Informer](#informer) 6 | - [processorListener](#processorlistener) 7 | - [add() 方法](#add-方法) 8 | - [pop() 方法](#pop-方法) 9 | - [run() 方法](#run-方法) 10 | - [sharedProcessor](#sharedprocessor) 11 | - [addListener() 和 addListenerLocked() 方法](#addlistener-和-addlistenerlocked-方法) 12 | - [distribute() 方法](#distribute-方法) 13 | - [run() 方法](#run-方法-1) 14 | - [shouldResync() 方法](#shouldresync-方法) 15 | - [SharedInformer 和 SharedIndexInformer](#sharedinformer-和-sharedindexinformer) 16 | - [实现 SharedIndexInformer 接口的 sharedIndexInformer 类型](#实现-sharedindexinformer-接口的-sharedindexinformer-类型) 17 | - [Run() 方法](#run-方法) 18 | - [HasSynced() 方法](#hassynced-方法) 19 | - [AddEventHandler() 方法](#addeventhandler-方法) 20 | - [AddEventHandlerWithResyncPeriod() 方法](#addeventhandlerwithresyncperiod-方法) 21 | - [HandleDeltas() 方法](#handledeltas-方法) 22 | - [WaitForCacheSync() 函数](#waitforcachesync-函数) 23 | - [codegen 为特定资源类型创建的 SharedIndexInformer](#codegen-为特定资源类型创建的-sharedindexinformer) 24 | 25 | 26 | 27 | Inforer 的主要使用场景是自定义 Controller,它需要从 apiserver List/Watch 特定类型的资源对象的所有事件,缓存到内存,供后续快速查找,根据事件类型,调用用户注册的回调函数。 28 | 29 | [前面分析了] (3.listwatch-reflector-controller.md) NewInformer()、NewIndexInformer() 函数使用 controller 的 Reflector List/Watch 特定资源类型的对象,缓存到本地,并调用用户设置的 OnAdd/OnUpdate/OnDelete 回调函数(保存在 ResourceEventHandler 中)。 30 | 31 | 这两个函数返回的 Store 和 Index 都缓存了从 apiserver List/Watch 更新的资源类型对象,并保持与 etcd 同步。另外,可以使用 Index 创建 Lister,进而更方便的从本地缓存中 List 和 Get 符合条件的资源对象。 32 | 33 | 这两个函数只能注册一组 OnAdd/OnUpdate/OnDelete 回调函数,如果需要注册多组回调函数(例如 kube-controller-manager)但它们又共享一份 Indexer 缓存,则可以使用 SharedInformer 或 SharedIndexInformer。 34 | 35 | SharedInformer 提供一个共享的对象缓存,并且可以将缓存中对象的变化事件(Add/Update/Deleted)分发给多个通过 AddEventHandler() 方法添加的 listeners。需要注意的是通知事件包含的对象与缓存中的对象可能不一致,但是缓存中的对象一定是最新的,例如:同一个对象先后发送了两个 Update 事件,这时缓存中时最新 Update 的结果,但是先处理 Update 事件的回调函数从事件中拿到的对象时旧的。如果现实 Update 再 Deleted,则 Update 的回调函数可能从缓存中找不到对象。 36 | 37 | 另外,SharedInformer 内部使用循环队列,先将对象从 DeltaFIFO Pop 出来,然后再调用回调函数。 38 | 39 | SharedInformer 和 SharedIndexInformer 一般和 workqueue 同时使用,具体参考:[8.customize-controller.md](8.customize-controller.md) 40 | 41 | 在分析 SharedInformer 和 SharedIndexInformer 之前,先分析它使用的 processorListener 和 sharedProcessor 结构类型。 42 | 43 | ## processorListener 44 | 45 | processorListener 封装了监听器处理函数 ResourceEventHandler 以及 RingGrowing 类型的循环队列。 46 | 47 | 它通过 addCh Channel 接收对象,保存到循环队列 pendingNotifications 中缓存(队列大),然后 pop 到 nextCh,最后 run() 方法获得该对象,调用监听器函数。 48 | 49 | ``` go 50 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 51 | type processorListener struct { 52 | // nextCh 保存从 pendingNotifications.ReadOne() 读取的对象; 53 | nextCh chan interface{} 54 | // addCh 用于接收 add() 方法发送的对象,pop 方法读取它后 endingNotifications.WriteOne(notificationToAdd) 该对象; 55 | addCh chan interface{} 56 | 57 | // 用户实际配置的回调函数 58 | handler ResourceEventHandler 59 | 60 | // 循环队列,默认缓存 1024 个对象,从而提供了事件缓冲的能力 61 | pendingNotifications buffer.RingGrowing 62 | 63 | // 创建 listner 时,用户指定的 Resync 周期 64 | requestedResyncPeriod time.Duration 65 | 66 | // 该 listener 实际使用的 Resync 周期,一般是所有 listner 周期的最小值,所以可能与 requestedResyncPeriod 不同 67 | resyncPeriod time.Duration 68 | 69 | // nextResync is the earliest time the listener should get a full resync 70 | nextResync time.Time 71 | // resyncLock guards access to resyncPeriod and nextResync 72 | resyncLock sync.Mutex 73 | } 74 | ``` 75 | 76 | ### add() 方法 77 | 78 | add() 方法将通知对象写入到 p.addCh Channel 中,如果 pendingNotifications 未满(默认 1024)则可以直接写入,而**不需要等待** ResourceEventHandler 处理完毕。 79 | 80 | ``` go 81 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 82 | func (p *processorListener) add(notification interface{}) { 83 | p.addCh <- notification 84 | } 85 | ``` 86 | 87 | ### pop() 方法 88 | 89 | pop() 方法是 processorListener 的核心方法,它实现了: 90 | 91 | 1. 从 p.addCh Channel 读取数据,存入循环队列 p.pendingNotifications; 92 | 2. 从 循环队列 p.pendingNotifications 取数据,写入 p.nextCh,供后续 run() 方法读取; 93 | 94 | run() 从 p.nextCh 读取对象,然后调用用户的处理函数,执行时间可能较长,而 pop() 方法通过 channel selector 机制以及循环队列,巧妙地实现了**异步非阻塞**的写入和读取对象。 95 | 96 | 97 | ``` go 98 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 99 | func (p *processorListener) pop() { 100 | defer utilruntime.HandleCrash() 101 | defer close(p.nextCh) // Tell .run() to stop 102 | 103 | var nextCh chan<- interface{} 104 | var notification interface{} 105 | for { 106 | select { 107 | // 先将对象写入 p.nextCh,然后从循环对内中读取一个对象,下一次执行另一个 case 时写入 p.nextCh 108 | case nextCh <- notification: 109 | // Notification dispatched 110 | var ok bool 111 | notification, ok = p.pendingNotifications.ReadOne() 112 | if !ok { // Nothing to pop 113 | nextCh = nil // Disable this select case 114 | } 115 | // 从 p.addCh 读取一个待加入的对象,然后看是否有待通知的对象,如果有则设置好通知返回,否则写入循环队列 116 | case notificationToAdd, ok := <-p.addCh: 117 | if !ok { 118 | return 119 | } 120 | if notification == nil { // No notification to pop (and pendingNotifications is empty) 121 | // Optimize the case - skip adding to pendingNotifications 122 | notification = notificationToAdd 123 | nextCh = p.nextCh 124 | } else { // There is already a notification waiting to be dispatched 125 | p.pendingNotifications.WriteOne(notificationToAdd) 126 | } 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | ### run() 方法 133 | 134 | run() 方法从 p.nextCh channel 中获取通知对象,然后根据对象的类型调用注册的监听器函数。 135 | 136 | ``` go 137 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 138 | func (p *processorListener) run() { 139 | // this call blocks until the channel is closed. When a panic happens during the notification 140 | // we will catch it, **the offending item will be skipped!**, and after a short delay (one second) 141 | // the next notification will be attempted. This is usually better than the alternative of never 142 | // delivering again. 143 | stopCh := make(chan struct{}) 144 | wait.Until(func() { 145 | // this gives us a few quick retries before a long pause and then a few more quick retries 146 | err := wait.ExponentialBackoff(retry.DefaultRetry, func() (bool, error) { 147 | for next := range p.nextCh { 148 | switch notification := next.(type) { 149 | case updateNotification: 150 | p.handler.OnUpdate(notification.oldObj, notification.newObj) 151 | case addNotification: 152 | p.handler.OnAdd(notification.newObj) 153 | case deleteNotification: 154 | p.handler.OnDelete(notification.oldObj) 155 | default: 156 | utilruntime.HandleError(fmt.Errorf("unrecognized notification: %#v", next)) 157 | } 158 | } 159 | // the only way to get here is if the p.nextCh is empty and closed 160 | return true, nil 161 | }) 162 | 163 | // the only way to get here is if the p.nextCh is empty and closed 164 | if err == nil { 165 | close(stopCh) 166 | } 167 | }, 1*time.Minute, stopCh) 168 | } 169 | ``` 170 | 171 | ## sharedProcessor 172 | 173 | sharedProcessor 类型封装了多个 processorListener,用于表示多组用户注册的通知函数。 174 | 175 | ``` go 176 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 177 | type sharedProcessor struct { 178 | // 标记 processor 是否启动 179 | listenersStarted bool 180 | listenersLock sync.RWMutex 181 | listeners []*processorListener 182 | syncingListeners []*processorListener 183 | clock clock.Clock 184 | wg wait.Group 185 | } 186 | ``` 187 | 188 | ### addListener() 和 addListenerLocked() 方法 189 | 190 | addListener() 方法用于向 Processor 添加新的、封装了用户处理函数的 listener,如果 process 的 run() 方法已经在运行,则启动 listener。 191 | 192 | ``` go 193 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 194 | func (p *sharedProcessor) addListener(listener *processorListener) { 195 | p.listenersLock.Lock() 196 | defer p.listenersLock.Unlock() 197 | 198 | p.addListenerLocked(listener) 199 | if p.listenersStarted { 200 | p.wg.Start(listener.run) 201 | p.wg.Start(listener.pop) 202 | } 203 | } 204 | 205 | func (p *sharedProcessor) addListenerLocked(listener *processorListener) { 206 | p.listeners = append(p.listeners, listener) 207 | p.syncingListeners = append(p.syncingListeners, listener) 208 | } 209 | ``` 210 | 211 | ### distribute() 方法 212 | 213 | 遍历 listeners,调用他们的 add() 方法添加对象。如果 obj 的事件类型是 Sync,则 sync 为 true。 214 | 215 | ``` go 216 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 217 | func (p *sharedProcessor) distribute(obj interface{}, sync bool) { 218 | p.listenersLock.RLock() 219 | defer p.listenersLock.RUnlock() 220 | 221 | if sync { 222 | for _, listener := range p.syncingListeners { 223 | listener.add(obj) 224 | } 225 | } else { 226 | for _, listener := range p.listeners { 227 | listener.add(obj) 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | ### run() 方法 234 | 235 | 运行已经注册的 listeners。 236 | 237 | ``` go 238 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 239 | func (p *sharedProcessor) run(stopCh <-chan struct{}) { 240 | func() { 241 | p.listenersLock.RLock() 242 | defer p.listenersLock.RUnlock() 243 | for _, listener := range p.listeners { 244 | p.wg.Start(listener.run) 245 | p.wg.Start(listener.pop) 246 | } 247 | p.listenersStarted = true 248 | }() 249 | <-stopCh 250 | p.listenersLock.RLock() 251 | defer p.listenersLock.RUnlock() 252 | for _, listener := range p.listeners { 253 | close(listener.addCh) // Tell .pop() to stop. .pop() will tell .run() to stop 254 | } 255 | p.wg.Wait() // Wait for all .pop() and .run() to stop 256 | } 257 | ``` 258 | 259 | ### shouldResync() 方法 260 | 261 | 根据已经注册的所有 listerns,判断所有需要 syncing 的 listeners。 262 | 263 | ``` go 264 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 265 | func (p *sharedProcessor) shouldResync() bool { 266 | p.listenersLock.Lock() 267 | defer p.listenersLock.Unlock() 268 | 269 | p.syncingListeners = []*processorListener{} 270 | 271 | resyncNeeded := false 272 | now := p.clock.Now() 273 | for _, listener := range p.listeners { 274 | // need to loop through all the listeners to see if they need to resync so we can prepare any 275 | // listeners that are going to be resyncing. 276 | if listener.shouldResync(now) { 277 | resyncNeeded = true 278 | p.syncingListeners = append(p.syncingListeners, listener) 279 | listener.determineNextResync(now) 280 | } 281 | } 282 | return resyncNeeded 283 | } 284 | 285 | func (p *sharedProcessor) resyncCheckPeriodChanged(resyncCheckPeriod time.Duration) { 286 | p.listenersLock.RLock() 287 | defer p.listenersLock.RUnlock() 288 | 289 | for _, listener := range p.listeners { 290 | resyncPeriod := determineResyncPeriod(listener.requestedResyncPeriod, resyncCheckPeriod) 291 | listener.setResyncPeriod(resyncPeriod) 292 | } 293 | } 294 | ``` 295 | 296 | ## SharedInformer 和 SharedIndexInformer 297 | 298 | ``` go 299 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 300 | type SharedInformer interface { 301 | // AddEventHandler adds an event handler to the shared informer using the shared informer's resync 302 | // period. Events to a single handler are delivered sequentially, but there is no coordination 303 | // between different handlers. 304 | AddEventHandler(handler ResourceEventHandler) 305 | // AddEventHandlerWithResyncPeriod adds an event handler to the shared informer using the 306 | // specified resync period. Events to a single handler are delivered sequentially, but there is 307 | // no coordination between different handlers. 308 | AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) 309 | // GetStore returns the Store. 310 | GetStore() Store 311 | // GetController gives back a synthetic interface that "votes" to start the informer 312 | GetContrtarts the shared informer, which will be stopped when stopCh is closed. 313 | Run(stopoller() Controller 314 | // Run sCh <-chan struct{}) 315 | // HasSynced returns true if the shared informer's store has synced. 316 | HasSynced() bool 317 | // LastSyncResourceVersion is the resource version observed when last synced with the underlying 318 | // store. The value returned is not synchronized with access to the underlying store and is not 319 | // thread-safe. 320 | LastSyncResourceVersion() string 321 | } 322 | 323 | type SharedIndexInformer interface { 324 | SharedInformer 325 | // AddIndexers add indexers to the informer before it starts. 326 | AddIndexers(indexers Indexers) error 327 | GetIndexer() Indexer 328 | } 329 | ``` 330 | 331 | 函数 NewSharedInformer() 和 NewSharedIndexInformer() 分别返回实现这两个接口的对象 sharedIndexInformer 实例: 332 | 333 | ``` go 334 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 335 | // NewSharedInformer creates a new instance for the listwatcher. 336 | func NewSharedInformer(lw ListerWatcher, objType runtime.Object, resyncPeriod time.Duration) SharedInformer { 337 | return NewSharedIndexInformer(lw, objType, resyncPeriod, Indexers{}) 338 | } 339 | 340 | // NewSharedIndexInformer creates a new instance for the listwatcher. 341 | func NewSharedIndexInformer(lw ListerWatcher, objType runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers Indexers) SharedIndexInformer { 342 | realClock := &clock.RealClock{} 343 | sharedIndexInformer := &sharedIndexInformer{ 344 | processor: &sharedProcessor{clock: realClock}, 345 | // indexer 即为对象缓存 346 | indexer: NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers), 347 | listerWatcher: lw, 348 | objectType: objType, 349 | resyncCheckPeriod: defaultEventHandlerResyncPeriod, 350 | defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod, 351 | cacheMutationDetector: NewCacheMutationDetector(fmt.Sprintf("%T", objType)), 352 | clock: realClock, 353 | } 354 | return sharedIndexInformer 355 | } 356 | ``` 357 | 358 | + 传给 NewSharedIndexInformer () 的 indexers 一般是 `cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}`,然后用 `DeletionHandlingMetaNamespaceKeyFunc` 作为对象的 KeyFunc 创建 Indexer 缓存; 359 | 360 | 后文会介绍,一般情况下,我们不需要使用 NewSharedInformer() 和 NewSharedIndexInformer() 函数为特定资源类型创建 SharedInformer,而是使用 codegen 为特定资源类型创建的 NewXXXInformer() 和 NewFilteredXXXInformer() 函数来创建。 361 | 362 | ### 实现 SharedIndexInformer 接口的 sharedIndexInformer 类型 363 | 364 | 这两个 Informer 包含: 365 | 366 | 1. 带索引的对象缓存 Indexer; 367 | 2. 内置 Controller,它根据 ListerWatcher、resyncCheckPeriod、objectType 等参数创建 Relfector; 368 | 3. Controller 的 Process 函数为 Infomer 的 HandleDeltas() 方法,后者负责向多组 listener 发送事件; 369 | 370 | ``` go 371 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 372 | type sharedIndexInformer struct { 373 | // indexer 为对象缓存,一般是 NewIndexer() 函数创建的,如:NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) 374 | indexer Indexer 375 | // 从 apiserver List/Watch 对象的 Controller 376 | controller Controller 377 | 378 | // 可以注册多组监听器的共享 processor 379 | processor *sharedProcessor 380 | 381 | cacheMutationDetector CacheMutationDetector 382 | 383 | // 创建 controller 使用的 ListerWatcher 和对象类型 384 | listerWatcher ListerWatcher 385 | objectType runtime.Object 386 | 387 | // 指定 Reflector 的将对象从 indexer 同步到 DeltaFIFO 的周期 388 | // 一般是所有注册的监听函数指定的**最小值**:每次注册监听函数组时,都会比较传入的 period 是否比这个值小,如果小,则使用传入的值 period 设置该值; 389 | resyncCheckPeriod time.Duration 390 | 391 | // 缺省的 ResyncPeriod。使用 AddEventHandler() 方法添加 listener 时,使用该 ResyncPeriod 392 | defaultEventHandlerResyncPeriod time.Duration 393 | 394 | // clock allows for testability 395 | clock clock.Clock 396 | 397 | started, stopped bool 398 | startedLock sync.Mutex 399 | 400 | // blockDeltas gives a way to stop all event distribution so that a late event handler 401 | // can safely join the shared informer. 402 | blockDeltas sync.Mutex 403 | } 404 | ``` 405 | 406 | ### Run() 方法 407 | 408 | sharedIndexInformer 也是基于 Controller 实现,Run() 方法: 409 | 410 | 1. 创建对象历史事件缓存 DeltaFIFO; 411 | 2. 创建 Controller,它的 Process 函数是支持向多个 listener 派发对象事件的 s.HandleDeltas() 方法; 412 | 3. 启动 processer,用于处理分发对象事件; 413 | 4. 启动 Controller,进而启动 Reflector 和 Pop 处理过程; 414 | 415 | ``` go 416 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 417 | func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) { 418 | defer utilruntime.HandleCrash() 419 | 420 | // 传入对象缓存 s.indexer(参考 NewSharedIndexInformer() 函数,一般是 NewIndexer() 函数创建的),创建 DeltaFIFO 421 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, s.indexer) 422 | 423 | // 使用 DeltaFIFO 创建 Controller 的配置 424 | cfg := &Config{ 425 | Queue: fifo, 426 | ListerWatcher: s.listerWatcher, 427 | ObjectType: s.objectType, 428 | FullResyncPeriod: s.resyncCheckPeriod, 429 | RetryOnError: false, 430 | ShouldResync: s.processor.shouldResync, 431 | 432 | // Controller 的处理函数是 s.HandleDeltas() 方法 433 | Process: s.HandleDeltas, 434 | } 435 | 436 | func() { 437 | s.startedLock.Lock() 438 | defer s.startedLock.Unlock() 439 | 440 | // 创建 Controller 441 | s.controller = New(cfg) 442 | s.controller.(*controller).clock = s.clock 443 | s.started = true 444 | }() 445 | 446 | // Separate stop channel because Processor should be stopped strictly after controller 447 | processorStopCh := make(chan struct{}) 448 | var wg wait.Group 449 | defer wg.Wait() // Wait for Processor to stop 450 | defer close(processorStopCh) // Tell Processor to stop 451 | wg.StartWithChannel(processorStopCh, s.cacheMutationDetector.Run) 452 | // 启动 processer,用于处理分发对象事件 453 | wg.StartWithChannel(processorStopCh, s.processor.run) 454 | 455 | defer func() { 456 | s.startedLock.Lock() 457 | defer s.startedLock.Unlock() 458 | s.stopped = true // Don't want any new listeners 459 | }() 460 | // 运行 Controller 461 | s.controller.Run(stopCh) 462 | } 463 | ``` 464 | 465 | ### HasSynced() 方法 466 | 467 | 该方法在写自定义 Controller 时经常用到,用于判断 Informer Cache 是否完成同步,然后再启动 workqueue 的 worker,具体参考:[3.listwatch-reflector-controller.md](3.listwatch-reflector-controller.md#hassynced-方法) 468 | 469 | ``` go 470 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 471 | func (s *sharedIndexInformer) HasSynced() bool { 472 | s.startedLock.Lock() 473 | defer s.startedLock.Unlock() 474 | 475 | if s.controller == nil { 476 | return false 477 | } 478 | return s.controller.HasSynced() 479 | } 480 | ``` 481 | 482 | ### AddEventHandler() 方法 483 | 484 | 该方法用于注册监听函数,使用**默认的同步周期**,可以调用多次,从而注册多组监听函数: 485 | 486 | ``` go 487 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 488 | func (s *sharedIndexInformer) AddEventHandler(handler ResourceEventHandler) { 489 | // 参考后文对 AddEventHandlerWithResyncPeriod 的分析 490 | s.AddEventHandlerWithResyncPeriod(handler, s.defaultEventHandlerResyncPeriod) 491 | } 492 | ``` 493 | 494 | 结构体类型 `ResourceEventHandlerFuncs` 和 `FilteringResourceEventHandler` 实现了 `ResourceEventHandler` 接口: 495 | 496 | ``` go 497 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 498 | type ResourceEventHandler interface { 499 | OnAdd(obj interface{}) 500 | OnUpdate(oldObj, newObj interface{}) 501 | OnDelete(obj interface{}) 502 | } 503 | 504 | type ResourceEventHandlerFuncs struct { 505 | AddFunc func(obj interface{}) 506 | UpdateFunc func(oldObj, newObj interface{}) 507 | DeleteFunc func(obj interface{}) 508 | } 509 | 510 | type FilteringResourceEventHandler struct { 511 | FilterFunc func(obj interface{}) bool 512 | Handler ResourceEventHandler 513 | } 514 | ``` 515 | 516 | FilteringResourceEventHandler 在调用 Handler 的方法前,会先用 FilterFunc 对对象进行过滤,通过过滤的对象才调用 Handler 处理。 517 | 518 | 实际例子: 519 | 520 | ``` go 521 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 522 | fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 523 | AddFunc: controller.enqueueFoo, 524 | UpdateFunc: func(old, new interface{}) { 525 | controller.enqueueFoo(new) 526 | }, 527 | }) 528 | 529 | deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 530 | AddFunc: controller.handleObject, 531 | UpdateFunc: func(old, new interface{}) { 532 | newDepl := new.(*appsv1.Deployment) 533 | oldDepl := old.(*appsv1.Deployment) 534 | if newDepl.ResourceVersion == oldDepl.ResourceVersion { 535 | // Periodic resync will send update events for all known Deployments. 536 | // Two different versions of the same Deployment will always have different RVs. 537 | return 538 | } 539 | controller.handleObject(new) 540 | }, 541 | DeleteFunc: controller.handleObject, 542 | }) 543 | ``` 544 | 545 | 对于 DeleteFunc() 函数而言,传入的 obj 可能是 K8S 资源对象,也可能说 DeletedFinalStateUnknown 类型对象,所以它需要区分: 546 | 547 | ``` go 548 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 549 | // handleObject 是 DeleteFunc 指定的函数 550 | func (c *Controller) handleObject(obj interface{}) { 551 | var object metav1.Object 552 | var ok bool 553 | // 对接口类型的 obj 类型进行断言,先是 K8S 资源类型 554 | if object, ok = obj.(metav1.Object); !ok { 555 | // 如果失败,则可能是 DeletedFinalStateUnknown 类型对象 556 | tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 557 | if !ok { 558 | utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) 559 | return 560 | } 561 | object, ok = tombstone.Obj.(metav1.Object) 562 | if !ok { 563 | utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) 564 | return 565 | } 566 | klog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName()) 567 | } 568 | ... 569 | } 570 | ``` 571 | 572 | ### AddEventHandlerWithResyncPeriod() 方法 573 | 574 | 该方法实际负责注册监听函数,resyncPeriod 参数用于指定需求的 DeltaFIFO 同步周期。 575 | 576 | 该放的主要工作如下: 577 | 578 | 1. 比较传本次传入的 resyncPeriod 与当前全局同步周期(s.resyncCheckPeriod),使用**较小的值**作为全局同步周期。 579 | 2. 创建监听器; 580 | 3. 注册监听器; 581 | 582 | 如果在 sharedIndexInformer 已经 Run 的情况下注册监听器,则该访问将遍历对象缓存,**为所有对象生成 Add 事件**,调用传入的事件处理函数。 583 | 这样可以保证即使后注册监听器,也不会丢失对象的历史事件。 584 | 585 | ``` go 586 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 587 | // DeltaFIFO 的最小同步周期是 1s 588 | const minimumResyncPeriod = 1 * time.Second 589 | 590 | const initialBufferSize = 1024 591 | 592 | func (s *sharedIndexInformer) AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) { 593 | s.startedLock.Lock() 594 | defer s.startedLock.Unlock() 595 | 596 | if s.stopped { 597 | klog.V(2).Infof("Handler %v was not added to shared informer because it has stopped already", handler) 598 | return 599 | } 600 | 601 | if resyncPeriod > 0 { 602 | if resyncPeriod < minimumResyncPeriod { 603 | klog.Warningf("resyncPeriod %d is too small. Changing it to the minimum allowed value of %d", resyncPeriod, minimumResyncPeriod) 604 | resyncPeriod = minimumResyncPeriod 605 | } 606 | 607 | if resyncPeriod < s.resyncCheckPeriod { 608 | if s.started { 609 | klog.Warningf("resyncPeriod %d is smaller than resyncCheckPeriod %d and the informer has already started. Changing it to %d", resyncPeriod, s.resyncCheckPeriod, s.resyncCheckPeriod) 610 | resyncPeriod = s.resyncCheckPeriod 611 | } else { 612 | // if the event handler's resyncPeriod is smaller than the current resyncCheckPeriod, update 613 | // resyncCheckPeriod to match resyncPeriod and adjust the resync periods of all the listeners 614 | // accordingly 615 | s.resyncCheckPeriod = resyncPeriod 616 | s.processor.resyncCheckPeriodChanged(resyncPeriod) 617 | } 618 | } 619 | } 620 | 621 | // 创建监听器 622 | listener := newProcessListener(handler, resyncPeriod, determineResyncPeriod(resyncPeriod, s.resyncCheckPeriod), s.clock.Now(), initialBufferSize) 623 | 624 | // informer 未启动时,直接注册监听器即可 625 | if !s.started { 626 | // 注册监听器 627 | s.processor.addListener(listener) 628 | return 629 | } 630 | 631 | // 如果 informer 已经启动,为了安全地 Join: 632 | // 1. stop sending add/update/delete notifications 633 | // 2. do a list against the store 634 | // 3. send synthetic "Add" events to the new handler 635 | // 4. unblock 636 | 637 | // 获取发送事件的锁(因为后续要遍历对象缓存,加锁的话,可以防止其它 goroutine 更新对象缓存) 638 | s.blockDeltas.Lock() 639 | defer s.blockDeltas.Unlock() 640 | // 注册监听器 641 | s.processor.addListener(listener) 642 | // 遍历对象缓存中的各对象,为它们生成 Add 事件,然后发送给新的 Handler; 643 | for _, item := range s.indexer.List() { 644 | listener.add(addNotification{newObj: item}) 645 | } 646 | } 647 | ``` 648 | 649 | ### HandleDeltas() 方法 650 | 651 | sharedIndexInformer 内置的 Controller 从 DeltaFIFO Pop 出对象(Deltas 类型)时调用该方法: 652 | 1. 遍历 Deltas 中的 Delta; 653 | 2. 根据 Delta 的事件类型,更新缓存,分发对应的事件,从而执行各监听器函数; 654 | 655 | ``` go 656 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 657 | func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { 658 | s.blockDeltas.Lock() 659 | defer s.blockDeltas.Unlock() 660 | 661 | // from oldest to newest 662 | // Controller 传入的 obj 实际是从 Reflector.Pop() 方法返回的 Deltas,它是 Delta 的列表 663 | for _, d := range obj.(Deltas) { 664 | switch d.Type { 665 | case Sync, Added, Updated: 666 | isSync := d.Type == Sync 667 | s.cacheMutationDetector.AddObject(d.Object) 668 | // 查找对象缓存,看是否存在该对象 669 | if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { 670 | // 如果存在则更新缓存, 671 | if err := s.indexer.Update(d.Object); err != nil { 672 | return err 673 | } 674 | // 分发 update 事件 675 | s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) 676 | } else { 677 | // 缓存中没有该对象,则添加到缓存 678 | if err := s.indexer.Add(d.Object); err != nil { 679 | return err 680 | } 681 | // 分发 add 事件 682 | s.processor.distribute(addNotification{newObj: d.Object}, isSync) 683 | } 684 | case Deleted: 685 | if err := s.indexer.Delete(d.Object); err != nil { 686 | return err 687 | } 688 | s.processor.distribute(deleteNotification{oldObj: d.Object}, false) 689 | } 690 | } 691 | return nil 692 | } 693 | ``` 694 | 695 | ## WaitForCacheSync() 函数 696 | 697 | 该方法用于等待 stopCh 关闭(返回的 err 不为 nil),或者 cacheSyncs 列表的所有 InformerSynced 类型的函数都返回 true(返回的 err 为 nil): 698 | 699 | ``` go 700 | // 来源于:k8s.io/client-go/tools/cache/shared_informer.go 701 | syncedPollPeriod = 100 * time.Millisecond 702 | func WaitForCacheSync(stopCh <-chan struct{}, cacheSyncs ...InformerSynced) bool { 703 | // wait.PollUntil 会每隔 100ms 执行匿名函数,直到它返回 true,或者 stopCh 被关闭 704 | err := wait.PollUntil(syncedPollPeriod, 705 | func() (bool, error) { 706 | for _, syncFunc := range cacheSyncs { 707 | if !syncFunc() { 708 | return false, nil 709 | } 710 | } 711 | return true, nil 712 | }, 713 | stopCh) 714 | if err != nil { 715 | klog.V(2).Infof("stop requested") 716 | return false 717 | } 718 | 719 | klog.V(4).Infof("caches populated") 720 | return true 721 | } 722 | ``` 723 | + 返回 true 时表示 Informer Cache 都 Synced; 724 | + 返回 false 时表示 stopCh 被管理; 725 | 726 | 实例例子: 727 | 728 | ``` go 729 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 730 | func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { 731 | ... 732 | // c.deploymentsSynced = deploymentInformer.Informer().HasSynced 733 | if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok { 734 | return fmt.Errorf("failed to wait for caches to sync") 735 | } 736 | ... 737 | } 738 | ``` 739 | 740 | ## codegen 为特定资源类型创建的 SharedIndexInformer 741 | 742 | 一般情况下,我们不需要使用 NewSharedInformer() 和 NewSharedIndexInformer() 函数为特定资源类型创建 SharedInformer,而是使用 codegen 为特定资源类型创建的 NewXXXInformer() 和 NewFilteredXXXInformer() 函数来创建。 743 | 744 | ``` go 745 | // 来源于:k8s.io/client-go/informers/apps/v1/deployment.go 746 | func NewDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 747 | return NewFilteredDeploymentInformer(client, namespace, resyncPeriod, indexers, nil) 748 | } 749 | 750 | func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 751 | return cache.NewSharedIndexInformer( 752 | &cache.ListWatch{ 753 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 754 | if tweakListOptions != nil { 755 | tweakListOptions(&options) 756 | } 757 | return client.AppsV1().Deployments(namespace).List(options) 758 | }, 759 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 760 | if tweakListOptions != nil { 761 | tweakListOptions(&options) 762 | } 763 | return client.AppsV1().Deployments(namespace).Watch(options) 764 | }, 765 | }, 766 | &appsv1.Deployment{}, 767 | resyncPeriod, 768 | indexers, 769 | ) 770 | } 771 | 772 | func (f *deploymentInformer) Informer() cache.SharedIndexInformer { 773 | return f.factory.InformerFor(&appsv1.Deployment{}, f.defaultInformer) 774 | } 775 | 776 | func (f *deploymentInformer) Lister() v1.DeploymentLister { 777 | return v1.NewDeploymentLister(f.Informer().GetIndexer()) 778 | } 779 | ``` 780 | 781 | + client 一般是 client-go 的 kubernets 或 CRD 的 clientset,如: 782 | 783 | ``` go 784 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/main.go 785 | cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) 786 | if err != nil { 787 | klog.Fatalf("Error building kubeconfig: %s", err.Error()) 788 | } 789 | 790 | kubeClient, err := kubernetes.NewForConfig(cfg) 791 | if err != nil { 792 | klog.Fatalf("Error building kubernetes clientset: %s", err.Error()) 793 | } 794 | 795 | exampleClient, err := clientset.NewForConfig(cfg) 796 | if err != nil { 797 | klog.Fatalf("Error building example clientset: %s", err.Error()) 798 | } 799 | ``` 800 | 801 | + 传给 NewFilteredDeploymentInformer() 函数的 indexers 一般是 `cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}`。 802 | 803 | 一般情况下,我们不直接使用上面的 NewXXX 函数创建各资源类型的 SharedInformer,而是使用 codegen 生成的 sharedInformerFactory 来创建它们,具体参考:[6.sharedInformerFactory.md](6.sharedInformerFactory.md) 804 | 805 | -------------------------------------------------------------------------------- /client-go/5.lister.md: -------------------------------------------------------------------------------- 1 | # Lister 接口 2 | 3 | 4 | 5 | - [Lister 接口](#lister-接口) 6 | - [GenericLister 和 GenericNamespaceLister 接口](#genericlister-和-genericnamespacelister-接口) 7 | - [codegent 生成的特定资源类型的 Informer 和 Lister](#codegent-生成的特定资源类型的-informer-和-lister) 8 | 9 | 10 | 11 | `Lister` 是可以通过对象名称、命名空间、标签选择器等查询条件 Get 或 List 对象的接口,一般是基于 `Indexer` 实现。 12 | 使用 codegen 工具可以为各资源类型生成类型相关的 `Informer`和 `Lister`。 13 | 14 | ## GenericLister 和 GenericNamespaceLister 接口 15 | 16 | 这两种 `Lister` 接口都基于 `Indexer` 实现,可以通过对象名称、命名空间、标签选择器等查询条件 Get 或 List `Indexer` 中的对象(返回的是实现 runtime.Object 接口的**通用对象类型**,需要再类型转换为特定对象)。 17 | 18 | 两者的差别在于 `GenericLister` 不限 Namespace,而 `GenericNamespaceLister` 只能 Get/List 特定 Namespace 中的对象。 19 | 20 | ``` go 21 | // 来源于 k8s.io/client-go/tools/cache/listers.go 22 | type GenericLister interface { 23 | List(selector labels.Selector) (ret []runtime.Object, err error) 24 | Get(name string) (runtime.Object, error) 25 | ByNamespace(namespace string) GenericNamespaceLister 26 | } 27 | 28 | type GenericNamespaceLister interface { 29 | List(selector labels.Selector) (ret []runtime.Object, err error) 30 | Get(name string) (runtime.Object, error) 31 | } 32 | 33 | type genericLister struct { 34 | indexer Indexer 35 | resource schema.GroupResource 36 | } 37 | 38 | type genericNamespaceLister struct { 39 | indexer Indexer 40 | namespace string 41 | resource schema.GroupResource 42 | } 43 | ``` 44 | 45 | 函数 `NewGenericLister()` 返回一个实现 `GenericLister` 接口的对象: 46 | 47 | ``` go 48 | // 来源于 k8s.io/client-go/tools/cache/listers.go 49 | func NewGenericLister(indexer Indexer, resource schema.GroupResource) GenericLister { 50 | return &genericLister{indexer: indexer, resource: resource} 51 | } 52 | ``` 53 | 54 | + 传入的 resource 只用于错误信息,显示 Group 和 Resource 名称。 55 | 56 | ## codegent 生成的特定资源类型的 Informer 和 Lister 57 | 58 | 实际开发时,一般用 codegen 工具自动生成**对象类型相关的 Informer 和 Lister**,Informer 从 apiserver List/Watch 资源对象,保存到内部 Indexer 缓存, 59 | 保证 Indexer 缓存中的对象和 etcd 一致,所以**可以使用 Informer 创建 Lister**,而且 Lister 中的对象和 etcd 一致: 60 | 61 | ``` go 62 | kubeInformerFactory := kubeinformers.NewFilteredSharedInformerFactory(kubeClient, config.RsyncPeriod, metav1.NamespaceAll, listOptionsFunc) 63 | // 创建 Informer 64 | deployInformer := kubeInformerFactory.Extensions().V1beta1().Deployments() 65 | // 从 Informer 获取 Lister 66 | aolDeployLister := aolDeployInformer.Lister() 67 | // 通过 Lister List 对象 68 | aolDeployes, err := aolDeployLister.List(labels.Selector{"app": "aol"}) 69 | // 通过 Lister Get 对象 70 | aolDeploy, err := aolDeployLister.Deployments("my-namespace").Get("deployName") 71 | ``` 72 | 73 | client-go 包的 `lister` 和 `informers` 目录下,分别有各内置对象的 `Lister` 和 `Informer` 定义,如 `DeployLister`: 74 | 75 | ``` go 76 | // 来源于 k8s.io/client-go/informers/apps/v1/deployment.go 77 | type DeploymentInformer interface { 78 | // 返回通用的 SharedIndexInformer 79 | Informer() cache.SharedIndexInformer 80 | // 返回对象相关的 Lister 81 | Lister() v1.DeploymentLister 82 | } 83 | // 对象的 Informer 包含 Indexer,所以可以使用 Informer 创建 Lister 84 | func (f *deploymentInformer) Lister() v1.DeploymentLister { 85 | return v1.NewDeploymentLister(f.Informer().GetIndexer()) 86 | } 87 | ``` 88 | 89 | 函数 NewDeploymentLister 返回一个 DeploymentLister,传入的是从 DeploymentInformer 获取的 Indexer: 90 | 91 | ``` go 92 | // 来源于 k8s.io/client-go/listers/apps/v1/deployment.go 93 | // NewDeploymentLister returns a new DeploymentLister. 94 | func NewDeploymentLister(indexer cache.Indexer) DeploymentLister { 95 | return &deploymentLister{indexer: indexer} 96 | } 97 | 98 | type DeploymentLister interface { 99 | // 列出所有 Namespace 中匹配 selector 的 Deployment 对象列表(非 runtime.Object 列表); 100 | List(selector labels.Selector) (ret []*v1.Deployment, err error) 101 | Deployments(namespace string) DeploymentNamespaceLister 102 | DeploymentListerExpansion 103 | } 104 | ``` 105 | 106 | `DeploymentNamespaceLister` Get/List 特定 Namespace 中的 Deployment: 107 | 108 | ``` go 109 | // 来源于 k8s.io/client-go/listers/apps/v1/deployment.go 110 | type DeploymentNamespaceLister interface { 111 | // List lists all Deployments in the indexer for a given namespace. 112 | List(selector labels.Selector) (ret []*v1.Deployment, err error) 113 | // Get retrieves the Deployment from the indexer for a given namespace and name. 114 | Get(name string) (*v1.Deployment, error) 115 | DeploymentNamespaceListerExpansion 116 | } 117 | ``` -------------------------------------------------------------------------------- /client-go/6.sharedInformerFactory.md: -------------------------------------------------------------------------------- 1 | # SharedInformerFactory 接口 2 | 3 | 4 | 5 | - [SharedInformerFactory 接口](#sharedinformerfactory-接口) 6 | - [自定向下](#自定向下) 7 | - [internalinterfaces.SharedInformerFactory 接口](#internalinterfacessharedinformerfactory-接口) 8 | - [实现 SharedInformerFactory 接口的类型 sharedInformerFactory](#实现-sharedinformerfactory-接口的类型-sharedinformerfactory) 9 | - [InformerFor() 方法](#informerfor-方法) 10 | - [Start() 方法](#start-方法) 11 | - [ForResource() 方法](#forresource-方法) 12 | - [WaitForCacheSync() 方法](#waitforcachesync-方法) 13 | - [使用 InformerFactory 创建特定资源类型的 SharedIndexInformer 过程分析](#使用-informerfactory-创建特定资源类型的-sharedindexinformer-过程分析) 14 | - [extensions informers](#extensions-informers) 15 | - [extensions v1beat1 informers](#extensions-v1beat1-informers) 16 | - [GenericInformer 接口](#genericinformer-接口) 17 | 18 | 19 | 20 | 由于 K8S 支持很多种资源类型,有些 Controller 需要能注册和接收多种资源类型的事件并提供本地缓存,如果挨个为每种类型创建 Informer/Lister 则代码显得有点臃肿。 21 | 22 | SharedInformerFactory 是 codegen 生成的,可以用来创建各资源类型 SharedInformer 的工厂函数。 23 | 24 | SharedInformerFactory 的 InformerFor() 方法,使用传入的资源对象的 NewInformerFunc 类型函数从 K8S ClientSet 创建对象相关的实现 cache.SharedIndexInformer 接口的对象。 25 | 26 | 在[实现自定义 Controller](7.customize-controller.md) 时,一般先后创建 clienset -> InformerFactory -> Infofmer -> Lister 27 | 28 | ## 自定向下 29 | 30 | ``` go 31 | // 创建 K8S Client 配置参数 32 | cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) 33 | 34 | // 根据配置参数创建一个 K8S Client 35 | kubeClient, err := kubernetes.NewForConfig(cfg) 36 | 37 | // 使用 K8S Client 创建一个 SharedInformerFactory 38 | kubeInformerFactory := kubeinformers.NewFilteredSharedInformerFactory(kubeClient, time.Second*30, "default", nil) 39 | 40 | // 从 SharedInformerFactory 创建一个 DeploymentInformer 41 | // 注意:该过程会将 DeploymentInformer 注册到 kubeInformerFactory 42 | deployInformer extensionslistersv1beta1.DeploymentInformer := kubeInformerFactory.Extensions().V1beta1().Deployments() 43 | 44 | // 运行 kubeInformerFactory 中已注册的所有 Infomer,所以必须在创建 DeploymentInformer 之后才能执行 kubeInformerFactory 的 Start 方法! 45 | kubeInformerFactory.Start(stopCh) 46 | 47 | // 再从 DeploymentInformer 创建 DeploymentLister 48 | deployLister extensionslistersv1beta1.DeploymentLister := deployInformer.Lister() 49 | 50 | // 从 DeploymentLister 查询 Deployment 51 | deploy, err := c.deployLister.Deployments(aolDeploy.ObjectMeta.Namespace).Get(aolDeployName) 52 | ``` 53 | 54 | ## internalinterfaces.SharedInformerFactory 接口 55 | 56 | 在介绍 SharedInformerFactory 前,先介绍它使用的 internalinterfaces.SharedInformerFactory 接口: 57 | 58 | ``` go 59 | // 来源于 k8s.io/client-go/informers/internalinterfaces/factory_interfaces.go 60 | // 根据传入的 K8S Client 和同步时间,创建一个实现 cache.SharedIndexInformer 接口的对象 61 | type NewInformerFunc func(kubernetes.Interface, time.Duration) cache.SharedIndexInformer 62 | 63 | type SharedInformerFactory interface { 64 | // 开始运行 SharedInformerFactory 65 | Start(stopCh <-chan struct{}) 66 | // 使用 newFunc 为特定资源对象 obj 创建一个实现 cache.SharedIndexInformer 接口的对象 67 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 68 | } 69 | 70 | type TweakListOptionsFunc func(*v1.ListOptions) 71 | ``` 72 | 73 | 然后看看 SharedInformerFactory 接口的定义: 74 | 75 | ``` go 76 | // 来源于 k8s.io/client-go/informers/factory.go 77 | type SharedInformerFactory interface { 78 | internalinterfaces.SharedInformerFactory 79 | 80 | // 为特定 Group Version 的 Resource 创建一个实现 GenericInformer 接口的对象 81 | ForResource(resource schema.GroupVersionResource) (GenericInformer, error) 82 | 83 | // 等待 Cache 都同步完毕 84 | WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 85 | 86 | // 以下这些是 K8S 内置的各 API Group 87 | Admissionregistration() admissionregistration.Interface 88 | Apps() apps.Interface 89 | Auditregistration() auditregistration.Interface 90 | Autoscaling() autoscaling.Interface 91 | Batch() batch.Interface 92 | Certificates() certificates.Interface 93 | Coordination() coordination.Interface 94 | Core() core.Interface 95 | Events() events.Interface 96 | Extensions() extensions.Interface 97 | Networking() networking.Interface 98 | Policy() policy.Interface 99 | Rbac() rbac.Interface 100 | Scheduling() scheduling.Interface 101 | Settings() settings.Interface 102 | Storage() storage.Interface 103 | } 104 | ``` 105 | 106 | SharedInformerFactory 可以为各 API Group 下所有版本的资源对象创建对应的 SharedInformer,所以称之为 Factory。 107 | 108 | 函数 `NewSharedInformerFactory()`、`NewFilteredSharedInformerFactory()`、`NewSharedInformerFactoryWithOptions()` 返回实现 SharedInformerFactory 接口的内置类型 `sharedInformerFactory` 的实例: 109 | 110 | ``` go 111 | // 来源于 k8s.io/client-go/informers/factory.go 112 | // 创建一个监控所有 Namespace 中特定类型资源对象的 sharedInformerFactory 实例 113 | func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { 114 | return NewSharedInformerFactoryWithOptions(client, defaultResync) 115 | } 116 | 117 | // 创建一个监控指定 Namespace,用 List 选项过滤结果的 sharedInformerFactory 实例 118 | // 该函数已过时,应该使用 NewSharedInformerFactoryWithOptions 替换它 119 | func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { 120 | return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) 121 | } 122 | 123 | // 根据指定选项创建一个 sharedInformerFactory 实例 124 | func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { 125 | factory := &sharedInformerFactory{ 126 | client: client, 127 | namespace: v1.NamespaceAll, 128 | defaultResync: defaultResync, 129 | informers: make(map[reflect.Type]cache.SharedIndexInformer), 130 | startedInformers: make(map[reflect.Type]bool), 131 | customResync: make(map[reflect.Type]time.Duration), 132 | } 133 | // 应用配置选项 134 | for _, opt := range options { 135 | factory = opt(factory) 136 | } 137 | return factory 138 | } 139 | ``` 140 | 141 | client-go 的 `informers` package 提供了如下三个预定义的 SharedInformerOption: 142 | 143 | 1. WithCustomResyncConfig:为指定资源类型指定 Resync 同步周期; 144 | 2. WithTweakListOptions:使用 internalinterfaces.TweakListOptionsFunc 更新 SharedInformerOption; 145 | 3. WithNamespace:指定只监控指定的 Namespace; 146 | 147 | ## 实现 SharedInformerFactory 接口的类型 sharedInformerFactory 148 | 149 | sharedInformerFactory 类型定义如下: 150 | 151 | ``` go 152 | // 来源于 k8s.io/client-go/informers/factory.go 153 | type sharedInformerFactory struct { 154 | // Kubernetes ClientSet,SharedInformer 使用它和 apiserver 通信,ListAndWatch 特定类型资源对象 155 | client kubernetes.Interface 156 | // 监控的 Namespace 157 | namespace string 158 | // List 选项 159 | tweakListOptions internalinterfaces.TweakListOptionsFunc 160 | lock sync.Mutex 161 | // 缺省的 Rsync 周期 162 | defaultResync time.Duration 163 | // 各资源对象类型对应的 Rsync 周期 164 | customResync map[reflect.Type]time.Duration 165 | // 各资源对象类型对应的 cache.SharedIndexInformer 166 | informers map[reflect.Type]cache.SharedIndexInformer 167 | // 记录各资源对象类型的 cache.SharedIndexInformer 是否以启动运行 168 | startedInformers map[reflect.Type]bool 169 | } 170 | ``` 171 | 172 | ### InformerFor() 方法 173 | 174 | 为特定资源对象类型创建 cache.SharedIndexInformer,并将他们注册到 sharedInformerFactory: 175 | 176 | ``` go 177 | // 来源于 k8s.io/client-go/informers/factory.go 178 | // InternalInformerFor returns the SharedIndexInformer for obj using an internal 179 | // client. 180 | func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { 181 | f.lock.Lock() 182 | defer f.lock.Unlock() 183 | 184 | informerType := reflect.TypeOf(obj) 185 | informer, exists := f.informers[informerType] 186 | if exists { 187 | return informer 188 | } 189 | 190 | resyncPeriod, exists := f.customResync[informerType] 191 | if !exists { 192 | resyncPeriod = f.defaultResync 193 | } 194 | 195 | informer = newFunc(f.client, resyncPeriod) 196 | f.informers[informerType] = informer 197 | 198 | return informer 199 | } 200 | ``` 201 | 202 | 后文会以 Deployment 为例介绍,各内置 K8S 资源对象是如何注册到 sharedInformerFactory 的。 203 | 204 | ### Start() 方法 205 | 206 | 运行所有**已注册的资源对象类型**的 cache.SharedIndexInformer: 207 | 208 | ``` go 209 | // 来源于 k8s.io/client-go/informers/factory.go 210 | // Start initializes all requested informers. 211 | func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { 212 | f.lock.Lock() 213 | defer f.lock.Unlock() 214 | 215 | for informerType, informer := range f.informers { 216 | // 如果对应类型的 informer 没有运行,则运行它 217 | if !f.startedInformers[informerType] { 218 | go informer.Run(stopCh) 219 | f.startedInformers[informerType] = true 220 | } 221 | } 222 | } 223 | ``` 224 | 225 | 由于有锁保护,同时保存的有运行状态指示,所以可以并发、多次调用 Start() 方法,从而确保新注册的 Informer 能被运行。 226 | 227 | ### ForResource() 方法 228 | 229 | 返回实现 GenericInformer 接口的对象,该接口的定义如下: 230 | 231 | ``` go 232 | // 来源于 8s.io/client-go/informers/generic.go 233 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 234 | switch resource { 235 | ... 236 | // Group=extensions, Version=v1beta1 237 | case extensionsv1beta1.SchemeGroupVersion.WithResource("deployments"): 238 | return &genericInformer{resource: resource.GroupResource(), informer: f.Extensions().V1beta1().Deployments().Informer()}, nil 239 | ... 240 | } 241 | } 242 | ``` 243 | 244 | ### WaitForCacheSync() 方法 245 | 246 | 等待所有已经启动的 Informer 的 Cache 同步完成: 247 | 248 | ``` go 249 | // 来源于 k8s.io/client-go/informers/factory.go 250 | func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { 251 | // 获取已经启动的 Informer 集合 252 | informers := func() map[reflect.Type]cache.SharedIndexInformer { 253 | f.lock.Lock() 254 | defer f.lock.Unlock() 255 | 256 | informers := map[reflect.Type]cache.SharedIndexInformer{} 257 | for informerType, informer := range f.informers { 258 | if f.startedInformers[informerType] { 259 | informers[informerType] = informer 260 | } 261 | } 262 | return informers 263 | }() 264 | // 等待他们的 Cache 都同步完成 265 | res := map[reflect.Type]bool{} 266 | for informType, informer := range informers { 267 | res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) 268 | } 269 | return res 270 | } 271 | ``` 272 | 273 | TODO: 以 DeltaFIFO 为例,说明 WaitForCacheSync() 方法的意义。 274 | 275 | ## 使用 InformerFactory 创建特定资源类型的 SharedIndexInformer 过程分析 276 | 277 | 以 Extensions api group 下的 Deployment 为例。 278 | 279 | 280 | ``` go 281 | // 从 SharedInformerFactory 创建一个 DeploymentInformer 282 | deployInformer extensionslistersv1beta1.DeploymentInformer := kubeInformerFactory.Extensions().V1beta1().Deployments() 283 | ``` 284 | 285 | sharedInformerFactory 的 Extensions() 方法定义如下: 286 | 287 | ``` go 288 | // 来源于 k8s.io/client-go/informers/factory.go 289 | ... 290 | func (f *sharedInformerFactory) Core() core.Interface { 291 | return core.New(f, f.namespace, f.tweakListOptions) 292 | } 293 | ... 294 | func (f *sharedInformerFactory) Extensions() extensions.Interface { 295 | return extensions.New(f, f.namespace, f.tweakListOptions) 296 | } 297 | ... 298 | ``` 299 | 300 | extensions.New() 函数和 extensions.Interface 都位于 client-go/informers/extensions 目录下。 301 | 302 | ### extensions informers 303 | 304 | ``` go 305 | // 来源于 k8s.io/client-go/informers/extensions 306 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 307 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 308 | } 309 | 310 | type Interface interface { 311 | V1beta1() v1beta1.Interface 312 | } 313 | ``` 314 | 315 | 接着我们看下一级方法调用 V1beta1(): 316 | 317 | ``` go 318 | // 来源于 k8s.io/client-go/informers/extensions 319 | // 继续向下一级传递 factory、namespace 和 tweakListOptions 320 | func (g *group) V1beta1() v1beta1.Interface { 321 | return v1beta1.New(g.factory, g.namespace, g.tweakListOptions) 322 | } 323 | ``` 324 | 325 | externsions 是 API Group,它可以包含多个 API 版本子目录,v1beat1 就是其中一个 API 版本的目录。 326 | 327 | ### extensions v1beat1 informers 328 | 329 | ``` go 330 | // 来源于 k8s.io/client-go/informers/extensions/v1beta1/interface.go 331 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 332 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 333 | } 334 | 335 | type Interface interface { 336 | ... 337 | Deployments() DeploymentInformer 338 | ... 339 | } 340 | 341 | func (v *version) Deployments() DeploymentInformer { 342 | return &deploymentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 343 | } 344 | ``` 345 | 346 | ``` go 347 | // 来源于 k8s.io/client-go/informers/extensions/v1beta1/deployment.go 348 | type DeploymentInformer interface { 349 | Informer() cache.SharedIndexInformer 350 | Lister() v1beta1.DeploymentLister 351 | } 352 | ``` 353 | 354 | f.factory.InformerFor() 只是创建 cache.SharedIndexInformer 并将它注册到 f.factory,并不会实际运行它,故**需要调用返回的 cache.SharedIndexInformer 的 Run() 方法来实际运行它,或者(再)次调用 f.factory 的 Start() 方法(参考 sample-controller 的 main.go,在 NewController() 后调用 factory 的 Start() 方法)**。 355 | 356 | ``` go 357 | // 来源于:k8s.io/client-go/informers/extensions/v1beta1/deployment.go 358 | // 使用 Factory 的 InformerFor() 方法将创建 Depooyment 的函数注册到 Factory; 359 | // 注册的时候传入了 Deployment 的 Scheme 定义!后续的处理会自动编解码; 360 | func (f *deploymentInformer) Informer() cache.SharedIndexInformer { 361 | return f.factory.InformerFor(&extensionsv1beta1.Deployment{}, f.defaultInformer) 362 | } 363 | 364 | // defaultInformer() :使用 K8S Rest Client 创建 Deployment Informer 的方法 365 | func (f *deploymentInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 366 | return NewFilteredDeploymentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 367 | } 368 | 369 | // 调用 K8S 的 clientset 进行 List 和 Watch 370 | func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 371 | return cache.NewSharedIndexInformer( 372 | &cache.ListWatch{ 373 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 374 | if tweakListOptions != nil { 375 | tweakListOptions(&options) 376 | } 377 | return client.ExtensionsV1beta1().Deployments(namespace).List(options) 378 | }, 379 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 380 | if tweakListOptions != nil { 381 | tweakListOptions(&options) 382 | } 383 | return client.ExtensionsV1beta1().Deployments(namespace).Watch(options) 384 | }, 385 | }, 386 | &extensionsv1beta1.Deployment{}, 387 | resyncPeriod, 388 | indexers, 389 | ) 390 | } 391 | 392 | 从 Informer 获取 Lister,注意,都是针对 Deployment 这种特定资源对象的: 393 | 394 | func (f *deploymentInformer) Lister() v1beta1.DeploymentLister { 395 | return v1beta1.NewDeploymentLister(f.Informer().GetIndexer()) 396 | } 397 | ``` 398 | 399 | ## GenericInformer 接口 400 | 401 | GenericInformer 接口封装了 SharedIndexInformer 接口,codegen 为各 K8S 资源类型生成的 XXXInformer(如上面的 DeploymentInformer) 均实现了该接口: 402 | 403 | ``` go 404 | // 来源于 k8s.io/client-go/informers/generic.go 405 | type GenericInformer interface { 406 | Informer() cache.SharedIndexInformer 407 | Lister() cache.GenericLister 408 | } 409 | ``` 410 | 411 | 内置类型 genericInformer 实现了该接口: 412 | 413 | ``` go 414 | type genericInformer struct { 415 | informer cache.SharedIndexInformer 416 | resource schema.GroupResource 417 | } 418 | // Informer returns the SharedIndexInformer. 419 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 420 | return f.informer 421 | } 422 | 423 | // Lister returns the GenericLister. 424 | func (f *genericInformer) Lister() cache.GenericLister { 425 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 426 | } 427 | ``` 428 | 429 | 一般情况下,我们可用通过以下两种方式创建 GenericInformer 接口: 430 | 431 | 1. 直接使用 codegen 为各 K8S 资源类型生成的 XXXInformer; 432 | 2. 或者使用 sharedInformerFactory 的 ForResource() 方法; 433 | 434 | 435 | ``` go 436 | // 来源于 8s.io/client-go/informers/generic.go 437 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 438 | switch resource { 439 | ... 440 | // Group=extensions, Version=v1beta1 441 | case extensionsv1beta1.SchemeGroupVersion.WithResource("deployments"): 442 | return &genericInformer{resource: resource.GroupResource(), informer: f.Extensions().V1beta1().Deployments().Informer()}, nil 443 | ... 444 | } 445 | } 446 | ``` 447 | -------------------------------------------------------------------------------- /client-go/7.rateLimiter-workqueue.md: -------------------------------------------------------------------------------- 1 | # workqueue 2 | 3 | 4 | 5 | - [workqueue](#workqueue) 6 | - [Interface 接口](#interface-接口) 7 | - [实现 Interface 接口的 Type 类型](#实现-interface-接口的-type-类型) 8 | - [Add() 方法](#add-方法) 9 | - [Get() 方法](#get-方法) 10 | - [Done() 方法](#done-方法) 11 | - [向 workqueue 添加 item 的 4 种情况](#向-workqueue-添加-item-的-4-种情况) 12 | - [RateLimiter 接口](#ratelimiter-接口) 13 | - [实现 RateLimter 接口的 BucketRateLimiter 类型](#实现-ratelimter-接口的-bucketratelimiter-类型) 14 | - [实现 RateLimter 接口的 ItemExponentialFailureRateLimiter 类型](#实现-ratelimter-接口的-itemexponentialfailureratelimiter-类型) 15 | - [实现 RateLimter 接口的 ItemFastSlowRateLimiter 类型](#实现-ratelimter-接口的-itemfastslowratelimiter-类型) 16 | - [实现 RateLimter 接口的 MaxOfRateLimiter 类型](#实现-ratelimter-接口的-maxofratelimiter-类型) 17 | - [DelayingInterface 接口](#delayinginterface-接口) 18 | - [RateLimitingInterface 接口](#ratelimitinginterface-接口) 19 | - [workqueue 的使用场景](#workqueue-的使用场景) 20 | 21 | 22 | 23 | workqueue 提供了实现 RateLimitingInterface 接口的 Queue,它支持如下特性: 24 | 25 | 1. 顺序性(Fair):item 处理的顺序和添加它的顺序一致; 26 | 2. 单实例(Stingy):一个 item 只会被一个 worker 处理,如果在被处理前添加了多次,则只会被处理一次; 27 | 3. 在处理过程中,可以将 item 重新入队列,后续可以重新处理; 28 | 4. 关闭通知; 29 | 30 | 在实现 K8S Controller 时,写入 workqueue 的 item 是**资源对象的标识 Key**,而非资源对象本身(因为对象本身一般都在变化)。 31 | 32 | Interface 接口定义了 queue 接口。 33 | 34 | RateLimiter 接口的 When(item interface{}) time.Duration 方法,返回对某个对象进行操作的延迟。 35 | 36 | DelayingInterface 接口包含 Interface 接口,但是提供了额外的 AddAfter(item interface{}, duration time.Duration) 方法,其中的 duration 一般来源于 RateLimiter.When() 方法。 37 | 38 | RateLimitingInterface 接口包含 DelayingInterface 接口,但是提供了额外的、操作 RateLimiter 的 AddRateLimited(item interface{})、Forget(item interface{})、NumRequeues(item interface{}) int 方法。 39 | 40 | 函数 NewRateLimitingQueue() 返回实现 RateLimitingInterface 接口的对象,它的参数是实现 RateLimter 接口的对象。 41 | 42 | ## Interface 接口 43 | 44 | Interface 接口定义了 queue 接口。 45 | 46 | ``` go 47 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 48 | type Interface interface { 49 | Add(item interface{}) 50 | Len() int 51 | Get() (item interface{}, shutdown bool) 52 | Done(item interface{}) 53 | ShutDown() 54 | ShuttingDown() bool 55 | } 56 | ``` 57 | 58 | 函数 New()、NewNamed()、newQueue() 均返回实现 Interface 接口的 Type 类型对象: 59 | 60 | ``` go 61 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 62 | func New() *Type { 63 | return NewNamed("") 64 | } 65 | 66 | func NewNamed(name string) *Type { 67 | rc := clock.RealClock{} 68 | return newQueue( 69 | rc, 70 | globalMetricsFactory.newQueueMetrics(name, rc), 71 | defaultUnfinishedWorkUpdatePeriod, 72 | ) 73 | } 74 | 75 | func newQueue(c clock.Clock, metrics queueMetrics, updatePeriod time.Duration) *Type { 76 | t := &Type{ 77 | clock: c, 78 | dirty: set{}, 79 | processing: set{}, 80 | cond: sync.NewCond(&sync.Mutex{}), 81 | metrics: metrics, 82 | unfinishedWorkUpdatePeriod: updatePeriod, 83 | } 84 | go t.updateUnfinishedWorkLoop() 85 | return t 86 | } 87 | ``` 88 | 89 | ### 实现 Interface 接口的 Type 类型 90 | 91 | Type 类型实现了 Interface 接口,即实现了 work queue。 92 | 93 | ``` go 94 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 95 | type Type struct { 96 | // queue 是一个将要处理的 item 有序列表,item 顺序和加入的顺序一致; 97 | // queue 中的原生也位于 dirty 中,但是不能位于 processing 中。 98 | queue []t 99 | 100 | // dirty 包含所有正在处理的、即将被处理的 item 101 | dirty set 102 | 103 | // processing 包含正在处理的 item,该 item 可能同时位于 dirty 中 104 | // 当处理 item 结束时,如果发现 dirty 中有该 item,如果有的话,将它加到 queue 中 105 | processing set 106 | 107 | cond *sync.Cond 108 | 109 | shuttingDown bool 110 | 111 | metrics queueMetrics 112 | 113 | unfinishedWorkUpdatePeriod time.Duration 114 | clock clock.Clock 115 | } 116 | ``` 117 | 118 | q.dirty 缓存了**所有** item,q.processing 缓存了**正在处理**的 item,而 q.queue 缓存了**即将被**处理的 item。 119 | 120 | #### Add() 方法 121 | 122 | Add() 方法用于向 workqueue 中添加 item,但是在添加前,它会检查 queue 中是否有该 item,或 item 是否在处理过程中: 123 | 124 | 1. 如果 q.dirty 中有该 item,则直接返回; 125 | 2. 否则将 item 添加到 q.dirty 中; 126 | 3. 如果 item 位于 q.processing 中,则表示正在处理,则直接返回; 127 | 4. 否则,将 item 添加到 q.queue 中; 128 | 129 | ``` go 130 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 131 | func (q *Type) Add(item interface{}) { 132 | q.cond.L.Lock() 133 | defer q.cond.L.Unlock() 134 | if q.shuttingDown { 135 | return 136 | } 137 | if q.dirty.has(item) { 138 | return 139 | } 140 | 141 | q.metrics.add(item) 142 | 143 | q.dirty.insert(item) 144 | if q.processing.has(item) { 145 | return 146 | } 147 | 148 | q.queue = append(q.queue, item) 149 | q.cond.Signal() 150 | } 151 | ``` 152 | #### Get() 方法 153 | 154 | Get() 方法从 queue 中返回一个 item: 155 | 1. 从 q.queue 中返回第一个 item; 156 | 2. 将该 item 插入到 q.processing 中,表示正在处理; 157 | 3. 将该 item 从 q.dirty 删除; 158 | 159 | Get() 方法将 item 从 q.queue 中弹出,插入 q.processing,从 q.dirty 中删除。 160 | 161 | ``` go 162 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 163 | func (q *Type) Get() (item interface{}, shutdown bool) { 164 | q.cond.L.Lock() 165 | defer q.cond.L.Unlock() 166 | for len(q.queue) == 0 && !q.shuttingDown { 167 | q.cond.Wait() 168 | } 169 | if len(q.queue) == 0 { 170 | // We must be shutting down. 171 | return nil, true 172 | } 173 | 174 | item, q.queue = q.queue[0], q.queue[1:] 175 | 176 | q.metrics.get(item) 177 | 178 | q.processing.insert(item) 179 | q.dirty.delete(item) 180 | 181 | return item, false 182 | } 183 | ``` 184 | 185 | #### Done() 方法 186 | 187 | Done() 方法表示对 item 的处理结束,将它从 q.processing 移除。同时查看 q.dirty 中是否有该 item,如果有的话,将它加入 q.queue,供后续 Get() 返回处理: 188 | 189 | 注意:处理结束后必须调用 Done() 方法,否则对象一致处于 q.processing 中,后续 Add() 该 item 可能会失效(只会成功一次,添加到 q.dirty 中,后续都失效)。 190 | 191 | ``` go 192 | // 来源于:k8s.io/client-go/util/workqueue/queue.go 193 | func (q *Type) Done(item interface{}) { 194 | q.cond.L.Lock() 195 | defer q.cond.L.Unlock() 196 | 197 | q.metrics.done(item) 198 | 199 | q.processing.delete(item) 200 | if q.dirty.has(item) { 201 | q.queue = append(q.queue, item) 202 | q.cond.Signal() 203 | } 204 | } 205 | ``` 206 | 207 | #### 向 workqueue 添加 item 的 4 种情况 208 | 209 | 第一次添加 item: 210 | 1. 添加到 q.dirty 中; 211 | 2. q.processing 中没有该 item,故添加 q.queue 中; 212 | 213 | 如果该 item 没有被 Get() 返回,也没有正在被处理,再次添加 item: 214 | 1. q.dirty 中有该对象,直接返回;(无效添加) 215 | 216 | 如果该 item 被 Get() 返回,但是在处理过程中,再次添加 item: 217 | 1. q.dirty 中没有该 item,故添加到 q.dirty 中; 218 | 2. q.processing 中有该 item,故直接返回(不添加到 q.queue 中); 219 | 220 | 如果该 item 被 Get() 返回,且处理结束(调用 Done 方法),再次添加 item: 221 | 1. 和第一次添加该 time 的效果一致。 222 | 223 | 对于第三种情况,即处理过程中再次添加该 item,在处理结束调用 Done() 方法的过程中,会将 itme 从 q.dirty 中移除,添加到 q.queue 中,这样后续可以 Get() 处理。 224 | 225 | Type 类型的 workqueue 一般不会单独使用,而是内嵌到 deleyQueue 中,和 rateLimiter 联合使用。 226 | 227 | ## RateLimiter 接口 228 | 229 | RateLimiter 接口的 When() 方法,返回了下一次添加 item 需要**等待的时间**。 230 | 231 | ``` go 232 | // 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go 233 | type RateLimiter interface { 234 | // When gets an item and gets to decide how long that item should wait 235 | When(item interface{}) time.Duration 236 | // Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing 237 | // or for success, we'll stop tracking it 238 | Forget(item interface{}) 239 | // NumRequeues returns back how many failures the item has had 240 | NumRequeues(item interface{}) int 241 | } 242 | ``` 243 | 244 | 函数 `DefaultControllerRateLimiter()` 返回实现该接口的 `NewMaxOfRateLimiter` 类型对象。 245 | 246 | ``` go 247 | // 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go 248 | func DefaultControllerRateLimiter() RateLimiter { 249 | return NewMaxOfRateLimiter( 250 | NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second), 251 | // 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item) 252 | &BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}, 253 | ) 254 | } 255 | ``` 256 | 257 | 在开发自定义 Controller 时,经常使用该函数返回的 RateLimiter 来创建 workqueue: 258 | 259 | 1. BucketRateLimiter 实现 overall rate limiting,NewItemExponentialFailureRateLimiter 实现 per-item 的 rate limiting; 260 | 2. overall 是 token bucket,per-item 是 exponential; 261 | 262 | ### 实现 RateLimter 接口的 BucketRateLimiter 类型 263 | 264 | BucketRateLimiter 是基于标准的 token bucket 实现的限速器 : 265 | 266 | ``` go 267 | // 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go 268 | type BucketRateLimiter struct { 269 | *rate.Limiter 270 | } 271 | 272 | var _ RateLimiter = &BucketRateLimiter{} 273 | 274 | func (r *BucketRateLimiter) When(item interface{}) time.Duration { 275 | return r.Limiter.Reserve().Delay() 276 | } 277 | 278 | func (r *BucketRateLimiter) NumRequeues(item interface{}) int { 279 | return 0 280 | } 281 | 282 | func (r *BucketRateLimiter) Forget(item interface{}) { 283 | } 284 | ``` 285 | 286 | 创建 BucketRateLimiter(参考上面的 `DefaultControllerRateLimiter()` 函数): 287 | 288 | ``` go 289 | BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)} 290 | ``` 291 | 292 | 这会创建一个 10qps、100 bucket size 的 token bucket 限速器。该类型的限速器,如果 bucket size 有富余,则允许 burst 大于 10qps。 293 | 294 | ### 实现 RateLimter 接口的 ItemExponentialFailureRateLimiter 类型 295 | 296 | ItemExponentialFailureRateLimiter 实现了**根据失败次数**对延迟进行指数型增长的限速器,延迟时间为 baseDelay*2^,调用者可以执行 maxDelay 来限制最大延迟时间: 297 | 298 | ``` go 299 | // 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go 300 | type ItemExponentialFailureRateLimiter struct { 301 | failuresLock sync.Mutex 302 | failures map[interface{}]int 303 | 304 | baseDelay time.Duration 305 | maxDelay time.Duration 306 | } 307 | ``` 308 | 309 | 函数 NewItemExponentialFailureRateLimiter() 和 DefaultItemBasedRateLimiter() 方法返回创建该类型的实例: 310 | 311 | ``` go 312 | // 来源于:k8s.io/client-go/util/workqueue/default_rate_limiters.go 313 | func NewItemExponentialFailureRateLimiter(baseDelay time.Duration, maxDelay time.Duration) RateLimiter { 314 | return &ItemExponentialFailureRateLimiter{ 315 | failures: map[interface{}]int{}, 316 | baseDelay: baseDelay, 317 | maxDelay: maxDelay, 318 | } 319 | } 320 | 321 | func DefaultItemBasedRateLimiter() RateLimiter { 322 | return NewItemExponentialFailureRateLimiter(time.Millisecond, 1000*time.Second) 323 | } 324 | ``` 325 | 326 | 创建 ItemExponentialFailureRateLimiter(参考上面的 `DefaultControllerRateLimiter()` 函数): 327 | 328 | ``` go 329 | NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second), 330 | } 331 | ``` 332 | 333 | 其它方法: 334 | 335 | + 每次调用 When() 方法时,对应 item 失败次数加 1。 336 | + NumRequests() 方法返回对象失败的次数; 337 | + Forget()方法将对象从限速器移除,相当于**将失败次数置 0**,这样下次延迟从 baseDelay 开始。 338 | 339 | ### 实现 RateLimter 接口的 ItemFastSlowRateLimiter 类型 340 | ### 实现 RateLimter 接口的 MaxOfRateLimiter 类型 341 | 342 | ## DelayingInterface 接口 343 | ## RateLimitingInterface 接口 344 | ## workqueue 的使用场景 345 | 346 | ``` go 347 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 348 | type Controller struct { 349 | ... 350 | workqueue workqueue.RateLimitingInterface 351 | ... 352 | } 353 | 354 | func NewController( 355 | kubeclientset kubernetes.Interface, 356 | sampleclientset clientset.Interface, 357 | deploymentInformer appsinformers.DeploymentInformer, 358 | fooInformer informers.FooInformer) *Controller { 359 | ... 360 | controller := &Controller{ 361 | ... 362 | workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"), 363 | ... 364 | } 365 | ... 366 | } 367 | 368 | func (c *Controller) enqueueFoo(obj interface{}) { 369 | var key string 370 | var err error 371 | if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { 372 | utilruntime.HandleError(err) 373 | return 374 | } 375 | c.workqueue.AddRateLimited(key) 376 | } 377 | 378 | func (c *Controller) processNextWorkItem() bool { 379 | ... 380 | err := func(obj interface{}) error { 381 | defer c.workqueue.Done(obj) 382 | var key string 383 | var ok bool 384 | if key, ok = obj.(string); !ok { 385 | c.workqueue.Forget(obj) 386 | utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) 387 | return nil 388 | } 389 | if err := c.syncHandler(key); err != nil { 390 | c.workqueue.AddRateLimited(key) 391 | return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error()) 392 | } 393 | c.workqueue.Forget(obj) 394 | klog.Infof("Successfully synced '%s'", key) 395 | return nil 396 | }(obj) 397 | ... 398 | } 399 | ``` -------------------------------------------------------------------------------- /client-go/8.customize-controller.md: -------------------------------------------------------------------------------- 1 | # 自定义 Controller 2 | 3 | 4 | 5 | - [自定义 Controller](#自定义-controller) 6 | - [使用 Informer 的自定义 Controller](#使用-informer-的自定义-controller) 7 | - [参考](#参考) 8 | 9 | 10 | 11 | 一般自定义 controller 的模式是: 12 | 13 | 1. 创建一个 SharedIndexInformer 和 workerqueue。 14 | 2. 在 SharedIndexInformer 中注册 OnAdd/OnUpdate/OnDelete 的处理函数是 enqueue,它向队列压入对象的 Key。 15 | 3. 运行几个 worker,分别从 workerqueue 中 Pop 对象 Key,从 Key 提取 namespace 和 objname; 16 | 4. 再调用从 SharedIndexInformer 获取的对象 Lister,根据 namespace 和 objname 获取对象; 17 | 18 | workqueue 可以通过通过内部的机制可以保证,同时只能有一个 worker 处理某个对象,处理结束后(调用队列的 Done() 或 Forget() 方法),才能向队列中 Add/Get 对象。 19 | 20 | 由于 SharedIndexInformer 内部会有循环队列缓存 Controller Pop 出的对象事件,所以如果快速的 Add 再 Delete 对象, worker 用 Add 事件的对象 key 查找缓存,可能出现**找不到**的情况。 21 | 22 | 对于 CRD 对象的删除事件,一般是**不需**要定义处理函数(如 enqueue 或自定义函数),因为删除意味着该对象已经不在 K8S 中了。但是如果需要清理该 CRD 创建的 K8S 资源对象,则可能需要为 CRD 对象的 OnDelete 事件绑定处理函数,或者使用 finalized 机制。 23 | 24 | 如何快速查找 CRD 创建的 K8S 资源对象呢?简单的办法是给它创建的对象打上**包含 CRD 对象名的标签**。另外 CRD 对象名最好是随机的,否则删除和创建同一个 CRD 对象的资源时可能出现**竞争**关系。 25 | 26 | 对于 CRD 创建的类型对象,获取到后,需要看它是否是 CRD 创建的,如果不是则需要忽略。另外,对这些对象的删除事件,需要捕获,一般需要再次创建该对象。 27 | 28 | ## 使用 Informer 的自定义 Controller 29 | 30 | 在实现自定义 Controller 时, SharedIndexInformer 一般和 workqueue 联合使用: 31 | 32 | 1. OnAdd/OnUpdate 的时候,都将对象的 Key 保存到 workqueue 中,后续弹出后的处理逻辑是一致的: 33 | 1. 从 Key 提取 namespace 和 name; 34 | 2. 从 Lister 获取 namespace 和 name 对应的资源对象(它的最新版本); 35 | 3. 通过 Lister 获取受控资源对象(一般是通过主资源对象的 name、spec 等来查找它的受控资源对象); 36 | 4. 如果受控资源对象不存在,则创建它; 37 | 5. 比较主资源对象和受控资源对象,如果不一致,则更新受控资源对象; 38 | 6. 用受控资源对象 Status 更新主资源对象; 39 | 2. 对于 OnDelete: 40 | + 如果是主资源对象,则需要特殊的 Delete Handler 直接处理; 41 | + 对于受控资源类型对象,则查找它的主资源对象,然后将主资源对象的 Key 存入 workqueue。对受控资源类型对象自身,一般除不需要处理; 42 | 3. 实现真正并发:可以为 workqueue 定义多个 workers; 43 | 4. 通过 workqueue 可以做到限流和重试; 44 | 45 | 对于 OnDelete() 处理函数,Informer 传入的 Obj 可能是 `cache.DeletedFinalStateUnknown` 类型而非特定的资源类型如 aolV1alpha1.AolDeployment,处理函数应该能判断和处理这种情况: 46 | 47 | ``` go 48 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 49 | func (c *Controller) handleObject(obj interface{}) { 50 | var object metav1.Object 51 | var ok bool 52 | if object, ok = obj.(metav1.Object); !ok { 53 | tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 54 | if !ok { 55 | utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) 56 | return 57 | } 58 | object, ok = tombstone.Obj.(metav1.Object) 59 | if !ok { 60 | utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) 61 | return 62 | } 63 | klog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName()) 64 | } 65 | klog.V(4).Infof("Processing object: %s", object.GetName()) 66 | ... 67 | } 68 | ``` 69 | 70 | 同时需要等 informer 的 HasSynced() 返回为 true 时才开始启动 worker: 71 | 72 | ``` go 73 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 74 | func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { 75 | defer utilruntime.HandleCrash() 76 | defer c.workqueue.ShutDown() 77 | 78 | // Start the informer factories to begin populating the informer caches 79 | klog.Info("Starting Foo controller") 80 | 81 | // Wait for the caches to be synced before starting workers 82 | klog.Info("Waiting for informer caches to sync") 83 | if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok { 84 | return fmt.Errorf("failed to wait for caches to sync") 85 | } 86 | ... 87 | } 88 | ``` 89 | 90 | 1. HasSynced() 返回 false 时,表明部分对象还处于 DeltaFIFO 中,没有被 Informer 内部的 controller 更新到 clientState 缓存;这时如果执行 worker,则 worker 使用对象名称(Key)查找 clientState 缓存(通过 Informer 的 Lister)时会找不到对象; 91 | 2. HasSynced() 返回 true 时表明 Reflecter List 的第一批对象都从 DeltaFIFO 弹出,并由 controller **更新到 clientState 缓存中,这样 worker 才能通过通过对象名称(Key)从 Lister Get 到对象**。 92 | 93 | 考虑一种情况,有多个 worker 处理 workqueue,如果用户先 Update 再 Delete 资源对象: 94 | 95 | 1. worker1 弹出 Update 事件; 96 | 2. worker1 还在执行过程中,worker2 弹出和执行 Delete 事件; 97 | 3. worker1 执行 Update 失败,将 key 放回 workqueue; 98 | 4. 下一次 work3 弹出 key,查找 Lister,发现对象不存在。**这时除了打印日志外,啥都做不了**; 99 | 100 | 因为多个 worker 并发处理 workqueue,有可能出现两个 worker 同时处理一个对象的不同版本情况么? 101 | 答案是否定的。 102 | 103 | ## 参考 104 | 105 | + [cache.NewIndexerInformer 的用法示例](https://github.com/kubernetes/client-go/blob/master/examples/workqueue/main.go#L164-L195) 106 | + [sample-controller](https://github.com/kubernetes/sample-controller) -------------------------------------------------------------------------------- /client-go/9.scheme-clientset-codegen.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Clientset 2 | 3 | 4 | 5 | - [Kubernetes Clientset](#kubernetes-clientset) 6 | - [资源类型 Scheme](#资源类型-scheme) 7 | - [types.go 文件](#typesgo-文件) 8 | - [zz_generated.deepcopy.go 文件](#zz_generateddeepcopygo-文件) 9 | - [register.go 文件](#registergo-文件) 10 | - [注册所有内置资源类型到 Scheme 对象](#注册所有内置资源类型到-scheme-对象) 11 | - [创建和使用 Kubernetes Clientset](#创建和使用-kubernetes-clientset) 12 | - [创建支持所有资源类型的全局 Clientset](#创建支持所有资源类型的全局-clientset) 13 | - [各资源类型的 Clientset](#各资源类型的-clientset) 14 | - [各资源类型的 RestFul 方法](#各资源类型的-restful-方法) 15 | - [使用资源类型的 Clientset 创建 Informer 和 Lister](#使用资源类型的-clientset-创建-informer-和-lister) 16 | - [使用 codegen 工具生成资源类型的 Clientset、Informer 和 Lister](#使用-codegen-工具生成资源类型的-clientsetinformer-和-lister) 17 | - [参考](#参考) 18 | 19 | 20 | 21 | ## 资源类型 Scheme 22 | 23 | Clienset 和 apiserver 通信时,需要根据资源对象的类型生成 Resource URL、对 Wire-data 进行**编解码(序列化/反序列化)**。 24 | 25 | 资源类型的 Group、Version、Kind、go struct 定义、编解码(序列化/反序列化) 等内容构成了它的 `Scheme`。 26 | 27 | K8S 内置资源类型的 Scheme 位于 `k8s.io/api//` 目录下,以 `Deployment` 为例: 28 | 29 | ``` bash 30 | $ pwd 31 | /Users/zhangjun/go/src/gitlab.4pd.io/pht3/aol/vendor/k8s.io/api/extensions/v1beta1 32 | 33 | $ ls -l 34 | total 1048 35 | -rw-r--r-- 1 zhangjun staff 642 Jan 22 15:16 doc.go 36 | -rw-r--r-- 1 zhangjun staff 308747 Jan 22 15:16 generated.pb.go 37 | -rw-r--r-- 1 zhangjun staff 49734 Jan 22 15:16 generated.proto 38 | -rw-r--r-- 1 zhangjun staff 2042 Jan 22 15:16 register.go 39 | -rw-r--r-- 1 zhangjun staff 69022 Jan 23 22:30 types.go 40 | -rw-r--r-- 1 zhangjun staff 47996 Jan 22 15:16 types_swagger_doc_generated.go 41 | -rw-r--r-- 1 zhangjun staff 41555 Jan 22 15:16 zz_generated.deepcopy.go 42 | ``` 43 | 44 | 可以暂时忽略无关的文件,我们主要分析 `types.go`、`zz_generated.deepcopy.go` 和 `register.go` 三个文件。 45 | 46 | 1. types.go:定义本 `/` 下所有的资源类型和 codegen 注释; 47 | 2. zz_generated.deepcopy.go:`deepcopy-gen` 工具创建的、定义各资源类型 `DeepCopyObject()` 方法的文件; 48 | 3. register.go:定义了 `AddToScheme()` 函数,用于将本 `/` 下的各资源类型注册到 Clientset 使用的 Scheme 对象中(k8s.io/client-go/kubernetes/scheme/); 49 | 4. 对于自定义资源类型,需要在 doc.go 中添加注释 **// +groupName=**,否则后续自动生成的 fake clientset 的 Group 不对; 50 | 51 | ### types.go 文件 52 | 53 | 该文件包含资源类型的 go struct 定义及 codegen 命令行工具使用的注释: 54 | 55 | ``` go 56 | // 来源于:k8s.io/api/extensions/v1beta1/types.go 57 | 58 | // +genclient 59 | // +genclient:method=GetScale,verb=get,subresource=scale,result=Scale 60 | // +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale 61 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 62 | 63 | // DEPRECATED - This group version of Deployment is deprecated by apps/v1beta2/Deployment. See the release notes for 64 | // more information. 65 | // Deployment enables declarative updates for Pods and ReplicaSets. 66 | type Deployment struct { 67 | metav1.TypeMeta `json:",inline"` 68 | // Standard object metadata. 69 | // +optional 70 | metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 71 | 72 | // Specification of the desired behavior of the Deployment. 73 | // +optional 74 | Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 75 | 76 | // Most recently observed status of the Deployment. 77 | // +optional 78 | Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` 79 | } 80 | 81 | // DeploymentSpec is the specification of the desired behavior of the Deployment. 82 | type DeploymentSpec struct { 83 | Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` 84 | Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"` 85 | Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` 86 | Strategy DeploymentStrategy `json:"strategy,omitempty" patchStrategy:"retainKeys" protobuf:"bytes,4,opt,name=strategy"` 87 | MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,5,opt,name=minReadySeconds"` 88 | RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,6,opt,name=revisionHistoryLimit"` 89 | Paused bool `json:"paused,omitempty" protobuf:"varint,7,opt,name=paused"` 90 | RollbackTo *RollbackConfig `json:"rollbackTo,omitempty" protobuf:"bytes,8,opt,name=rollbackTo"` 91 | ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,9,opt,name=progressDeadlineSeconds"` 92 | } 93 | ``` 94 | 95 | ### zz_generated.deepcopy.go 文件 96 | 97 | 所有注册到 Scheme 的资源类型都要实现 `runtime.Object` 接口: 98 | 99 | ``` go 100 | // 来源于:k8s.io/apimachinery/pkg/runtime/interfaces.go 101 | type Object interface { 102 | GetObjectKind() schema.ObjectKind 103 | DeepCopyObject() Object 104 | } 105 | ``` 106 | 107 | K8S 各资源类型的 go struct 定义都嵌入了 `metav1.TypeMeta` 类型,而该类型实现了 `GetObjectKind()` 方法,故各资源类型只需要实现 `DeepCopyObject()` 方法。 108 | 109 | 我们不需要手动为各资源类型定义 `DeepCopyObject()` 方法,而是使用 `deepcopy-gen` 工具命令统一、自动地生成该方法。 110 | 111 | deepcopy-gen 工具读取 `types.go` 文件中的 `+k8s:deepcopy-gen` 注释,如: 112 | 113 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 114 | 115 | 然后将生成的结果保存到 `zz_generated.deepcopy.go` 文件。 116 | 117 | 以 `Deployment` 类型为例: 118 | 119 | ``` go 120 | // 来源于:k8s.io/api/extensions/v1beta1/zz_generated.deepcopy.go 121 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Deployment. 122 | func (in *Deployment) DeepCopy() *Deployment { 123 | if in == nil { 124 | return nil 125 | } 126 | out := new(Deployment) 127 | in.DeepCopyInto(out) 128 | return out 129 | } 130 | 131 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 132 | func (in *Deployment) DeepCopyObject() runtime.Object { 133 | if c := in.DeepCopy(); c != nil { 134 | return c 135 | } 136 | return nil 137 | } 138 | ``` 139 | 140 | ### register.go 文件 141 | 142 | 使用 `deepcopy-gen` 工具命令统一、自动地为各资源类型生成 `DeepCopyObject()` 方法后,各资源类型就满足了 `runtime.Object` 接口,进而可以注册到 Scheme 中。 143 | 144 | 各 API Group/Version 目录下都有一个 `register.go` 文件,该文件对外提供的 `AddToScheme()` 方法用于将本 Group/Version 下的各资源类型注册到**传入**的 Scheme 对象中(k8s.io/client-go/kubernetes/scheme/register.go 中创建该 Scheme 对象),然后 Clientset 就可以使用它进行 Wired-data 和对象之间的转换了。 145 | 146 | ``` go 147 | // 来源于:k8s.io/api/extensions/v1beta1/register.go 148 | // 本 package 的 Group 名称 149 | const GroupName = "extensions" 150 | 151 | // 注册时提供的 Group/Version 信息 152 | // 一个 Group 目录下,有可能有多个 Version 的子目录 153 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"} 154 | 155 | // Resource 实际就是资源类型的完整路径 //,如 extensions/v1beta1/deployments 156 | // Plural 是资源类型的复数形式 157 | func Resource(resource string) schema.GroupResource { 158 | return SchemeGroupVersion.WithResource(resource).GroupResource() 159 | } 160 | 161 | var ( 162 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 163 | localSchemeBuilder = &SchemeBuilder 164 | // 对外暴露的 AddToScheme() 方法用于注册该 Group/Verion 下的所有资源类型 165 | AddToScheme = localSchemeBuilder.AddToScheme 166 | ) 167 | 168 | // 将本 Group/Version 下的所有资源类型注册到传入的 scheme 169 | func addKnownTypes(scheme *runtime.Scheme) error { 170 | scheme.AddKnownTypes(SchemeGroupVersion, 171 | &Deployment{}, 172 | &DeploymentList{}, 173 | ... 174 | ) 175 | // Add the watch version that applies 176 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 177 | return nil 178 | } 179 | ``` 180 | 181 | ## 注册所有内置资源类型到 Scheme 对象 182 | 183 | 需要将 `k8s.io/api//` 目录下的各资源类型注册到**全局 Scheme 对象**,这样 Clienset 才能识别和使用它们。 184 | 185 | client-go 的 `scheme package`(k8s.io/client-go/kubernetes/scheme/)定义了这个全局 `Scheme` 对象,并将各 `k8s.io/api//` 目录下的资源类型注册到它上面。 186 | 187 | Scheme 对象还被用于创建另外两个外部对象: 188 | 189 | 1. 对资源类型对象进行编解码(序列化/反序列化)的工厂对象 `Codecs`,后续使用它配置 `rest.Config.NegotiatedSerializer`; 190 | 2. 参数编解码对象 `ParameterCodec`,后续调用 RestFul 的方法时使用,如 `VersionedParams(&options, scheme.ParameterCodec)`。 191 | 192 | ``` go 193 | // 来源于 k8s.io/client-go/kubernetes/scheme/register.go 194 | // 新建一个 Scheme,后续所有 K8S 类型均添加到该 Scheme; 195 | var Scheme = runtime.NewScheme() 196 | // 为 Scheme 中的所有类型创建一个编解码工厂; 197 | var Codecs = serializer.NewCodecFactory(Scheme) 198 | // 为 Scheme 中的所有类型创建一个参数编解码工厂 199 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 200 | 201 | // 将各 `k8s.io/api//` 目录下的资源类型的 AddToScheme() 方法注册到 SchemeBuilder 中 202 | var localSchemeBuilder = runtime.SchemeBuilder{ 203 | ... 204 | extensionsv1beta1.AddToScheme, 205 | ... 206 | } 207 | var AddToScheme = localSchemeBuilder.AddToScheme 208 | 209 | func init() { 210 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 211 | // 调用 SchemeBuilder 中各资源对象的 AddToScheme() 方法,将它们注册到到 Scheme 对象。 212 | utilruntime.Must(AddToScheme(Scheme)) 213 | } 214 | ``` 215 | 216 | ## 创建和使用 Kubernetes Clientset 217 | 218 | 经过前面的铺垫分析后,我们开始分析 Kubernetes Clientset 的创建过程。 219 | 220 | 先从使用者的角度看看如何创建和使用 Kubernetes Clientset: 221 | 222 | ``` go 223 | var err error 224 | var config *rest.Config 225 | // 使用 ServiceAccount 创建集群配置 226 | if config, err = rest.InClusterConfig(); err != nil { 227 | // 使用 kubeConfig 指向的配置文件创建集群配置 228 | if config, err = clientcmd.BuildConfigFromFlags("", *kubeConfig); err != nil { 229 | panic(err.Error()) 230 | } 231 | } 232 | 233 | // 创建 k8s clientset 234 | clientset, err = kubernetes.NewForConfig(config) 235 | if err != nil { 236 | panic(err.Error()) 237 | } 238 | 239 | // 使用 clienset 创建一个 Deploy 240 | deploy, err := c.kubeclientset.ExtensionsV1beta1().Deployments(aolDeploy.ObjectMeta.Namespace).Create(myDeploy) 241 | ``` 242 | 243 | 1. 使用 Kubeconfig 文件或 ServiceAccount 创建 Kubernetes 的 RestFul 配置参数; 244 | 2. 使用 Kubernetes 的 RestFul 配置参数,创建 Clientset; 245 | 3. 调用 Clientset 的方法对资源对象进行 CRUD; 246 | 247 | ## 创建支持所有资源类型的全局 Clientset 248 | 249 | `k8s.io/client-go/kubernetes/clientset.go` 文件中创建的 Clientset 实际上是对各资源类型的 Clientset 做了一次封装: 250 | 251 | 1. 调用各资源类型的 NewForConfig() 函数创建对应的 Clientset; 252 | 2. 后续可以使用 Clientset.(),如 Clientset.ExtensionsV1beta1() 来调用具体资源类型的 Clientset; 253 | 254 | ``` go 255 | // 来源于 k8s.io/client-go/kubernetes/clientset.go 256 | // 传入的 rest.Config 包含 apiserver 服务器和认证信息 257 | func NewForConfig(c *rest.Config) (*Clientset, error) { 258 | configShallowCopy := *c 259 | ... 260 | // 透传 rest.Config,调用具体分组和版本的资源类型的 ClientSet 构造函数 261 | cs.extensionsV1beta1, err = extensionsv1beta1.NewForConfig(&configShallowCopy) 262 | if err != nil { 263 | return nil, err 264 | } 265 | ... 266 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 267 | if err != nil { 268 | return nil, err 269 | } 270 | return &cs, nil 271 | } 272 | 273 | // ExtensionsV1beta1 retrieves the ExtensionsV1beta1Client 274 | func (c *Clientset) ExtensionsV1beta1() extensionsv1beta1.ExtensionsV1beta1Interface { 275 | return c.extensionsV1beta1 276 | } 277 | ``` 278 | 279 | ## 各资源类型的 Clientset 280 | 281 | 各资源类型的 Clientset 定义位于 `k8s.io/client-go/kubernetes/typed///_client.go` 文件中,如 282 | `k8s.io/client-go/kubernetes/typed/extensions/v1beta1/extensions_client.go`。 283 | 284 | 比较关键的是 `setConfigDefaults()` 函数,它负责为 Clientset 配置参数: 285 | 286 | 1. 资源对象的 GroupVersion; 287 | 2. 资源对象的 root path; 288 | 3. 对 wired data 进行编解码(序列化/反序列化)的 `NegotiatedSerializer`,使用的 `scheme.Codecs` 为前面介绍过的 `scheme package`; 289 | 290 | RESTClient 根据配置的 root path 和 GroupVersion,构造 Resource 地址(格式为 `/apis///`)。 291 | 292 | ``` go 293 | // 来源于 k8s.io/client-go/kubernetes/typed/extensions/v1beta1/extensions_client.go 294 | // 传入的 rest.Config 包含 apiserver 服务器和认证信息 295 | func NewForConfig(c *rest.Config) (*ExtensionsV1beta1Client, error) { 296 | config := *c 297 | // 为 rest.Config 设置资源对象相关的参数 298 | if err := setConfigDefaults(&config); err != nil { 299 | return nil, err 300 | } 301 | // 创建 ExtensionsV1beta1 的 RestClient 302 | client, err := rest.RESTClientFor(&config) 303 | if err != nil { 304 | return nil, err 305 | } 306 | return &ExtensionsV1beta1Client{client}, nil 307 | } 308 | 309 | func setConfigDefaults(config *rest.Config) error { 310 | // 资源对象的 GroupVersion 311 | gv := v1beta1.SchemeGroupVersion 312 | config.GroupVersion = &gv 313 | // 资源对象的 root path 314 | config.APIPath = "/apis" 315 | // 使用注册的资源类型 Schema 对请求和响应进行编解码 316 | // scheme 为前面分析过的 k8s.io/client-go/kubernetes/scheme package 317 | config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} 318 | 319 | if config.UserAgent == "" { 320 | config.UserAgent = rest.DefaultKubernetesUserAgent() 321 | } 322 | 323 | return nil 324 | } 325 | 326 | func (c *ExtensionsV1beta1Client) Deployments(namespace string) DeploymentInterface { 327 | return newDeployments(c, namespace) 328 | } 329 | ``` 330 | 331 | ## 各资源类型的 RestFul 方法 332 | 333 | 使用各资源类型的 Clientset 创建特定资源类型的 RestFul 方法,参数的编解码工厂 `scheme.ParameterCodec` 来源于前面介绍的 `scheme package` 中。 334 | 335 | ``` go 336 | // 来源于 k8s.io/client-go/kubernetes/typed/extensions/v1beta1/deployment.go 337 | // newDeployments returns a Deployments 338 | func newDeployments(c *ExtensionsV1beta1Client, namespace string) *deployments { 339 | return &deployments{ 340 | client: c.RESTClient(), 341 | ns: namespace, 342 | } 343 | } 344 | 345 | // Get takes name of the deployment, and returns the corresponding deployment object, and an error if there is any. 346 | func (c *deployments) Get(name string, options v1.GetOptions) (result *v1beta1.Deployment, err error) { 347 | result = &v1beta1.Deployment{} 348 | // 发起实际的 RestFul 请求; 349 | err = c.client.Get(). 350 | Namespace(c.ns). 351 | Resource("deployments"). 352 | Name(name). 353 | VersionedParams(&options, scheme.ParameterCodec). 354 | Do(). 355 | Into(result) 356 | return 357 | } 358 | ``` 359 | 360 | ## 使用资源类型的 Clientset 创建 Informer 和 Lister 361 | ## 使用 codegen 工具生成资源类型的 Clientset、Informer 和 Lister 362 | 363 | 目录结构: 364 | 365 | ``` bash 366 | zhangjun:core zhangjun$ pwd 367 | /Users/zhangjun/go/src/gitlab.4pd.io/pht3/aol/pkg/apis/core/v1alpha1 368 | 369 | zhangjun:v1alpha1 zhangjun$ ls -l 370 | total 976 371 | -rw-r--r-- 1 zhangjun staff 643 Jan 28 21:14 doc.go 372 | -rw-r--r-- 1 zhangjun staff 1200 Jan 28 21:37 objectreference.go 373 | -rw-r--r-- 1 zhangjun staff 2741 Jan 28 21:39 register.go 374 | -rw-r--r-- 1 zhangjun staff 273720 Jan 28 21:40 types.go 375 | -rw-r--r-- 1 zhangjun staff 154116 Jan 28 21:41 zz_generated.deepcopy.go 376 | ``` 377 | 378 | 在 `doc.go` 文件里添加 `// +k8s:deepcopy-gen=package`,否则后续不会为 `types.go` 中的类型生成 `DeepCopy()` 方法: 379 | 380 | ``` go 381 | // 来源于 doc.go 382 | 383 | // +k8s:deepcopy-gen=package 384 | // +k8s:openapi-gen=true 385 | 386 | package v1alpha1 387 | ``` 388 | 389 | ## 参考 390 | 391 | [Kubernetes Deep Dive: Code Generation for CustomResources](https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/) -------------------------------------------------------------------------------- /client-go/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes client-go 库介绍和源码解析 2 | 3 | client-go 项目地址:https://github.com/kubernetes/client-go 4 | 5 | 本文档源码分析基于 kubernetes-1.13.5-beta.0 版本。 6 | 7 | ## 基本介绍 8 | 9 | Kubernetes 官方从 2016 年 8 月份开始,将 Kubernetes 资源操作相关的核心源码抽取出来,独立出来一个项目 `client-go`,作为官方提供的 Go client。 10 | 11 | Kubernetes 的部分代码也是基于这个 client 实现的,所以对这个 client 的质量、性能等方面还是非常有信心的。 12 | 13 | client-go 是一个调用 kubernetes 集群资源对象 API 的客户端,即通过 client-go 实现对 kubernetes 集群中资源对象(包括 deployment、service、ingress、replicaSet、pod、namespace、node 等)的增删改查等操作。 14 | 15 | ## 源码简介 16 | 17 | 主要目录功能说明: 18 | 19 | + discovery:通过 Kubernetes API 进行服务发现; 20 | + dynamic:对任意 Kubernetes 对象执行通用操作的动态 client; 21 | + informers:Kubernetes 内置对象的 Informer 定义, 22 | + kubernetes: 访问 Kubernetes API 的一系列的 clientset; 23 | + rest:访问 Kubernetes API 的 Rest 库,是 dynamic 和 clientset 的基础; 24 | + transport:启动连接和鉴权 auth; 25 | + tools/cache:controllers 控制器; 26 | 27 | ## Client 类型 28 | 29 | 1. RESTClient:RESTClient 是最基础的,相当于的底层基础结构,可以直接通过 RESTClient 提供的 RESTful 方法如 Get(),Put(),Post(),Delete() 进行交互 30 | + 同时支持 Json 和 protobuf 31 | + 支持所有原生资源和 CRDs 32 | + 但是,一般而言,为了更为优雅的处理,需要进一步封装,通过 clientset 封装 RESTClient,然后再对外提供接口和服务; 33 | 34 | 2. Clientset:Clientset 是调用 Kubernetes 资源对象最常用的 client,可以操作所有的资源对象,它是基于 RESTClient 实现的。 35 | + 访问资源时,需要指定它的 Group、Version、Resource; 36 | + 优雅的姿势是利用一个 controller 对象,再加上 Informer; 37 | 38 | 3. DynamicClient:DynamicClient 是一种动态的 client,它能处理 kubernetes 所有的资源。不同于 Clientset,DynamicClient 返回的对象是一个 `map[string]interface{}`。 39 | + 如果一个 controller 中需要控制所有的 API,可以使用 DynamicClient,目前它在 garbage collector 和 namespace controller 中被使用。 40 | + 只支持 JSON 41 | 42 | ## Informer 43 | 44 | Informer 是 client-go 中较为高级的类型。无论是 Kubernetes 内置的还是自己实现的 Controller,都会用到它。 45 | 46 | Informer 设计为 List/Watch 的方式。Informer 在初始化的时先通过 List 从 Kubernetes 中取出资源的全部对象,并同时缓存,然后后面通过 Watch 的机制去监控资源,这样的话,通过 Informer 及其缓存,我们就可以直接和 Informer 交互而不是每次都和 Kubernetes 交互。 47 | 48 | Informer 另外一块内容在于提供了事件 Handler 机制,并会触发回调,这样上层应用如 Controller 就可以基于回调处理具体业务逻辑。 49 | 50 | 因为Informer 通过 List、Watch 机制可以监控到所有资源的所有事件,因此只要给 Informer 添加 ResourceEventHandler 实例的回调函数实例取实现 OnAdd(obj interface{}) OnUpdate(oldObj, newObj interface{}) 和 OnDelete(obj interface{})这三个方法,就可以处理好资源的创建、更新和删除操作。 51 | 52 | ## 对象资源的操作接口 53 | 54 | 默认的每一种资源对象都有一个 Interface,封装了对象的 CURD 方法和 List/Watch方法。 55 | 56 | 如 Deployment(k8s.io/client-go/kubernetes/typed/apps/v1/deployment.go): 57 | 58 | ``` go 59 | type DeploymentInterface interface { 60 | Create(*v1.Deployment) (*v1.Deployment, error) 61 | Update(*v1.Deployment) (*v1.Deployment, error) 62 | UpdateStatus(*v1.Deployment) (*v1.Deployment, error) 63 | Delete(name string, options *metav1.DeleteOptions) error 64 | DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error 65 | Get(name string, options metav1.GetOptions) (*v1.Deployment, error) 66 | List(opts metav1.ListOptions) (*v1.DeploymentList, error) 67 | Watch(opts metav1.ListOptions) (watch.Interface, error) 68 | Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Deployment, err error) 69 | DeploymentExpansion 70 | } 71 | ``` 72 | 73 | 如 Service(k8s.io/client-go/kubernetes/typed/core/v1/service.go): 74 | 75 | ``` go 76 | // ServiceInterface has methods to work with Service resources. 77 | type ServiceInterface interface { 78 | Create(*v1.Service) (*v1.Service, error) 79 | Update(*v1.Service) (*v1.Service, error) 80 | UpdateStatus(*v1.Service) (*v1.Service, error) 81 | Delete(name string, options *metav1.DeleteOptions) error 82 | Get(name string, options metav1.GetOptions) (*v1.Service, error) 83 | List(opts metav1.ListOptions) (*v1.ServiceList, error) 84 | Watch(opts metav1.ListOptions) (watch.Interface, error) 85 | Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Service, err error) 86 | ServiceExpansion 87 | } 88 | ``` 89 | 90 | 通过这种 Interface 定义,Kubernetes 中所有对象资源的操作方式都是统一的。 91 | 92 | ## client-go 的设计思想 93 | 94 | client-go/tool/cache/ 和自定义 Controller 的控制流([来源](https://itnext.io/how-to-create-a-kubernetes-custom-controller-using-client-go-f36a7a7536cc)): 95 | 96 | ![](image/2019-03-06-18-42-40.png) 97 | 98 | 上图相对较为复杂,有很多细节,我自己结合源码的理解如下: 99 | 100 | ![](image/2019-01-25-23-49-26.png) 101 | 102 | ## client-go 组件 103 | 104 | + Reflector:通过 Kubernetes API 监控 Kubernetes 的资源类型 105 | 采用List、Watch机制 106 | 可以Watch任何资源包括CRD 107 | 添加object对象到FIFO队列,然后Informer会从队列里面取数据 108 | 109 | + Informer:controller机制的基础 110 | 循环处理object对象 111 | 从Reflector取出数据,然后将数据给到Indexer去缓存 112 | 提供对象事件的handler接口 113 | 114 | + Indexer:提供object对象的索引,是线程安全的,缓存对象信息 115 | 116 | ## controller 组件 117 | 118 | + Informer reference: controller需要创建合适的Informer才能通过Informer reference操作资源对象 119 | + Indexer reference: controller创建Indexer reference然后去利用索引做相关处理 120 | + Resource Event Handlers:Informer会回调这些handlers 121 | + Work queue: Resource Event Handlers被回调后将key写到工作队列 122 | 这里的key相当于事件通知,后面根据取出事件后,做后续的处理 123 | + Process Item:从工作队列中取出key后进行后续处理,具体处理可以通过Indexer reference 124 | controller可以直接创建上述两个引用对象去处理,也可以采用工厂模式,官方都有相关示例 125 | 126 | ## 参考 127 | 1. [Kubernetes的client-go库介绍](https://www.jianshu.com/p/d17f70369c35) 128 | 2. [client-go under the hood](https://github.com/kubernetes/sample-controller/blob/master/docs/controller-client-go.md) 129 | 3. [sample-controller](https://github.com/kubernetes/sample-controller) 130 | 4. [kubebuilder 文档](https://book.kubebuilder.io/) 131 | 5. [deployment_controller](https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/deployment/deployment_controller.go) 132 | 6. [Writing Controllers](https://github.com/kubernetes/community/blob/master/contributors/devel/controllers.md) 133 | 7. [controller-runtime 实例](https://github.com/googlecloudrobotics/core/blob/master/src/go/pkg/controller/chartassignment/controller.go) 134 | 8. [etcd-operator](https://github.com/coreos/etcd-operator) -------------------------------------------------------------------------------- /client-go/controller-runtime.md: -------------------------------------------------------------------------------- 1 | 来源:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg 2 | 3 | 使用 client-go 写 controller copy&paste 的太多,而 controller-runtime 项目可以解决该问题。 4 | controller-runtime 是 kubebuilder 项目的一部分,kubebuilder 是为 CRD 创建 Controller 的库。 5 | 6 | Slice: https://kccna18.sched.com/event/GrUR/why-are-we-copying-and-pasting-so-much-solly-ross-philip-wittrock-google 7 | 示例:https://github.com/googlecloudrobotics/core/blob/master/src/go/pkg/controller/chartassignment/controller.go 8 | 9 | import "github.com/kubernetes-sigs/controller-runtime/pkg" 10 | 11 | Package pkg provides libraries for building Controllers. Controllers implement Kubernetes APIs and are foundational to building Operators, Workload APIs, Configuration APIs, Autoscalers, and more. 12 | 13 | ## Client 14 | Client provides a Read + Write client for reading and writing Kubernetes objects. 15 | 16 | ## Cache 17 | Cache provides a Read client for reading objects from a local cache. A cache may register handlers to respond to events that update the cache. 18 | 19 | ## Manager 20 | Manager is required for creating a Controller and provides the Controller shared dependencies such as clients, caches, schemes, etc. Controllers should be Started through the Manager by calling Manager.Start. 21 | 22 | ## Controller 23 | Controller implements a Kubernetes API by responding to events (object Create, Update, Delete) and ensuring that the state specified in the Spec of the object matches the state of the system. This is called a Reconciler. If they do not match, the Controller will create / update / delete objects as needed to make them match. 24 | 25 | Controllers are implemented as worker queues that process reconcile.Requests (requests to Reconciler the state for a specific object). 26 | 27 | Unlike http handlers, Controllers DO NOT handle events directly, but enqueue Requests to eventually Reconciler the object. This means the handling of multiple events may be batched together and the full state of the system must be read for each Reconciler. 28 | 29 | * Controllers require a Reconciler to be provided to perform the work pulled from the work queue. 30 | * Controllers require Watches to be configured to enqueue reconcile.Requests in response to events. 31 | 32 | ## Webhook 33 | Admission Webhooks are a mechanism for extending kubernetes APIs. Webhooks can be configured with target event type (object Create, Update, Delete), the API server will send AdmissionRequests to them when certain events happen. The webhooks may mutate and (or) validate the object embedded in the AdmissionReview requests and send back the response to the API server. 34 | 35 | There are 2 types of admission webhook: mutating and validating admission webhook. Mutating webhook is used to mutate a core API object or a CRD instance before the API server admits it. Validating webhook is used to validate if an object meets certain requirements. 36 | 37 | * Admission Webhooks require Handler(s) to be provided to process the received AdmissionReview requests. 38 | 39 | ## Reconciler 40 | Reconciler is a function provided to a Controller that may be called at anytime with the Name and Namespace of an object. When called, Reconciler will ensure that the state of the system matches what is specified in the object at the time Reconciler is called. 41 | 42 | Example: Reconciler invoked for a ReplicaSet object. The ReplicaSet specifies 5 replicas but only 3 Pods exist in the system. Reconciler creates 2 more Pods and sets their OwnerReference to point at the ReplicaSet with controller=true. 43 | 44 | * Reconciler contains all of the business logic of a Controller. 45 | * Reconciler typically works on a single object type. - e.g. it will only reconcile ReplicaSets. For separate types use separate Controllers. If you wish to trigger reconciles from other objects, you can provide a mapping (e.g. owner references) that maps the object that triggers the reconcile to the object being reconciled. 46 | * Reconciler is provided the Name / Namespace of the object to reconcile. 47 | * Reconciler does not care about the event contents or event type responsible for triggering the Reconciler. - e.g. it doesn't matter whether a ReplicaSet was created or updated, Reconciler will always compare the number of Pods in the system against what is specified in the object at the time it is called. 48 | 49 | ## Source 50 | resource.Source is an argument to Controller.Watch that provides a stream of events. Events typically come from watching Kubernetes APIs (e.g. Pod Create, Update, Delete). 51 | 52 | Example: source.Kind uses the Kubernetes API Watch endpoint for a GroupVersionKind to provide Create, Update, Delete events. 53 | 54 | * Source provides a stream of events (e.g. object Create, Update, Delete) for Kubernetes objects typically through the Watch API. 55 | * Users SHOULD only use the provided Source implementations instead of implementing their own for nearly all cases. 56 | 57 | ## EventHandler 58 | handler.EventHandler is a argument to Controller.Watch that enqueues reconcile.Requests in response to events. 59 | 60 | Example: a Pod Create event from a Source is provided to the eventhandler.EnqueueHandler, which enqueues a reconcile.Request containing the name / Namespace of the Pod. 61 | 62 | * EventHandlers handle events by enqueueing reconcile.Requests for one or more objects. 63 | * EventHandlers MAY map an event for an object to a reconcile.Request for an object of the same type. 64 | * EventHandlers MAY map an event for an object to a reconcile.Request for an object of a different type - e.g. map a Pod event to a reconcile.Request for the owning ReplicaSet. 65 | * EventHandlers MAY map an event for an object to multiple reconcile.Requests for objects of the same or a different type - e.g. map a Node event to objects that respond to cluster resize events. 66 | * Users SHOULD only use the provided EventHandler implementations instead of implementing their own for almost all cases. 67 | 68 | ## Predicate 69 | predicate.Predicate is an optional argument to Controller.Watch that filters events. This allows common filters to be reused and composed. 70 | 71 | * Predicate takes an event and returns a bool (true to enqueue) 72 | * Predicates are optional arguments 73 | * Users SHOULD use the provided Predicate implementations, but MAY implement additional Predicates e.g. generation changed, label selectors changed etc. 74 | 75 | ## PodController Diagram 76 | Source provides event: 77 | * &source.KindSource{&v1.Pod{}} -> (Pod foo/bar Create Event) 78 | 79 | EventHandler enqueues Request: 80 | * &handler.EnqueueRequestForObject{} -> (reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}}) 81 | 82 | Reconciler is called with the Request: 83 | * Reconciler(reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}}) 84 | 85 | ## Usage 86 | The following example shows creating a new Controller program which Reconciles ReplicaSet objects in response to Pod or ReplicaSet events. The Reconciler function simply adds a label to the ReplicaSet. 87 | 88 | See the example/main.go for a usage example. 89 | 90 | Controller Example 91 | 1. Watch ReplicaSet and Pods Sources 92 | 93 | 1.1 ReplicaSet -> handler.EnqueueRequestForObject - enqueue a Request with the ReplicaSet Namespace and Name. 94 | 95 | 1.2 Pod (created by ReplicaSet) -> handler.EnqueueRequestForOwnerHandler - enqueue a Request with the Owning ReplicaSet Namespace and Name. 96 | 97 | 2. Reconciler ReplicaSet in response to an event 98 | 99 | 2.1 ReplicaSet object created -> Read ReplicaSet, try to read Pods -> if is missing create Pods. 100 | 101 | 2.2 Reconciler triggered by creation of Pods -> Read ReplicaSet and Pods, do nothing. 102 | 103 | 2.3 Reconciler triggered by deletion of Pods from some other actor -> Read ReplicaSet and Pods, create replacement Pods. 104 | 105 | ## Watching and EventHandling 106 | Controllers may Watch multiple Kinds of objects (e.g. Pods, ReplicaSets and Deployments), but they Reconciler only a single Type. When one Type of object must be updated in response to changes in another Type of object, an EnqueueRequestFromMapFunc may be used to map events from one type to another. e.g. Respond to a cluster resize event (add / delete Node) by re-reconciling all instances of some API. 107 | 108 | A Deployment Controller might use an EnqueueRequestForObject and EnqueueRequestForOwner to: 109 | 110 | * Watch for Deployment Events - enqueue the Namespace and Name of the Deployment. 111 | * Watch for ReplicaSet Events - enqueue the Namespace and Name of the Deployment that created the ReplicaSet (e.g the Owner) 112 | 113 | Note: reconcile.Requests are deduplicated when they are enqueued. Many Pod Events for the same ReplicaSet may trigger only 1 reconcile invocation as each Event results in the Handler trying to enqueue the same reconcile.Request for the ReplicaSet. 114 | 115 | ## Controller Writing Tips 116 | Reconciler Runtime Complexity: 117 | 118 | * It is better to write Controllers to perform an O(1) Reconciler N times (e.g. on N different objects) instead of performing an O(N) Reconciler 1 time (e.g. on a single object which manages N other objects). 119 | 120 | * Example: If you need to update all Services in response to a Node being added - Reconciler Services but Watch Nodes (transformed to Service object name / Namespaces) instead of Reconciling Nodes and updating Services 121 | 122 | Event Multiplexing: 123 | 124 | * reconcile.Requests for the same Name / Namespace are batched and deduplicated when they are enqueued. This allows Controllers to gracefully handle a high volume of events for a single object. Multiplexing multiple event Sources to a single object Type will batch requests across events for different object types. 125 | 126 | * Example: Pod events for a ReplicaSet are transformed to a ReplicaSet Name / Namespace, so the ReplicaSet will be Reconciled only 1 time for multiple events from multiple Pods. -------------------------------------------------------------------------------- /client-go/how-to-create-a-kubernetes-custom-controller-using-client-go.md: -------------------------------------------------------------------------------- 1 | 来源:https://itnext.io/how-to-create-a-kubernetes-custom-controller-using-client-go-f36a7a7536cc 2 | 3 | With Kubernetes custom controller, you can further develop your own custom business logic by watching events from Kubernetes API objects such as namespace, deployment or pod, or your own CRD (custom resource definitions) resource. 4 | 5 | The next part of the article will provide a deep dive on the client-go module, following by a custom controller example. 6 | 7 | ## client-go module 8 | 9 | client-go is being used by Kubernetes as the offical API client library, providing access to Kubernetes` restful API interface served by the Kubernetes API server. Tools like kubectl or prometheus-operator use it intensively. 10 | 11 | The library contains several important packages and utilities which can be used for accessing the API resources or facilitate a custom controller. 12 | 13 | + `kubernetes` package provides clientset and Kubernetes resource specific clientset methods for accessing standard Kubernetes APIs. Note you should not use it for accessing CRD resources. This package is auto generated. 14 | 15 | + `discovery` package provides ways to discover server-supported API groups, versions and resources. 16 | 17 | + `dynamic` package provides a dynamic client which can perform restful operations on arbitrary Kubernetes API resources [1]. Note that it’s discouraged to use it for access CRD resource due to it’s not type-safe [2]. 18 | 19 | + `transport` package setups the secure TCP authorisation and connection. The default will be using HTTP2 protocol if not explicitly disabled. The underlying HTTP2 facility is provided by k8s.io/apimachinery/util/net. Due to some of the operations requires transfer of binary streams between the client and a container, such as attach, exec, portforward and logging, the transport package also establishes streaming channels. It employed SPDY and WebSocket protocols before HTTP2 became available. 20 | 21 | + `plugin` package provides authorisation plugins for cloud providers such as Openstack, GCP and Azure. 22 | 23 | + `scale` package provides a polymorphic scale client capable of fetching and updating Scale for any resource which implements the “scale” subresource, as long as that subresource operates on aversion of scale convertable to autoscaling.Scale. Note prior to Kubernetes v1.10, it doesn’t support scaling CRD[2]. 24 | 25 | + `util` package provides convenient utilities such as work queue, buffer. 26 | 27 | + `tool/cache` package has many useful facilities such as SharedInformer, reflector, stores and indexers. It provides a client-side query and caching mechanism, reducing the number of request to the server and keep tracking of the events. I will be detailing workflow provided by utilities in this package because it does the plumbing when writing a custom controllers. 28 | 29 | ![client-go/tool/cache/ and custom controller flow](image/2019-02-18-11-09-56.png) 30 | 31 | As shown in the above graph, there are two main parts of actions. One happens in SharedIndexInformer, the other one is in custom controller. 32 | 33 | 1. Reflector performs object (such as namespace, pod etc) ListAndWatch queries to the Kubernetes API server. Three event types Added, Updated and Deleted are being recorded along with the related objects. They are then passed onto DeltaFIFO. Note only events occurred within the past five minutes can be retrieved by default. 34 | 35 | A given Kubernetes server will only preserve a historical list of changes for a limited time. Clusters using etcd3 preserve changes in the last 5 minutes by default. When the requested watch operations fail because the historical version of that resource is not available, clients must handle the case by recognizing the status code 410 Gone, clearing their local cache, performing a list operation, and starting the watch from the resourceVersion returned by that new list operation. Most client libraries offer some form of standard tool for this logic. (In Go this is called a Reflector and is located in the k8s.io/client-go/cache package.)[3] 36 | 37 | 2. DeltaFIFO ingests the events and the objects correspondingly to the watch events, then translates them into Delta object. Those Delta objects get appended into a queue waiting for processing. For Deleted, it will check if it’s already existed in the thread safe store so it can avoid queuing up a delete action while something doesn’t exist. 38 | 39 | 3. Cache controller (Not to confused with custom controller) calls Pop() method to dequeue the DeltaFIFO queue. The Delta objects get passed onto SharedIndexInformer’s HandleDelta() method for further processing. 40 | 41 | 4. According to the Delta object’s action (event) type, the objects firstly get saved into thread safe store via indexer’s method in HandleDeltas method. It then sends those objects via sharedProcessor’s distribute() method in SharedIndexInformer to event handlers which have been registered by the custom controller through SharedInformer’s methods such as AddEventHandlerWithResyncPeriod(). 42 | 43 | 5. The registered event handlers convert the object into key strings in a format of “namespace/name” or just “name” if no namespace through MetaNamespaceKeyFunc() for add and update events, DeletionHandlingMetaNamespaceKeyFunc() for delete event. The keys then get added into custom controller’s workqueue. The type of workqueues could be found in util/workqueue. 44 | 45 | 6. The custom controller pops keys from the workqueue for processing by calling custom handlers. The custom handlers will invoke indexer’s GetByKey() to retrieve the object from thread safe store. The custom handlers are where your business logic lives. 46 | 47 | ## Example of a simple custom controller using workqueue 48 | 49 | Below is an example of custom controller that watches pods in default namespace. The workqueue type is RateLimitQueue. The controller spawns out one worker in below example. You can change the number of workers when calling controller.Run(). 50 | 51 | Note that below example is using an IndexInformer which can only have one sets of the handlers to subscribe to the events. The recommendation is to use SharedIndexInformer instead. The difference is ShareIndexInformer provides methods like AddEventHandlerWithResyncPeriod() to allow you adding multiple set of handlers, consequently one event request can be distributed to different handlers at the same time, reducing the number of API requests. 52 | 53 | If you want to hook up with your CRD, you could follow this instruction to generate clientset, informer etc for your CRD resources. You then can use the generated SharedInformer in your custom controller. 54 | 55 | ``` go 56 | // source: k8s.io/client-go/examples/workqueue/main.go 57 | package main 58 | 59 | import ( 60 | "flag" 61 | "fmt" 62 | "time" 63 | 64 | "k8s.io/klog" 65 | 66 | "k8s.io/api/core/v1" 67 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 68 | "k8s.io/apimachinery/pkg/fields" 69 | "k8s.io/apimachinery/pkg/util/runtime" 70 | "k8s.io/apimachinery/pkg/util/wait" 71 | "k8s.io/client-go/kubernetes" 72 | "k8s.io/client-go/tools/cache" 73 | "k8s.io/client-go/tools/clientcmd" 74 | "k8s.io/client-go/util/workqueue" 75 | ) 76 | 77 | type Controller struct { 78 | indexer cache.Indexer 79 | queue workqueue.RateLimitingInterface 80 | informer cache.Controller 81 | } 82 | 83 | func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller { 84 | return &Controller{ 85 | informer: informer, 86 | indexer: indexer, 87 | queue: queue, 88 | } 89 | } 90 | 91 | func (c *Controller) processNextItem() bool { 92 | // Wait until there is a new item in the working queue 93 | key, quit := c.queue.Get() 94 | if quit { 95 | return false 96 | } 97 | // Tell the queue that we are done with processing this key. This unblocks the key for other workers 98 | // This allows safe parallel processing because two pods with the same key are never processed in 99 | // parallel. 100 | defer c.queue.Done(key) 101 | 102 | // Invoke the method containing the business logic 103 | err := c.syncToStdout(key.(string)) 104 | // Handle the error if something went wrong during the execution of the business logic 105 | c.handleErr(err, key) 106 | return true 107 | } 108 | 109 | // syncToStdout is the business logic of the controller. In this controller it simply prints 110 | // information about the pod to stdout. In case an error happened, it has to simply return the error. 111 | // The retry logic should not be part of the business logic. 112 | func (c *Controller) syncToStdout(key string) error { 113 | obj, exists, err := c.indexer.GetByKey(key) 114 | if err != nil { 115 | klog.Errorf("Fetching object with key %s from store failed with %v", key, err) 116 | return err 117 | } 118 | 119 | if !exists { 120 | // Below we will warm up our cache with a Pod, so that we will see a delete for one pod 121 | fmt.Printf("Pod %s does not exist anymore\n", key) 122 | } else { 123 | // Note that you also have to check the uid if you have a local controlled resource, which 124 | // is dependent on the actual instance, to detect that a Pod was recreated with the same name 125 | fmt.Printf("Sync/Add/Update for Pod %s\n", obj.(*v1.Pod).GetName()) 126 | } 127 | return nil 128 | } 129 | 130 | // handleErr checks if an error happened and makes sure we will retry later. 131 | func (c *Controller) handleErr(err error, key interface{}) { 132 | if err == nil { 133 | // Forget about the #AddRateLimited history of the key on every successful synchronization. 134 | // This ensures that future processing of updates for this key is not delayed because of 135 | // an outdated error history. 136 | c.queue.Forget(key) 137 | return 138 | } 139 | 140 | // This controller retries 5 times if something goes wrong. After that, it stops trying. 141 | if c.queue.NumRequeues(key) < 5 { 142 | klog.Infof("Error syncing pod %v: %v", key, err) 143 | 144 | // Re-enqueue the key rate limited. Based on the rate limiter on the 145 | // queue and the re-enqueue history, the key will be processed later again. 146 | c.queue.AddRateLimited(key) 147 | return 148 | } 149 | 150 | c.queue.Forget(key) 151 | // Report to an external entity that, even after several retries, we could not successfully process this key 152 | runtime.HandleError(err) 153 | klog.Infof("Dropping pod %q out of the queue: %v", key, err) 154 | } 155 | 156 | func (c *Controller) Run(threadiness int, stopCh chan struct{}) { 157 | defer runtime.HandleCrash() 158 | 159 | // Let the workers stop when we are done 160 | defer c.queue.ShutDown() 161 | klog.Info("Starting Pod controller") 162 | 163 | go c.informer.Run(stopCh) 164 | 165 | // Wait for all involved caches to be synced, before processing items from the queue is started 166 | if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { 167 | runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) 168 | return 169 | } 170 | 171 | for i := 0; i < threadiness; i++ { 172 | go wait.Until(c.runWorker, time.Second, stopCh) 173 | } 174 | 175 | <-stopCh 176 | klog.Info("Stopping Pod controller") 177 | } 178 | 179 | func (c *Controller) runWorker() { 180 | for c.processNextItem() { 181 | } 182 | } 183 | 184 | func main() { 185 | var kubeconfig string 186 | var master string 187 | 188 | flag.StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file") 189 | flag.StringVar(&master, "master", "", "master url") 190 | flag.Parse() 191 | 192 | // creates the connection 193 | config, err := clientcmd.BuildConfigFromFlags(master, kubeconfig) 194 | if err != nil { 195 | klog.Fatal(err) 196 | } 197 | 198 | // creates the clientset 199 | clientset, err := kubernetes.NewForConfig(config) 200 | if err != nil { 201 | klog.Fatal(err) 202 | } 203 | 204 | // create the pod watcher 205 | podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceDefault, fields.Everything()) 206 | 207 | // create the workqueue 208 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) 209 | 210 | // Bind the workqueue to a cache with the help of an informer. This way we make sure that 211 | // whenever the cache is updated, the pod key is added to the workqueue. 212 | // Note that when we finally process the item from the workqueue, we might see a newer version 213 | // of the Pod than the version which was responsible for triggering the update. 214 | indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{ 215 | AddFunc: func(obj interface{}) { 216 | key, err := cache.MetaNamespaceKeyFunc(obj) 217 | if err == nil { 218 | queue.Add(key) 219 | } 220 | }, 221 | UpdateFunc: func(old interface{}, new interface{}) { 222 | key, err := cache.MetaNamespaceKeyFunc(new) 223 | if err == nil { 224 | queue.Add(key) 225 | } 226 | }, 227 | DeleteFunc: func(obj interface{}) { 228 | // IndexerInformer uses a delta queue, therefore for deletes we have to use this 229 | // key function. 230 | key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) 231 | if err == nil { 232 | queue.Add(key) 233 | } 234 | }, 235 | }, cache.Indexers{}) 236 | 237 | controller := NewController(queue, indexer, informer) 238 | 239 | // We can now warm up the cache for initial synchronization. 240 | // Let's suppose that we knew about a pod "mypod" on our last run, therefore add it to the cache. 241 | // If this pod is not there anymore, the controller will be notified about the removal after the 242 | // cache has synchronized. 243 | indexer.Add(&v1.Pod{ 244 | ObjectMeta: meta_v1.ObjectMeta{ 245 | Name: "mypod", 246 | Namespace: v1.NamespaceDefault, 247 | }, 248 | }) 249 | 250 | // Now let's start the controller 251 | stop := make(chan struct{}) 252 | defer close(stop) 253 | go controller.Run(1, stop) 254 | 255 | // Wait forever 256 | select {} 257 | } 258 | ``` 259 | 260 | ## Reference 261 | 262 | [1] client-go v10.0 (github) 263 | 264 | [2] Introducing client-go version 6 265 | 266 | [3] Kubernetes API concept 267 | 268 | [4] Writting Kubernetes Custom Controller 269 | 270 | [5] A deep dive into Kubernetes controller 271 | 272 | Disclaimer: The views expressed here are solely those of the author in his private capacity and do not in any way represent the views of any organisation. -------------------------------------------------------------------------------- /client-go/image/2019-01-25-23-48-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsnull/kubernetes-dev-docs/884263f216bd6e454f640153ba61812ecbb821f3/client-go/image/2019-01-25-23-48-04.png -------------------------------------------------------------------------------- /client-go/image/2019-01-25-23-49-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsnull/kubernetes-dev-docs/884263f216bd6e454f640153ba61812ecbb821f3/client-go/image/2019-01-25-23-49-26.png -------------------------------------------------------------------------------- /client-go/image/2019-02-18-11-09-56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsnull/kubernetes-dev-docs/884263f216bd6e454f640153ba61812ecbb821f3/client-go/image/2019-02-18-11-09-56.png -------------------------------------------------------------------------------- /client-go/image/2019-03-06-18-42-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opsnull/kubernetes-dev-docs/884263f216bd6e454f640153ba61812ecbb821f3/client-go/image/2019-03-06-18-42-40.png -------------------------------------------------------------------------------- /client-go/kubernetes-deep-dive-code-generation-customresources.md: -------------------------------------------------------------------------------- 1 | // 来源:https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/ 2 | 3 | The more Kubernetes turns into a platform for distributed applications, the more projects will use the provided extension points to build software on a higher level in the stack. CustomResourceDefinitions (CRDs) as introduced in Kubernetes 1.7 as alpha and promoted to beta in 1.8 are a natural building block for many use-cases, especially those which implement the controller (or sometimes called operator) pattern in some way. Moreover, CustomResourceDefinitions are so easy to create and to use. 4 | 5 | With Kubernetes 1.8, their use in golang-based projects also becomes much more natural: With user-provided CustomResources we can utilize the same code-generation tools that are used inside of Kubernetes itself or in OpenShift. This post shows how the code-generators work and how you apply them in your own project with minimal lines of code, giving you generated deepcopy functions, typed clients, listers and informers, all with one shell script call and a couple of code annotations. A complete project suitable as a blueprint is available in the openshift-evangelists/crd-code-generation repo. 6 | 7 | Code Generation – Why? 8 | Those who have used ThirdPartyResources or CustomResourceDefinition natively in golang might be surprised that suddenly in Kubernetes 1.8, client-go code-generation is required. More specifically, client-go requires that runtime.Object types (CustomResources in golang have to implement the runtime.Object interface) must have DeepCopy methods. Here code-generation comes into play via the deepcopy-gen generator, which can be found in the k8s.io/code-generator repository. 9 | 10 | Next to deepcopy-gen there a handful of code-generators that most users of CustomResources want to use: 11 | 12 | deepcopy-gen—creates a method func (t* T) DeepCopy() *T for each type T 13 | client-gen—creates typed clientsets for CustomResource APIGroups 14 | informer-gen—creates informers for CustomResources which offer an event based interface to react on changes of CustomResources on the server 15 | lister-gen—creates listers for CustomResources which offer a read-only caching layer for GET and LIST requests. 16 | The last two are the basis for building controllers (or operators as some people call them). In a follow-up blog post we will look at controllers in more in detail. These four code-generator make up a powerful basis to build full-featured, production-ready controllers, using the same mechanisms and packages that the Kubernetes upstream controllers are using. 17 | 18 | There are some more generators in k8s.io/code-generator for other contexts, for example, if you build your own aggregated API server, you will work with internal types in addition to versioned types. Conversion-gen will create conversions functions between these internal and external types. Defaulter-gen will take care of defaulting certain fields. 19 | 20 | Calling the Code-Generators in Your Project 21 | All the Kubernetes code-generators are implemented on-top of k8s.io/gengo. They share a number of common command line flags. Basically, all the generators get a list of input packages (--input-dirs) which they go through type by type, and output the generated code for. The generated code: 22 | 23 | Either goes to the same directory as the input files like for deepcopy-gen (with --output-file-base “zz_generated.deepcopy” to define the file name). 24 | Or they generate into one or multiple output packages (with --output-package) like client-, informer- and lister-gen do (usually generating into pkg/client). 25 | The upper description might sound like long fiddling with command line arguments is necessary to get started, but this is luckily not true: k8s.io/code-generator ships a shell script generator-group.sh that does the heavy lifting of calling the generators with all their special small requirements for the use-case of CustomResources. All you have to do in your project boils down to a one-liner, usually inside of hack/update-codegen.sh: 26 | 27 | $ vendor/k8s.io/code-generator/generate-groups.sh all \ 28 | github.com/openshift-evangelist/crd-code-generation/pkg/client \ github.com/openshift-evangelist/crd-code-generation/pkg/apis \ 29 | example.com:v1 30 | It runs against a project which is set up like the following package tree: 31 | 32 | 33 | All the APIs are expected below pkg/apis and the clientsets, informers, and listers are created inside pkg/client. In other words, pkg/client is completely generated as is the zz_generated.deepcopy.go file next to the types.go file which contains our CustomResource golang types. Both are not supposed to be modified manually, but created by running: 34 | 35 | $ hack/update-codegen.sh 36 | Usually, next to it there is a hack/verify-codegen.sh script as well, which terminates with a non-zero return code if any of the generated files is not up-to-date. This is very helpful to put into a CI script: if a developer modified the files by accident or if the files are just outdated, CI will notice and complain. 37 | 38 | Controlling the Generated Code – Tags 39 | While some behaviour of the code-generators is controlled via command line flags as described above (especially the packages to process), a lot more properties are controlled via tags in your golang files. 40 | 41 | There are two kind of tags: 42 | 43 | Global tags above package in doc.go 44 | Local tags above a type that is processed 45 | Tags in general have the shape // +tag-name or // +tag-name=value, that is, they are written into comments. Depending on the tags, the position of the comment might be important. There are a number of tags which must be in a comment directly above a type (or the package line for a global tag), others must be separated from the type (pr the package line) with at least one empty line in-between. We are working on making this more consistent and less error-prone in the 1.9 release cycle (pull request #53579 and issue #53893). Just be prepared that an empty line might matter. Better follow an example and copy the basic shape. 46 | 47 | Global Tags 48 | Global tags are written into the doc.go file of a package. A typical pkg/apis///doc.go looks like this: 49 | 50 | // +k8s:deepcopy-gen=package,register 51 | 52 | 53 | // Package v1 is the v1 version of the API. 54 | // +groupName=example.com 55 | package v1 56 | It tells deepcopy-gen to create deepcopy methods by default for every type in that package. If you have types where deepcopy is not necessary or not desired, you can opt-out for such a type with a local tag // +k8s:deepcopy-gen=false. If you do not enable package-wide deepcopy, you have to opt-in to deepcopy for each desired type via // +k8s:deepcopy-gen=true. 57 | 58 | Note: The register keyword in the value of the upper example will enable the registration of deepcopy methods into the scheme. This will completely go away in Kubernetes 1.9 because the scheme won’t be responsible anymore to do deepcopies of runtime.Objects. Instead just call yourobject.DeepCopy() or yourobject.DeepCopyObject(). You can and you should do that already today in 1.8-based projects as it is faster and less error-prone. Moreover, you will be prepared for 1.9 which will require this pattern. 59 | 60 | Finally, the // +groupName=example.com defines the fully qualified API group name. If you get that wrong, client-gen will produce wrong code. Be warned that this tag must be in the comment block just above package (see Issue #53893). 61 | 62 | Local Tags 63 | Local tags are written either directly above an API type or in the second comment block above it. Here is an example types.go for the golang types of our API server deep dive series about CustomResources: 64 | 65 | // +genclient 66 | // +genclient:noStatus 67 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 68 | 69 | // Database describes a database. 70 | type Database struct { 71 | metav1.TypeMeta `json:",inline"` 72 | metav1.ObjectMeta `json:"metadata,omitempty"` 73 | 74 | Spec DatabaseSpec `json:"spec"` 75 | } 76 | 77 | // DatabaseSpec is the spec for a Foo resource 78 | type DatabaseSpec struct { 79 | User string `json:"user"` 80 | Password string `json:"password"` 81 | Encoding string `json:"encoding,omitempty"` 82 | } 83 | 84 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 85 | 86 | // DatabaseList is a list of Database resources 87 | type DatabaseList struct { 88 | metav1.TypeMeta `json:",inline"` 89 | metav1.ListMeta `json:"metadata"` 90 | 91 | Items []Database `json:"items"` 92 | } 93 | Note that we have enabled deepcopy for all types by default, that is, with possible opt-out. These types, though, are all API types and need the deepcopy. Therefore, we don’t have to switch deepcopy on or off in this example types.go, but only on package-wide in doc.go. 94 | 95 | RUNTIME.OBJECT AND DEEPCOPYOBJECT 96 | There is a special deepcopy tag which needs more explanation: 97 | 98 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 99 | If you have tried to use CustomResources with client-go based on Kubernetes 1.8—some people might have had the pleasure already because they accidentally vendored a k8s.op/apimachinery of the master branch—you have hit the compiler error that the CustomResource type does not implement runtime.Object because DeepCopyObject() runtime.Object is not defined on your type. The reason is that in 1.8 the runtime.Object interface was extended with this method signature and hence every runtime.Object has to implement DeepCopyObject. The implementation of DeepCopyObject() runtime.Object is trivial: 100 | 101 | func (in *T) DeepCopyObject() runtime.Object { 102 | if c := in.DeepCopy(); c != nil { 103 | return c 104 | } else { 105 | return nil 106 | } 107 | } 108 | But luckily you don’t have to implement this for every one of your types, but just put the following local tag above your top-level API types: 109 | 110 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 111 | In our example above both Database and DatabaseList are top-level types because they are used as runtime.Objects. As a rule of thumb, top-level types are those which have metav1.TypeMeta embedded. Also, those are the types which clients are create for using client-gen. 112 | 113 | Note, that the // +k8s:deepcopy-gen:interfaces tag can and should also be used in cases where you define API types that have fields of some interface type, for example, field SomeInterface. Then // +k8s:deepcopy-gen:interfaces=example.com/pkg/apis/example.SomeInterface will lead to the generation of a DeepCopySomeInterface() SomeInterface method. This allows it to deepcopy those fields in a type-correct way. 114 | 115 | CLIENT-GEN TAGS 116 | Finally, there are a number of tags to control client-gen, two of them we see in our example: 117 | 118 | // +genclient 119 | // +genclient:noStatus 120 | The first tag tells client-gen to create a client for this type (this is always opt-in). Note that you don’t have to and indeed MUST not put it above the List type of the API objects. 121 | 122 | The second tag tells client-gen that this type is not using spec-status separation via the /status subresource. The resulting client will not have the UpdateStatus method (client-gen would generate that blindly otherwise as soon as it finds a Status field in your struct). A /status subresource is only possible in 1.8 for natively (in golang) implemented resources. But this might change soon as subresources are discussed for CustomResources in PR 913. 123 | 124 | For cluster-wide resources, you have to use the tag: 125 | 126 | // +genclient:nonNamespaced 127 | For special purpose clients, you might also want to control in detail which HTTP methods are offered by the client. This can be done using a couple of tags, for example: 128 | 129 | // +genclient:noVerbs 130 | // +genclient:onlyVerbs=create,delete 131 | // +genclient:skipVerbs=get,list,create,update,patch,delete,deleteCollection,watch 132 | // +genclient:method=Create,verb=create,result=k8s.io/apimachinery/pkg/apis/meta/v1.Status 133 | The first three should be pretty self-explanatory, but the last one needs some explanation. The type where this tag is written above will be create-only and will not return the API type itself, but a metav1.Status. For CustomResources this does not make much sense, but for user-provided API servers written in golang those resources can exist and they do in practice, for example, in the OpenShift API. 134 | 135 | A Main Function Using the Types Clients 136 | While most examples based on Kubernetes 1.7 and older used the client-go dynamic client for CustomResources, native Kubernetes API types had a much more convenient typed client for a long time. This changed in 1.8: client-gen as described above creates a native, full-featured, and easy to use typed client also for your custom types. Actually, client-gen does not know whether you are applying it to a CustomResource type or a native one. 137 | Hence, using this client turns out to be exactly equivalent to using a client-go Kubernetes client. Here is a very simple example: 138 | 139 | import ( 140 | ... 141 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 142 | "k8s.io/client-go/tools/clientcmd" 143 | examplecomclientset "github.com/openshift-evangelist/crd-code-generation/pkg/client/clientset/versioned" 144 | ) 145 | 146 | var ( 147 | kuberconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") 148 | master = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") 149 | ) 150 | 151 | func main() { 152 | flag.Parse() 153 | 154 | cfg, err := clientcmd.BuildConfigFromFlags(*master, *kuberconfig) 155 | if err != nil { 156 | glog.Fatalf("Error building kubeconfig: %v", err) 157 | } 158 | 159 | exampleClient, err := examplecomclientset.NewForConfig(cfg) 160 | if err != nil { 161 | glog.Fatalf("Error building example clientset: %v", err) 162 | } 163 | 164 | list, err := exampleClient.ExampleV1().Databases("default").List(metav1.ListOptions{}) 165 | if err != nil { 166 | glog.Fatalf("Error listing all databases: %v", err) 167 | } 168 | 169 | for _, db := range list.Items { 170 | fmt.Printf("database %s with user %q\n", db.Name, db.Spec.User) 171 | } 172 | } 173 | It works with a kubeconfig file, in fact the same that can be used with kubectl and the Kubernetes clients. 174 | 175 | In contrast to old legacy TPR or CustomResource code with the dynamic client, you don’t have to type-cast. Instead, the actual client call looks completely native and it is: 176 | 177 | list, err := exampleClient.ExampleV1().Databases("default").List(metav1.ListOptions{}) 178 | The result is a DatabaseList in this example of all databases in your cluster. If you switch your type to cluster-wide (i.e. without namespaces; don’t forget to tell client-gen with the // +genclient:nonNamespaced tag!), the calls turns into 179 | 180 | list, err := exampleClient.ExampleV1().Databases().List(metav1.ListOptions{}) 181 | Creating a CustomResourceDefinition Programmatically in Golang 182 | As this question comes up quite often, a few words about how to create a CRD programmatically from your golang code. 183 | 184 | Client-gen always creates so-called clientsets. Clientsets bundle one or more API groups into one client. Usually, these API groups come from one repository and a placed inside a base package, for example, your pkg/apis as in the example in this blog post, or from k8s.io/api in the case of Kubernetes. 185 | 186 | CustomResourceDefinitions are provided by the 187 | [kubernetes/apiextensions-apiserver repository](https://github.com/kubernetes/apiextensions-apiserver repository). This API server (that can also be launched stand-alone) is embedded by kube-apiserver, such that CRDs are available on every Kubernetes cluster. But the client to create CRDs is created into the apiextensions-apiserver repository, of course also using client-gen. After having read this blog, it should not surprise you to find the client at kubernetes/apiextensions-apiserver/tree/master/pkg/client, nor should it be unexpected what it looks like to create a client instance and how to create the CRD: 188 | 189 | import ( 190 | ... 191 | apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset” 192 | ) 193 | 194 | apiextensionsClient, err := apiextensionsclientset.NewForConfig(cfg) 195 | ... 196 | createdCRD, err := apiextensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(yourCRD) 197 | Note that after this creation you will have to wait that the Established condition is set on the new CRD. Only then will the kube-apiserver start to serve the resource. If you don’t wait for that condition, every CR operation will return a 404 HTTP status code. 198 | 199 | Further Material 200 | The documentation of the Kubernetes generators has a lot of room for improvement, currently, and any kind of help is very welcome. They are freshly extracted from the Kubernetes project into k8s.io/code-generator for public consumption by CustomResource users. The documentation will certainly improve over time and this blog post also aims to contribute to this. 201 | 202 | For further information about the different generators it is often good to look at examples inside Kubernetes itself (for example, in k8s.io/api), into OpenShift, which has many advanced use-cases, and into the generators themselves: 203 | 204 | Deepcopy-gen has some documentation available inside its main.go file. 205 | Client-gen has some documentation available here. 206 | Informer-gen and lister-gen currently don’t posses further documentation, however generate-groups.sh shows how each is invoked. 207 | All the examples in this blog post are available as a fully functioning repository, which can easily serve as a blueprint for your own experiments: 208 | 209 | https://github.com/openshift-evangelists/crd-code-generation. -------------------------------------------------------------------------------- /client-go/开发笔记.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | 4 | 5 | - [目录](#目录) 6 | - [Get 后 Update K8S 对象时,可能会失败,较好的实践方式是使用 client-go 提供的 retry 工具函数](#get-后-update-k8s-对象时可能会失败较好的实践方式是使用-client-go-提供的-retry-工具函数) 7 | - [开发自定义 Controller 时,为何一般使用 workqueue?](#开发自定义-controller-时为何一般使用-workqueue) 8 | - [开发自定义 Controller 时,对资源类型的删除回调函数,为何一般不用 workqueue 来处理?](#开发自定义-controller-时对资源类型的删除回调函数为何一般不用-workqueue-来处理) 9 | - [创建 Index 和 DeltaFIFO 的 KeyFunc 为何不同?](#创建-index-和-deltafifo-的-keyfunc-为何不同) 10 | - [自定义 Controller 的 OnDelete Handler 为何要判断对象类型是否是 DeletedFinalStateUnknownn 类型?](#自定义-controller-的-ondelete-handler-为何要判断对象类型是否是-deletedfinalstateunknownn-类型) 11 | - [为何要在调用了 InformerFactory()的 Informer() 方法后,才调用它的 Run() 方法?](#为何要在调用了-informerfactory的-informer-方法后才调用它的-run-方法) 12 | - [为何要等所有 Informer 的 WaitSync 都返回后,再执行 workqueue 的 worker goroutine?](#为何要等所有-informer-的-waitsync-都返回后再执行-workqueue-的-worker-goroutine) 13 | - [如果 pkg/apis///register.go 中没有将自定义 CRD 类型添加到 scheme.AddKnownTypes 中,则创建 Core Group 中的资源对象时出错](#如果-pkgapisgroupversinregistergo-中没有将自定义-crd-类型添加到-schemeaddknowntypes-中则创建-core-group-中的资源对象时出错) 14 | - [启用 client-go 中的日志](#启用-client-go-中的日志) 15 | - [添加自定义类型的步骤](#添加自定义类型的步骤) 16 | - [CRD Kind 冲突 Bug](#crd-kind-冲突-bug) 17 | - [自定义资源对象的名称不能和 K8S 已有的重名](#自定义资源对象的名称不能和-k8s-已有的重名) 18 | - [给对象打 Path 的方法](#给对象打-path-的方法) 19 | - [K8S Job 的 .spec.selector 和 .spec.template 是不能更新的](#k8s-job-的-specselector-和-spectemplate-是不能更新的) 20 | 21 | 22 | 23 | # Get 后 Update K8S 对象时,可能会失败,较好的实践方式是使用 client-go 提供的 retry 工具函数 24 | 25 | ``` go 26 | // 来源于:k8s.io/client-go/examples/create-update-delete-deployment/main.go 27 | // You have two options to Update() this Deployment: 28 | // 29 | // 1. Modify the "deployment" variable and call: Update(deployment). 30 | // This works like the "kubectl replace" command and it overwrites/loses changes 31 | // made by other clients between you Create() and Update() the object. 32 | // 2. Modify the "result" returned by Get() and retry Update(result) until 33 | // you no longer get a conflict error. This way, you can preserve changes made 34 | // by other clients between Create() and Update(). This is implemented below 35 | // using the retry utility package included with client-go. (RECOMMENDED) 36 | // 37 | // More Info: 38 | // https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency 39 | 40 | retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { 41 | // Retrieve the latest version of Deployment before attempting update 42 | // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver 43 | result, getErr := deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) 44 | if getErr != nil { 45 | panic(fmt.Errorf("Failed to get latest version of Deployment: %v", getErr)) 46 | } 47 | 48 | result.Spec.Replicas = int32Ptr(1) // reduce replica count 49 | result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version 50 | _, updateErr := deploymentsClient.Update(result) 51 | return updateErr 52 | }) 53 | if retryErr != nil { 54 | panic(fmt.Errorf("Update failed: %v", retryErr)) 55 | } 56 | fmt.Println("Updated deployment...") 57 | ``` 58 | 59 | # 开发自定义 Controller 时,为何一般使用 workqueue? 60 | # 开发自定义 Controller 时,对资源类型的删除回调函数,为何一般不用 workqueue 来处理? 61 | 62 | 因为 Informer 内部使用的 controller 会先将对象从 ClientState Indexer 缓存中删除,再调用用户配置的回调函数。 63 | 所以,如果将对象 Key 入 workqueue 后,worker 使用 Key 从 Lister 会查不到对象。 64 | 65 | # 创建 Index 和 DeltaFIFO 的 KeyFunc 为何不同? 66 | 67 | ``` go 68 | clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) 69 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, clientState) 70 | ``` 71 | 72 | 因为 DeltaFIFO 接收的是从 apiserver List/Watch 的 K8S 资源对象,所以可以用 MetaNamespaceKeyFunc 提取它的 NS 和 Name。 73 | 74 | 但是 NewIndexer() 返回的 clientState 一般是 Reflector 从 DeltaFIFO 弹出的 Deltas,它是一个 Delta 的列表,而 Delta.Object 可能是 K8S 资源对象,也可能是 DeletedFinalStateUnknown。所以 NewIndexer() 使用能区分这两种类型的 DeletionHandlingMetaNamespaceKeyFunc KeyFunc。 75 | 76 | # 自定义 Controller 的 OnDelete Handler 为何要判断对象类型是否是 DeletedFinalStateUnknownn 类型? 77 | 78 | ``` go 79 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 80 | func (c *Controller) handleObject(obj interface{}) { 81 | var object metav1.Object 82 | var ok bool 83 | // 判断是原始对象还是 DeletedFinalStateUnknown 对象 84 | if object, ok = obj.(metav1.Object); !ok { 85 | tombstone, ok := obj.(cache.DeletedFinalStateUnknown) 86 | if !ok { 87 | utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) 88 | return 89 | } 90 | // 从 DeletedFinalStateUnknown 对象中提取实际对象 91 | object, ok = tombstone.Obj.(metav1.Object) 92 | if !ok { 93 | utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) 94 | return 95 | } 96 | klog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName()) 97 | } 98 | klog.V(4).Infof("Processing object: %s", object.GetName()) 99 | ... 100 | } 101 | ``` 102 | 103 | DeltaFIFO Watch apiserver 过程中可能因网络等问题出现丢事件的情况,如果丢失了 Delete 事件,则后续 Reflector 重复执行 `ListAndWatch()` 方法从 apiserver 获取的对象集合 set1 会出现与 knownObjects 对象集合 set2 不一致的情况。 104 | 为了保证两者一致,DeltaFIFO 的 `Replace()` 方法将位于 set1 但不在 set2 中的对象用 `DeletedFinalStateUnknown` 类型对象封装,再保存到 Delta.Object 中。 105 | 而上面 handlerObject() 的参数即为 Delta.Object。 106 | 107 | # 为何要在调用了 InformerFactory()的 Informer() 方法后,才调用它的 Run() 方法? 108 | 109 | # 为何要等所有 Informer 的 WaitSync 都返回后,再执行 workqueue 的 worker goroutine? 110 | 111 | 在开发 K8S Controller 的时候,一个惯例是调用 cache.WaitForCacheSync,等待所有 Informer Cache 都同步后才启动消费 workqueue 的 worker: 112 | 113 | ``` go 114 | // 来源于:https://github.com/kubernetes/sample-controller/blob/master/controller.go 115 | 116 | // 自定义的 Controller 117 | type Controller struct { 118 | ... 119 | deploymentsLister appslisters.DeploymentLister 120 | deploymentsSynced cache.InformerSynced // InformerSynced 函数类型 121 | ... 122 | } 123 | 124 | func NewController( 125 | kubeclientset kubernetes.Interface, 126 | sampleclientset clientset.Interface, 127 | deploymentInformer appsinformers.DeploymentInformer, 128 | fooInformer informers.FooInformer) *Controller { 129 | ... 130 | controller := &Controller{ 131 | kubeclientset: kubeclientset, 132 | sampleclientset: sampleclientset, 133 | deploymentsLister: deploymentInformer.Lister(), 134 | deploymentsSynced: deploymentInformer.Informer().HasSynced, // Infomer 的 HasSynced() 方法 135 | } 136 | ... 137 | } 138 | 139 | // 等待所有类型的 Informer 的 HasSynced() 方法返回为 true 时再启动 workers 140 | func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { 141 | ... 142 | // Wait for the caches to be synced before starting workers 143 | klog.Info("Waiting for informer caches to sync") 144 | if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok { 145 | return fmt.Errorf("failed to wait for caches to sync") 146 | } 147 | 148 | klog.Info("Starting workers") 149 | // Launch two workers to process Foo resources 150 | for i := 0; i < threadiness; i++ { 151 | go wait.Until(c.runWorker, time.Second, stopCh) 152 | } 153 | ... 154 | } 155 | ``` 156 | 157 | 为何要等各 informer 的 HasSynced() 返回为 true 时才开始启动 worker 呢? 158 | 159 | 因为 HasSynced() 返回 true 时表明 Reflecter List 的第一批对象都从 DeltaFIFO 弹出,并由 controller **更新到 clientState 缓存中,这样 worker 才能通过通过对象名称(Key)从 Lister Get 到对象**。否则对象可能还在 DeltaFIFO 中且没有同步到 clientState 缓存中,这样 worker 通过对象名称从 Lister 中 Get 不到对象。 160 | 161 | # 如果 pkg/apis///register.go 中没有将自定义 CRD 类型添加到 scheme.AddKnownTypes 中,则创建 Core Group 中的资源对象时出错 162 | 163 | 创建 AolPod 的语句: 164 | 165 | ``` go 166 | func newAolPod(pod *coreV1.Pod) *aolV1alpha1.AolPod { 167 | labels := pod.ObjectMeta.Labels 168 | labels["controller"] = pod.ObjectMeta.Name 169 | labels["pod.aol.4pd.io"] = pod.ObjectMeta.Name 170 | labels["aol.4pd.io"] = "true" 171 | 172 | return &aolV1alpha1.AolPod{ 173 | ObjectMeta: metav1.ObjectMeta{ 174 | Name: pod.ObjectMeta.Name, 175 | Namespace: pod.ObjectMeta.Namespace, 176 | OwnerReferences: []metav1.OwnerReference{ 177 | *metav1.NewControllerRef(pod, schema.GroupVersionKind{ 178 | Group: aolV1alpha1.SchemeGroupVersion.Group, 179 | Version: aolV1alpha1.SchemeGroupVersion.Version, 180 | Kind: "Pod", 181 | }), 182 | }, 183 | Labels: labels, 184 | }, 185 | Spec: pod.Spec, 186 | Status: pod.Status, 187 | } 188 | } 189 | ``` 190 | 191 | 后续创建: 192 | 193 | ``` go 194 | aolPod, err := c.aolPodLister.AolPods(pod.ObjectMeta.Namespace).Get(podName) 195 | if errors.IsNotFound(err) { 196 | aolPod, err = c.aolclientset.AolV1alpha1().AolPods(pod.ObjectMeta.Namespace).Create(newAolPod(pod)) 197 | } 198 | if err != nil { 199 | fmt.Printf("---> create failed: %#v\n", newAolPod(pod)) 200 | return err 201 | } 202 | ``` 203 | 失败日志: 204 | 205 | ``` txt 206 | ---> create failed: &v1alpha1.AolPod{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ObjectMeta:v1.ObjectMeta{Name:"my-aol-deploy-7996d57dfd-lqmtp", GenerateName:"", Namespace:"default", SelfLink:"", UID:"", ResourceVersion:"", Generation:0, CreationTimestamp:v1.Time{Time:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}, DeletionTimestamp:(*v1.Time)(nil), DeletionGracePeriodSeconds:(*int64)(nil), Labels:map[string]string{"controller":"my-aol-deploy-7996d57dfd-lqmtp", "deploy.aol.4pd.io":"my-aol-deploy", "pod-template-hash":"3552813898", "run":"my-aol-nginx", "pod.aol.4pd.io":"my-aol-deploy-7996d57dfd-lqmtp", "aol.4pd.io":"true"}, Annotations:map[string]string(nil), OwnerReferences:[]v1.OwnerReference{v1.OwnerReference{APIVersion:"aol.4pd.io/v1alpha1", Kind:"Pod", Name:"my-aol-deploy-7996d57dfd-lqmtp", UID:"39242763-350f-11e9-b4fd-0cc47a2a835a", Controller:(*bool)(0xc000c1be49), BlockOwnerDeletion:(*bool)(0xc000c1be48)}}, Initializers:(*v1.Initializers)(nil), Finalizers:[]string(nil), ClusterName:"", ManagedFields:[]v1.ManagedFieldsEntry(nil)}, Spec:v1.PodSpec{Volumes:[]v1.Volume{v1.Volume{Name:"aol-deploy-configmap", VolumeSource:v1.VolumeSource{HostPath:(*v1.HostPathVolumeSource)(nil), EmptyDir:(*v1.EmptyDirVolumeSource)(nil), GCEPersistentDisk:(*v1.GCEPersistentDiskVolumeSource)(nil), AWSElasticBlockStore:(*v1.AWSElasticBlockStoreVolumeSource)(nil), GitRepo:(*v1.GitRepoVolumeSource)(nil), Secret:(*v1.SecretVolumeSource)(nil), NFS:(*v1.NFSVolumeSource)(nil), ISCSI:(*v1.ISCSIVolumeSource)(nil), Glusterfs:(*v1.GlusterfsVolumeSource)(nil), PersistentVolumeClaim:(*v1.PersistentVolumeClaimVolumeSource)(nil), RBD:(*v1.RBDVolumeSource)(nil), FlexVolume:(*v1.FlexVolumeSource)(nil), Cinder:(*v1.CinderVolumeSource)(nil), CephFS:(*v1.CephFSVolumeSource)(nil), Flocker:(*v1.FlockerVolumeSource)(nil), DownwardAPI:(*v1.DownwardAPIVolumeSource)(nil), FC:(*v1.FCVolumeSource)(nil), AzureFile:(*v1.AzureFileVolumeSource)(nil), ConfigMap:(*v1.ConfigMapVolumeSource)(0xc0006168c0), VsphereVolume:(*v1.VsphereVirtualDiskVolumeSource)(nil), Quobyte:(*v1.QuobyteVolumeSource)(nil), AzureDisk:(*v1.AzureDiskVolumeSource)(nil), PhotonPersistentDisk:(*v1.PhotonPersistentDiskVolumeSource)(nil), Projected:(*v1.ProjectedVolumeSource)(nil), PortworxVolume:(*v1.PortworxVolumeSource)(nil), ScaleIO:(*v1.ScaleIOVolumeSource)(nil), StorageOS:(*v1.StorageOSVolumeSource)(nil)}}, v1.Volume{Name:"aol-deploy-secret", VolumeSource:v1.VolumeSource{HostPath:(*v1.HostPathVolumeSource)(nil), EmptyDir:(*v1.EmptyDirVolumeSource)(nil), GCEPersistentDisk:(*v1.GCEPersistentDiskVolumeSource)(nil), AWSElasticBlockStore:(*v1.AWSElasticBlockStoreVolumeSource)(nil), GitRepo:(*v1.GitRepoVolumeSource)(nil), Secret:(*v1.SecretVolumeSource)(0xc000616980), NFS:(*v1.NFSVolumeSource)(nil), ISCSI:(*v1.ISCSIVolumeSource)(nil), Glusterfs:(*v1.GlusterfsVolumeSource)(nil), PersistentVolumeClaim:(*v1.PersistentVolumeClaimVolumeSource)(nil), RBD:(*v1.RBDVolumeSource)(nil), FlexVolume:(*v1.FlexVolumeSource)(nil), Cinder:(*v1.CinderVolumeSource)(nil), CephFS:(*v1.CephFSVolumeSource)(nil), Flocker:(*v1.FlockerVolumeSource)(nil), DownwardAPI:(*v1.DownwardAPIVolumeSource)(nil), FC:(*v1.FCVolumeSource)(nil), AzureFile:(*v1.AzureFileVolumeSource)(nil), ConfigMap:(*v1.ConfigMapVolumeSource)(nil), VsphereVolume:(*v1.VsphereVirtualDiskVolumeSource)(nil), Quobyte:(*v1.QuobyteVolumeSource)(nil), AzureDisk:(*v1.AzureDiskVolumeSource)(nil), PhotonPersistentDisk:(*v1.PhotonPersistentDiskVolumeSource)(nil), Projected:(*v1.ProjectedVolumeSource)(nil), PortworxVolume:(*v1.PortworxVolumeSource)(nil), ScaleIO:(*v1.ScaleIOVolumeSource)(nil), StorageOS:(*v1.StorageOSVolumeSource)(nil)}}, v1.Volume{Name:"default-token-86q99", VolumeSource:v1.VolumeSource{HostPath:(*v1.HostPathVolumeSource)(nil), EmptyDir:(*v1.EmptyDirVolumeSource)(nil), GCEPersistentDisk:(*v1.GCEPersistentDiskVolumeSource)(nil), AWSElasticBlockStore:(*v1.AWSElasticBlockStoreVolumeSource)(nil), GitRepo:(*v1.GitRepoVolumeSource)(nil), Secret:(*v1.SecretVolumeSource)(0xc0006169c0), NFS:(*v1.NFSVolumeSource)(nil), ISCSI:(*v1.ISCSIVolumeSource)(nil), Glusterfs:(*v1.GlusterfsVolumeSource)(nil), PersistentVolumeClaim:(*v1.PersistentVolumeClaimVolumeSource)(nil), RBD:(*v1.RBDVolumeSource)(nil), FlexVolume:(*v1.FlexVolumeSource)(nil), Cinder:(*v1.CinderVolumeSource)(nil), CephFS:(*v1.CephFSVolumeSource)(nil), Flocker:(*v1.FlockerVolumeSource)(nil), DownwardAPI:(*v1.DownwardAPIVolumeSource)(nil), FC:(*v1.FCVolumeSource)(nil), AzureFile:(*v1.AzureFileVolumeSource)(nil), ConfigMap:(*v1.ConfigMapVolumeSource)(nil), VsphereVolume:(*v1.VsphereVirtualDiskVolumeSource)(nil), Quobyte:(*v1.QuobyteVolumeSource)(nil), AzureDisk:(*v1.AzureDiskVolumeSource)(nil), PhotonPersistentDisk:(*v1.PhotonPersistentDiskVolumeSource)(nil), Projected:(*v1.ProjectedVolumeSource)(nil), PortworxVolume:(*v1.PortworxVolumeSource)(nil), ScaleIO:(*v1.ScaleIOVolumeSource)(nil), StorageOS:(*v1.StorageOSVolumeSource)(nil)}}}, InitContainers:[]v1.Container(nil), Containers:[]v1.Container{v1.Container{Name:"nginx", Image:"nginx", Command:[]string(nil), Args:[]string(nil), WorkingDir:"", Ports:[]v1.ContainerPort{v1.ContainerPort{Name:"", HostPort:0, ContainerPort:80, Protocol:"TCP", HostIP:""}}, EnvFrom:[]v1.EnvFromSource(nil), Env:[]v1.EnvVar{v1.EnvVar{Name:"CONFIGMAP", Value:"", ValueFrom:(*v1.EnvVarSource)(0xc000afd0c0)}, v1.EnvVar{Name:"SESCRET", Value:"", ValueFrom:(*v1.EnvVarSource)(0xc000afd100)}}, Resources:v1.ResourceRequirements{Limits:v1.ResourceList{"cpu":resource.Quantity{i:resource.int64Amount{value:100, scale:-3}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"100m", Format:"DecimalSI"}, "memory":resource.Quantity{i:resource.int64Amount{value:100, scale:6}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"100M", Format:"DecimalSI"}}, Requests:v1.ResourceList{"cpu":resource.Quantity{i:resource.int64Amount{value:100, scale:-3}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"100m", Format:"DecimalSI"}, "memory":resource.Quantity{i:resource.int64Amount{value:100, scale:6}, d:resource.infDecAmount{Dec:(*inf.Dec)(nil)}, s:"100M", Format:"DecimalSI"}}}, VolumeMounts:[]v1.VolumeMount{v1.VolumeMount{Name:"aol-deploy-configmap", ReadOnly:false, MountPath:"/etc/configmap", SubPath:"", MountPropagation:(*v1.MountPropagationMode)(nil)}, v1.VolumeMount{Name:"aol-deploy-secret", ReadOnly:false, MountPath:"/etc/secret", SubPath:"", MountPropagation:(*v1.MountPropagationMode)(nil)}, v1.VolumeMount{Name:"default-token-86q99", ReadOnly:true, MountPath:"/var/run/secrets/kubernetes.io/serviceaccount", SubPath:"", MountPropagation:(*v1.MountPropagationMode)(nil)}}, VolumeDevices:[]v1.VolumeDevice(nil), LivenessProbe:(*v1.Probe)(nil), ReadinessProbe:(*v1.Probe)(nil), Lifecycle:(*v1.Lifecycle)(nil), TerminationMessagePath:"/dev/termination-log", TerminationMessagePolicy:"File", ImagePullPolicy:"Always", SecurityContext:(*v1.SecurityContext)(nil), Stdin:false, Std 207 | inOnce:false, TTY:false}}, RestartPolicy:"Always", TerminationGracePeriodSeconds:(*int64)(0xc0004661d0), ActiveDeadlineSeconds:(*int64)(nil), DNSPolicy:"ClusterFirst", NodeSelector:map[string]string(nil), ServiceAccountName:"default", DeprecatedServiceAccount:"default", AutomountServiceAccountToken:(*bool)(nil), NodeName:"m7-power-k8s01", HostNetwork:false, HostPID:false, HostIPC:false, ShareProcessNamespace:(*bool)(nil), SecurityContext:(*v1.PodSecurityContext)(0xc0005b1560), ImagePullSecrets:[]v1.LocalObjectReference(nil), Hostname:"", Subdomain:"", Affinity:(*v1.Affinity)(nil), SchedulerName:"default-scheduler", Tolerations:[]v1.Toleration(nil), HostAliases:[]v1.HostAlias(nil), PriorityClassName:"", Priority:(*int32)(nil), DNSConfig:(*v1.PodDNSConfig)(nil), ReadinessGates:[]v1.PodReadinessGate(nil), RuntimeClassName:(*string)(nil), EnableServiceLinks:(*bool)(nil)}, Status:v1.PodStatus{Phase:"Pending", Conditions:[]v1.PodCondition{v1.PodCondition{Type:"PodScheduled", Status:"True", LastProbeTime:v1.Time{Time:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}, LastTransitionTime:v1.Time{Time:time.Time{wall:0x0, ext:63686264311, loc:(*time.Location)(0x1fc55e0)}}, Reason:"", Message:""}}, Message:"", Reason:"", NominatedNodeName:"", HostIP:"", PodIP:"", StartTime:(*v1.Time)(nil), InitContainerStatuses:[]v1.ContainerStatus(nil), ContainerStatuses:[]v1.ContainerStatus(nil), QOSClass:"Guaranteed"}} 208 | ``` 209 | 210 | 解决办法:将自定义类型添加到 scheme.AddKnownTypes 中; 211 | 212 | # 启用 client-go 中的日志 213 | 214 | ``` go 215 | flagset := flag.NewFlagSet("klog", flag.ExitOnError) 216 | flagset.Set("logtostderr", "true") 217 | flagset.Set("v", "4") 218 | klog.InitFlags(flagset) 219 | ``` 220 | 221 | # 添加自定义类型的步骤 222 | 223 | 1. 在 `pkg/apis/` 目录下添加自定义资源类型的 两级子目录,子目录中添加三个 go 文件,如: 224 | 225 | ``` text 226 | pkg/ 227 | apis/ 228 | aol/ 229 | v1alpha1/ 230 | doc.go 231 | register.go 232 | types.go 233 | ``` 234 | 235 | 2. 修改 register.go 文件中的常量 GroupName 和 SchemeGroupVersion 变量的 Version,与实际情况一致,如: 236 | 237 | ``` go 238 | // GroupName is the group name use in this package 239 | const GroupName = "aol.4pd.io" 240 | 241 | // SchemeGroupVersion is group version used to register these objects 242 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} 243 | ``` 244 | 245 | 3. 如果 目录与上面定义的 GroupName 不一致,则需要在 doc.go 文件中添加 +groupName 注释: 246 | 247 | ``` go 248 | // 来源于:pkg/apis/aol/v1alpha1/doc.go 249 | 250 | // +k8s:deepcopy-gen=package 251 | // +k8s:openapi-gen=true 252 | // +groupName=aol.4pd.io 253 | 254 | package v1alpha1 255 | ``` 256 | + 上面的 `+k8s:deepcopy-gen=package` 注释也是必须的,否则不会为 `types.go` 中的各 go struct 类型创建 DeepCopy 方法; 257 | 258 | 否则,后续自动生成的 fake clientset 的 Group 不对,如: 259 | ``` go 260 | // 未添加 +groupName 注释时,自动生成的 Group: "aol" 为 目录名称 261 | // 来源于:pkg/client/clientset/versioned/typed/aol/v1alpha1/fake/fake_aoldeployment.go 262 | var aoldeploymentsResource = schema.GroupVersionResource{Group: "aol", Version: "v1alpha1", Resource: "aoldeployments"} 263 | ``` 264 | 265 | 使用这个错误的 fake clientset 会引起单元测试失败: 266 | 267 | ``` text 268 | E0219 17:02:08.771250 80977 reflector.go:134] gitlab.4pd.io/pht3/aol/pkg/client/informers/externalversions/factory.go:117: Failed to list *v1alpha1.AolDeployment: no kind "AolDeploymentList" is registered for version "aol/v1alpha1" in scheme "gitlab.4pd.io/pht3/aol/pkg/client/clientset/versioned/fake/register.go:30" 269 | 270 | 4. 在 `types.go` 文件中添加自定义类型的 `go struct` 定义,参考:[Kubernetes Deep Dive: Code Generation for CustomResources](https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/) 271 | 5. 将自定义类型加到 `pkg/apis/aol/v1alpha1/register.go` 的 `addKnownTypes` 列表中; 272 | 6. 安装 code-generator 命令行工具: 273 | ``` go 274 | cd $GOPATH/src/k8s.io 275 | git clone https://github.com/kubernetes/code-generator.git 276 | go install code-generator/cmd/... 277 | export PATH=$GOPATH/bin:$PATH 278 | ``` 279 | 7. 执行 `hack/update-codegen.sh` 脚本,为自定义类型生成 `DeepCopyObject()` 方法定义(位于 pkg/apis/aol/v1alpha1/zz_generated.deepcopy.go 文件中)和 `client` (位于 pkg/client 各子目录下); 280 | 281 | # CRD Kind 冲突 Bug 282 | 283 | ``` bash 284 | [root@m7-power-k8s01 ~]# ETCDCTL_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} --cacert=/opt/k8s/work/ca.pem --cert=/opt/k8s/work/etcd 285 | .pem --key=/opt/k8s/work/etcd-key.pem get /registry/ --prefix --keys-only|grep aol 286 | /registry/apiextensions.k8s.io/customresourcedefinitions/aoldeployments.aol.4pd.io 287 | /registry/apiregistration.k8s.io/apiservices/v1alpha1.aol.4pd.io 288 | /registry/events/default/my-aol-deploy-566c9889b8-59dln.157cb613778bf01d 289 | /registry/events/default/my-aol-deploy-566c9889b8-59dln.157cb613a468c8f0 290 | /registry/events/default/my-aol-deploy-566c9889b8-59dln.157cb6148930c78a 291 | /registry/events/default/my-aol-deploy-566c9889b8-59dln.157cb6148b273cf9 292 | 293 | 294 | [root@m7-power-k8s01 ~]# ETCDCTL_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} --cacert=/opt/k8s/work/ca.pem --cert=/opt/k8s/work/etcd.pem --key=/opt/k8s/work/etcd-key.pem del /registry/apiextensions.k8s.io/customresourcedefinitions/aoldeployments.aol.4pd.io 295 | [root@m7-power-k8s01 ~]# ETCDCTL_API=3 etcdctl --endpoints=${ETCD_ENDPOINTS} --cacert=/opt/k8s/work/ca.pem --cert=/opt/k8s/work/etcd 296 | .pem --key=/opt/k8s/work/etcd-key.pem del /registry/apiregistration.k8s.io/apiservices/v1alpha1.aol.4pd.io 297 | 0 298 | ``` 299 | 300 | # 自定义资源对象的名称不能和 K8S 已有的重名 301 | 302 | ``` bash 303 | [root@m7-power-k8s02 examples]# cat aol-deploy-crd.yaml 304 | apiVersion: apiextensions.k8s.io/v1beta1 305 | kind: CustomResourceDefinition 306 | metadata: 307 | name: deployments.aol.4pd.io 308 | spec: 309 | group: aol.4pd.io 310 | version: v1alpha1 311 | names: 312 | kind: AolDeploy 313 | plural: aoldeploys 314 | singular: aoldeploy 315 | shortNames: 316 | - adeploy 317 | scope: Namespaced 318 | subresources: 319 | status: {} 320 | 321 | [root@m7-power-k8s02 examples]# kubectl apply -f aol-deploy-crd.yaml 322 | The CustomResourceDefinition "deployments.aol.4pd.io" is invalid: metadata.name: Invalid value: "deployments.aol.4pd.io": must be spec.names.plural+"."+spec.group 323 | 324 | I0124 11:40:25.316703 31235 reflector.go:169] Listing and watching *v1alpha1.Deployment from gitlab.4pd.io/pht3/aol/pkg/client/informers/externalversions/factory.go:117 325 | E0124 11:40:25.318620 31235 reflector.go:134] gitlab.4pd.io/pht3/aol/pkg/client/informers/externalversions/factory.go:117: Failed to list *v1alpha1.Deployment: the server could not find the requested resource (get deployments.aol.4pd.io) 326 | ``` 327 | 328 | # 给对象打 Path 的方法 329 | 330 | 1. 命令行: 331 | 332 | ``` bash 333 | kubectl patch deploy xxx -n default --type=json -p='[{"op":"remove", "path":"/metadata/finalizers/0"}]' 334 | ``` 335 | 336 | 2. 简单编写 Patch: 337 | 338 | ``` go 339 | deletePolicy := metav1.DeletePropagationForeground 340 | deleteOptions := metav1.DeleteOptions{ 341 | PropagationPolicy: &deletePolicy, 342 | } 343 | listOptions := metav1.ListOptions{ 344 | LabelSelector: fmt.Sprintf("controller=%s", object.GetName()), 345 | } 346 | if list, err := c.kubeclientset.ExtensionsV1beta1().Deployments(object.GetNamespace()).List(listOptions); err != nil { 347 | utilruntime.HandleError(err) 348 | return 349 | } else { 350 | for _, deploy := range list.Items { 351 | if err := c.kubeclientset.ExtensionsV1beta1().Deployments(object.GetNamespace()).Delete(deploy.GetName(), &deleteOptions); err != nil { 352 | utilruntime.HandleError(err) 353 | return 354 | } 355 | if dep, err := c.kubeclientset.ExtensionsV1beta1().Deployments(object.GetNamespace()).Patch( 356 | deploy.GetName(), 357 | types.JSONPatchType, 358 | []byte(fmt.Sprintf(`[{"op": "remove", "path": "/metadata/finalizerz"}]`))); err != nil { 359 | utilruntime.HandleError(err) 360 | return 361 | } 362 | } 363 | } 364 | ``` 365 | 366 | 3. 复杂的 Patch: 367 | 368 | ``` go 369 | // 参考值: 370 | // 1. https://github.com/tamalsaha/patch-demo/blob/master/main.go 371 | // 2. https://github.com/kubernetes/client-go/issues/236 372 | // 创建一个 Deploy 373 | ko, err = kubeClient.AppsV1beta1().Deployments(ko.Namespace).Create(ko) 374 | if err != nil { 375 | log.Fatalln(err) 376 | } 377 | 378 | // 将该 Deploy JSON 编码 379 | oJson, err := json.Marshal(ko) 380 | if err != nil { 381 | log.Fatalln(err) 382 | } 383 | 384 | // 修改 Deploy 的内容 385 | if ko.Annotations == nil { 386 | ko.Annotations = map[string]string{} 387 | } 388 | ko.Annotations["example.com"] = "123" 389 | ko.Spec.Replicas = gt.Int32P(2) 390 | ko.Spec.Template.Spec.Containers = append(ko.Spec.Template.Spec.Containers, apiv1.Container{ 391 | Name: "bnew", 392 | Image: "busybox", 393 | ImagePullPolicy: apiv1.PullIfNotPresent, 394 | Command: []string{ 395 | "sleep", 396 | "3600", 397 | }, 398 | VolumeMounts: []apiv1.VolumeMount{ 399 | { 400 | Name: TestSourceDataVolumeName, 401 | MountPath: TestSourceDataMountPath, 402 | }, 403 | }, 404 | }) 405 | // 将修改后的 Deploy JSON 编码 406 | mJson, err := json.Marshal(ko) 407 | if err != nil { 408 | log.Fatalln(err) 409 | } 410 | 411 | // 获取两个 JSON 的差别 412 | patch, err := jsonpatch.CreatePatch(oJson, mJson) 413 | if err != nil { 414 | log.Fatalln(err) 415 | } 416 | pb, err := json.MarshalIndent(patch, "", " ") 417 | if err != nil { 418 | log.Fatalln(err) 419 | } 420 | fmt.Println(string(pb)) 421 | 422 | // 发送 Patch 请求 423 | final, err := kubeClient.AppsV1beta1().Deployments(ko.Namespace).Patch(ko.Name, types.JSONPatchType, pb) 424 | if err != nil { 425 | log.Fatalln(err) 426 | } 427 | 428 | fb, err := json.MarshalIndent(final, "", " ") 429 | if err != nil { 430 | log.Fatalln(err) 431 | } 432 | fmt.Println(string(fb)) 433 | ``` 434 | 435 | # K8S Job 的 .spec.selector 和 .spec.template 是不能更新的 436 | 437 | 如果更新 K8S Job 的 .spec.template 如 image,就会出错: 438 | 439 | ``` text 440 | Job.batch "aol-test" is invalid: spec.template: Invalid value: xxx: field is immutable 441 | ``` 442 | 443 | 各 K8S 资源类型不可更新的字段,可以参考:https://github.com/pulumi/pulumi-kubernetes/blob/master/pkg/provider/diff.go --------------------------------------------------------------------------------