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