Client-go学习--Informer中的Store和Index

1,081 阅读8分钟

Store

写在前面

在上一篇文章Reflector源码解析中,我们介绍了在Informer结构中Reflector是如何通过List/Watch(全量更新/增量更新)获取K8s资源。那么这个时候,我们就会有疑问,获取到的K8s资源,我们应该如何存放?因为我们不可能每次都通过Reflector去对K8sApiServer发请求获取。假设集群中有1w个不同的工作负载,那么这种获取对Server的消耗是巨大的。此时我们就需要一个储存的组件,用来作为缓存。将第一次全量更新和后续增量更新的资源进行缓存,那么我们本地获取资源就可以通过获取该储存组件中的资源即可,减轻对Server的消耗。

1.引子

有细心的同学,可能会看见在上一篇我们对Reflector源码进行分析的时候,当第一次进行全量更新时,Reflector调用了syncWith进行全量更新,进行增量更新时,调用了store的Add/Update/Delete进行增量更新。

全量更新同步底层Store

// Reflector_全量更新List
func (r *Reflector) list(stopCh <-chan struct{}) error {
    // ........省略若干代码
    if err := r.syncWith(items, resourceVersion); err != nil {
        return fmt.Errorf("unable to sync list result: %v", err)
    }
}
​
​
func (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error {
    found := make([]interface{}, 0, len(items))
​
    // 新建一个新的items map
    for _, item := range items {
        found = append(found, item)
    }
    // 将item和版本号`revision`传入
    // 至此全量同步结束
    return r.store.Replace(found, resourceVersion)
}

增量同步底层Store

// Reflector_watchHandleFunc
// 省略若干代码....
case watch.Added:
    err := store.Add(event.Object)
case watch.Modified:
    err := store.Update(event.Object)
case watch.Deleted:
    err := store.Delete(event.Object)

那么接下来我们就对Store的源码进行探究,看一下其底层到底是如何实现存储和缓存的功能。

2.Store

2.1 Store接口定义

根据Store的接口,我们不难猜测其行为。这里就不进行赘述。Replace是将传入的items替换掉底层的items,Resync在DeltaFIFO中会进行实现(后续会介绍)。在这里是不会进行实现。

type Store interface {
    Add(obj interface{}) error
    Update(obj interface{}) error
    Delete(obj interface{}) error
    List() []interface{}
    // ListKeys返回当前所有key的列表
    ListKeys() []string
    Get(obj interface{}) (item interface{}, exists bool, err error)
    GetByKey(key string) (item interface{}, exists bool, err error)
    // Replace将使用给定的列表删除存储的内容,
    Replace([]interface{}, string) error
    // 在这里出现的术语中,Resync是没有意义的,
    // 但在一些具有重要附加行为(例如,DeltaFIFO)的实现中,Resync是有意义的。
    Resync() error
}

2.2 ThreadSafeStore

通过这个名字,不拿看出,线程安全的store,但是这种线程安全是在只读情况下才可以保证。我们往下看。

先看一下接口定义:

看起来和store大差不差,我们可以看出来多了几个和索引Index有关的函数,我们第三部分再进行介绍。

type ThreadSafeStore interface {
    Add(key string, obj interface{})
    Update(key string, obj interface{})
    Delete(key string)
    Get(key string) (item interface{}, exists bool)
    List() []interface{}
    ListKeys() []string
    Replace(map[string]interface{}, string)
    Index(indexName string, obj interface{}) ([]interface{}, error)
    IndexKeys(indexName, indexedValue string) ([]string, error)
    ListIndexFuncValues(name string) []string
    ByIndex(indexName, indexedValue string) ([]interface{}, error)
    GetIndexers() Indexers
    // AddIndexers向此存储添加更多索引器。
    // 如果在存储中已经有数据之后调用此函数,则结果是未定义的。
    AddIndexers(newIndexers Indexers) error
    // Resync is a no-op and is deprecated
    // 在这里resync是无意义的
    Resync() error
}

再来看一下ThreadSafeStore结构体的定义。一共就三个字段。一把锁,一个map,一个index索引。这个map实际上就是我们最底层存储从Reflector中获取的K8s工作负载资源。如果我们map存储的是指针,那么通过Get/List获取该map的工作负载,并且进行修改,那么则无法保证线程安全。所以必须是在只读情况下才保证线程安全。

itemsMap的key是有说法的,我们后面对其进行介绍。

type threadSafeMap struct {
    // 读写锁,控制并发安全
    lock sync.RWMutex
    // 实际存储obj item的map
    items map[string]interface{}
    // index implements the indexing functionality
    // Index实现了索引功能
    index *storeIndex
}

我们大概看一下Add/Update/Delete方法,其实都是清一色的两步走: 上个锁,进行CRUD操作。

// Add 传入store和obj
// 底层会调用update
func (c *threadSafeMap) Add(key string, obj interface{}) {
    c.Update(key, obj)
}
​
// Update 更新和新增
func (c *threadSafeMap) Update(key string, obj interface{}) {
    // 开个🔒
    c.lock.Lock()
    defer c.lock.Unlock()
​
    oldObject := c.items[key]
    // 新增/更新 实际存储的map
    c.items[key] = obj
    // 新增/更新 进行index映射的map
    // 如果没有indexer,则不会进行更新
    c.index.updateIndices(oldObject, obj, key)
}
​
func (c *threadSafeMap) Delete(key string) {
    // 开个🔒
    c.lock.Lock()
    defer c.lock.Unlock()
​
    // 如果存在则进行删除
    if obj, exists := c.items[key]; exists {
        // 避免删除失败 先调用index的删除
        c.index.updateIndices(obj, nil, key)
        // 然后再删除实际的map
        delete(c.items, key)
    }
}

3.⭐Index

Store部分最重要的就是Index了。Index贯穿了Store的存储。我们在使用Mysql这类关系型数据库都会有一个索引的概念,是为了加快检索。在Informer架构中,Index为工作负载提供了分片的方式加快检索

3.1 Index接口定义

Indexer接口组合了Store接口,进行拓展了Index功能。

type Indexer interface {
    // Store 
    Store
    // 对于命名索引,Index返回其index value set与给定对象的index value set相交的存储对象
    Index(indexName string, obj interface{}) ([]interface{}, error)
    // IndexKeys返回所存储对象的存储`key`,这些对象的命名索引的索引值集包含给定的索引值
    // 返回索引下存储对象的key
    IndexKeys(indexName, indexedValue string) ([]string, error)
    // 返回给定索引名的所有索引值
    ListIndexFuncValues(indexName string) []string
    // 根据给定的索引名和索引值返回存储的对象
    ByIndex(indexName, indexedValue string) ([]interface{}, error)
    // GetIndexers 返回索引器
    GetIndexers() Indexers
    // 添加更多的索引器
    AddIndexers(newIndexers Indexers) error
}

第一次看见会有些懵,我们循序渐进。先来解决上述ThreadSafeStore底层的itemsMap是如何存储对应的KV工作负载。

V对应着K8s工作负载。

K的key一般是通过已经定义的MetaNamespaceKeyFunc进行获取,当然,你也可以实现自定义的KeyFunc。通过调用MetaNamespaceKeyFunc(),获取其元数据,拿该工作负载的Namespace+Name进行拼接,成为Key。在调用ThreadSafeStore时,其实外部还会进行一层封装,会先通过调用keyFunc获取到key,再将获取的Key+资源对象传入ThreadSafeStore进行操作。

假设传入一个 configMap 其namespce=kube-system,name=kube-configMap。

那么最后生成的key就为kube-system/kube-configMap

func MetaNamespaceKeyFunc(obj interface{}) (string, error) {
    if key, ok := obj.(ExplicitKey); ok {
        return string(key), nil
    }
    objName, err := ObjectToName(obj)
    if err != nil {
        return "", err
    }
    // 假设pod为 namespace: default name:test-pod
    // 那么最后key生成为default/test-pod
    return objName.String(), nil
}
​
​
// 获取obj的元数据
func ObjectToName(obj interface{}) (ObjectName, error) {
    // 将通过accessor获取obj的元数据
    meta, err := meta.Accessor(obj)
    if err != nil {
        return ObjectName{}, fmt.Errorf("object has no meta: %v", err)
    }
    return MetaObjectToName(meta), nil
}
​
// 新建ObjectName对象设置对应值并返回
func MetaObjectToName(obj metav1.Object) ObjectName {
    if len(obj.GetNamespace()) > 0 {
        return ObjectName{Namespace: obj.GetNamespace(), Name: obj.GetName()}
    }
    return ObjectName{Namespace: "", Name: obj.GetName()}
}

3.2Index概念

明白store底层存储的map存储结构后,我们再来看一下Index。整个索引的核心就是由以下三个map进行相互映射。为了方便理解。我们先对索引的名词概念进行定义。

  • ThreadSafeStore中的ItemsMap的key:称为storeKey
  • Indices的key: 称为indexName
  • Index的key: 称为indexValue
// 索引 sets存放的是storeKey
type Index map[string]sets.String
// 索引器
type Indexers map[string]IndexFunc
// 索引片
type Indices map[string]Index

为了更直观的展示索引之间的关系,我们通过画图进行演示。

假设现在有5个pod,我希望根据namespace进行index分类,那么就可以划分出如下图。

大概流程:首先我们需要通过IndexFunc获取到IndexValue,也就是放在哪个IndevValue中,然后再通过上述的KeyFunc获取StoreKey,一个是将该StoreKey设置到对应的IndexValue的map中,一个是将StoreKey设置到ThreadSafeStore中。

image.png

我们可以看一下对应的代码是如何实现。index.go中默认提供了根据namespace就行获取IndexValue的函数,我们可以进行拓展实现,例如根据labels,annotations获取IndexValue进行Index分片。上面介绍的,Indexers其实就是存放了根据IndexName对应的用于获取IndexValue的IndexFunc。

func MetaNamespaceIndexFunc(obj interface{}) ([]string, error) {
    meta, err := meta.Accessor(obj)
    if err != nil {
        return []string{""}, fmt.Errorf("object has no meta: %v", err)
    }
    return []string{meta.GetNamespace()}, nil
}

3.3 cache

cache实现了store和index的功能。

cache结构体

//  cache'通过ThreadSafeStore和关联的keyFunc实现Indexer。
type cache struct {
    // 底层使用ThreadSafeStore
    cacheStorage ThreadSafeStore
    // keyFunc 实际映射的是 `storeKey` -- `obj`
    keyFunc KeyFunc
}

cache创建函数,如果不传入Indexers,将不会使用索引。因为底层的索引并不知道该如何获取对应的IndexValue。

// ⭐: 实际上调用这个函数新建store,证明不需要用index进行索引,而是直接存储入底层的threadStore中
func NewStore(keyFunc KeyFunc) Store {
    return &cache{
        // 底层使用了锁控制并发
        cacheStorage: NewThreadSafeStore(Indexers{}, Indices{}),
        keyFunc:      keyFunc,
    }
}
​
// 注意: ** 如果不传入IndexFunc 则不会使用Index进行映射 **
func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer {
    return &cache{
        cacheStorage: NewThreadSafeStore(indexers, Indices{}),
        keyFunc:      keyFunc,
    }
}

我们看一下cache中的Add/Update/Delete。可以看出,首先先通过keyFunc获取到storeKey,再调用底层的ThreadSafeStore进行操作。

// Add inserts an item into the cache.
// 新增
func (c *cache) Add(obj interface{}) error {
    // 通过keyFunc获取obj的`storeKey`
    key, err := c.keyFunc(obj)
    if err != nil {
        return KeyError{obj, err}
    }
    // 调用底层的threadStore进行添加
    c.cacheStorage.Add(key, obj)
    return nil
}
​
// Update sets an item in the cache to its updated state.
// 更新
func (c *cache) Update(obj interface{}) error {
    key, err := c.keyFunc(obj)
    if err != nil {
        return KeyError{obj, err}
    }
    c.cacheStorage.Update(key, obj)
    return nil
}
​
// Delete removes an item from the cache.
// 删除
func (c *cache) Delete(obj interface{}) error {
    key, err := c.keyFunc(obj)
    if err != nil {
        return KeyError{obj, err}
    }
    c.cacheStorage.Delete(key)
    return nil
}

ThreadSafeStore进行增删改的时候,监测到有索引器的存在,则会进行索引设置。通过IndexFunc获取到对应的IndexValue。然后根据不同的操作,对不同的IndexValue对应的Map进行操作。

// updateIndices修改对象在位于indexes中的位置:
// —对于创建,您必须只提供newObj
// —对于更新,您必须同时提供oldObj和newObj
// —对于删除,您必须只提供oldObj。
// updateIndices 必须从已经锁定缓存的函数中调用
// 更新index
// 通过不同的map进行相互映射
func (i *storeIndex) updateIndices(oldObj interface{}, newObj interface{}, key string) {
    var oldIndexValues, indexValues []string
    var err error// 循环 indexers获取生成indexValue的Func
    // 如果indexers索引器不存在 则不会进行索引设置
    for name, indexFunc := range i.indexers {
        if oldObj != nil {
            // 传入oldObj获取indexValue
            oldIndexValues, err = indexFunc(oldObj)
        } else {
            // 置为空{}
            oldIndexValues = oldIndexValues[:0]
        }
​
        // 如果有错误则直接panic
        if err != nil {
            panic(fmt.Errorf("unable to calculate an index entry for key %q on index %q: %v", key, name, err))
        }
​
        if newObj != nil {
            // 传入newObj获取indexValue
            indexValues, err = indexFunc(newObj)
        } else {
            // 置为空
            indexValues = indexValues[:0]
        }
        if err != nil {
            panic(fmt.Errorf("unable to calculate an index entry for key %q on index %q: %v", key, name, err))
        }
​
        // 获取详细index
        index := i.indices[name]
        // 如果没有会进行创建
        if index == nil {
            index = Index{}
            i.indices[name] = index
        }
​
        // 实际上修改就是 删除一个旧的 新增一个新的 故直接continue即可
        // 也可以理解为 新旧的东西都是同一个,则index映射是不需要改变的
        if len(indexValues) == 1 && len(oldIndexValues) == 1 && indexValues[0] == oldIndexValues[0] {
            // We optimize for the most common case where indexFunc returns a single value which has not been changed
            continue
        }
​
        for _, value := range oldIndexValues {
            // 删除storeKey在IndexValue对应的Map中
            i.deleteKeyFromIndex(key, value, index)
        }
        for _, value := range indexValues {
            // 插入storeKey到IndexValue对应的Map中
            i.addKeyToIndex(key, value, index)
        }
    }
}

4.小试牛刀

通过上面的学习,我们大概能明白Index,Store之间的关系。Store通过storeKey :obj的map进行存储对应的K8s工作负载。Index通过IndexName + IndexValue根据不索引器映射不同的索引值,在IndexValue :storeKey的map进行存储。同时了解了KeyFunc是用来获取storeKey的函数,IndexFunc是用来获取IndexValue的函数。那么接下来我通过一个例子进行巩固。

FullCode

// 自定义的IndexFunc
func LabelsIndexFunc(obj interface{}) ([]string, error) {
    metaObj, err := meta.Accessor(obj)
    if err != nil {
        return []string{}, err
    }
    return []string{metaObj.GetLabels()["CI.io"]}, nil
}
​
// test func
func TestFullCodeTest(t *testing.T) {
    pod1 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "one", Labels: map[string]string{"CI.io": "CI.dev"}}, Spec: corev1.PodSpec{NodeName: "node-1"}}
    pod2 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "two", Labels: map[string]string{"CI.io": "CI.dev"}}}
    pod3 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "tre", Labels: map[string]string{"CI.io": "CD.prod"}}}
​
    indexer := NewIndexer(MetaNamespaceKeyFunc, Indexers{
        "labels": LabelsIndexFunc,
    })
​
    indexer.Add(pod1)
    indexer.Add(pod2)
    indexer.Add(pod3)
​
    for _, v := range indexer.List() {
        pod := v.(*corev1.Pod)
        fmt.Printf("v: %+v\n", pod.Name)
    }
}

我们可以通过打断点的方式看到,底层的items存放了对应storeKey:obj,Indices中存放着IndexName=labels,IndexValue='CI.dev'/IndexValue='CD.prod',其中对应的IndexValue存放着正是对应的storeKey。

image.png

写在最后

本文对store和index进行了讲解,仅列举了store中部分关键函数的源码,其余源码可以自行去研究学习。此时我们又会产生一个疑问,store支持的功能是在是太少了,只有基本的CRUD功能。我们需要监测同一个资源对象的增量变化,还希望能够异步处理,事件通知等功能。下一篇我们就针对Reflector于store中间的DelataFIFO进行解析。