k8s开发(三) informer上

182 阅读5分钟

一,index需求背景

根据上篇对client-go的介绍,其实已经足以对k8s资源进行管理了,例如用list,watch实现replicate伪代码:

func main(){
    //watcher pod变化
    //watcher replicate变化
}

//处理pod变化
func handlerPod(pod){
    //通过client-go replicate list 拿到所有replicate
    //找到pod对应的replicate
    //通过client-go pod list 拿到所有pod
    //找到replicate对应pod
    //比较replicate与现有的pod数量
    //对pod进行增删操作
}

//处理replicate变化
func handlerReplicate(pod){
    //通过client-go pod list 拿到所有pod
    //找到replicate对应pod
    //比较replicate与现有的pod数量
    //对pod进行增删操作
}

从上述伪代码发现,如果仅仅用list,watch实现控制器逻辑,只要有资源变动,都会导致数据拉取,会导致apiserver压力过大,很自然会想到在服务内做一个内存缓存

二,index设计

2.1 存储设计

在开发中内存缓存我们最容易想到的map结构,key作为主索引值,value作为需要存储的对象。以及需要考虑key从哪里来

可以有一个专门的keyFunc从对象中提取一个string作为主键key,并由使用者自行实现keyFunc

//会与官方结构有所差别
type storeIndex struct{
    //用来缓存对象
    items map[string]interface{}
    
    //用来从对象中提取key值
    keyFunc func(obj interface{}) (string, error)
}

2.2 索引表设计

上面结构体就可以存储对象,也能通过主key获得具体对象了,但是只能通过一个键值索引,索引能力太弱了,例如有时候需要通过对象label去获取,有时候需要根据namespace,那我们是不是可以考虑参考数据库设计,设计成存储数据的主表和辅助查询的索引表indices

//会与官方结构有所差别
type storeIndex struct{
    //用来缓存对象
    items map[string]interface{}
    
    //key为索引值,val为主键值数组,因为一个索引值可能会对应多个数据,官方设计为一个Index对象,为string的set
    indices map[string][]string
    
    //用来从对象中提取key值,
    keyFunc func(obj interface{}) (string, error)
}

2.3 索引indexFunc设计

有了索引表indices,我们就需要考虑这些索引值怎么来?

1,沿用keyFunc的设计方式,依旧可以采用func方式进行设计,只是keyFunc返回值是string,但是索引indexFunc返回值应该为[]string,例如pod对象可能有多个label,每个label都是一个索引值

2,主键func一个就够,毕竟主键具有唯一性,但索引func一个对于通用存储就不够用了,例如有的场景需要提取namespace作为索引值,有的需要提取label作为索引值

3,以及还需要考虑在什么场景用什么样的索引func,毕竟不能在需要提取namespace时候用了提取label的方法,那么对索引func的存储也应该是一个mapkey应该是场景名称valfunc

//会与官方结构有所差别
type storeIndex struct{
    //用来缓存对象
    items map[string]interface{}
    
    //key为索引值,val为主键值数组,因为一个索引值可能会对应多个数据,官方设计为一个Index对象,为string的set
    indices map[string][]string
    
    //主键提取方法,用来从对象中提取主键key值,
    keyFunc func(obj interface{}) (string, error)
    
    //索引提取方法集合,key为场景名称,value为索引提取方法
    indexers map[string]func(obj interface{}) ([]string, error)
}

2.4 增删改查

有了整个存储结构设计,对数据增删改查也就相对好设计了

增删改都属于数据的变动,数据的变动除了要修改主表数据,还会引发索引表的变动,那么需要一个updateIndices方法来维护索引表变动


func (i *storeIndex) updateIndices(oldObj interface{}, newObj interface{}, key string) {
    //for循环遍历所有的索引方法
        //根据索引方法拿出新旧数据索引
        //移除旧数据中索引下面存的主键key,这样旧索引就不在含有主键了
        //将新数据索引下面存下主键key,这样新索引就与主键进行了绑定
}

查找除了通过主键查找,应该还有通过索引查找方法

//indexName索引名称,即上文的场景,源码方法在threadSafeMap中
func (i *storeIndex) Index(indexName string, obj interface{}) ([]interface{}, error) {
}

三,client-go cache完整设计

上面是对缓存的一个简单设计,client-go中的模块更细,层次更深,以及还要考虑线程安全问题

image.png

3.1 cache

cache是整个缓存的实现类,他通过组合其他组件实现了数据增删改查索引维护功能,但真正的核心代码并不在cache中,大部分功能要么通过keyFunc获取主键key,或者直接透传参数treadSafeMap

3.2 treadSafeMap

treadSafeMap是存储功能的实现,里面包含了一把读写锁,因为应用场景上读写基本不在同一个协程。

3.3 storeIndex

treadSafeMap专注在数据操作上,索引方法维护以及提取索引值索引表的维护都由storeIndex负责

3.4 Indexer,Store接口

Store接口定义了数据操作接口Indexer包含了Store接口之外还定义了索引相关操作

那为什么不直接把Store的接口定义在Indexer中?

估计是实现这个功能的作者希望做到最小知道原则,例如在写入的功能模块中并不关心他是否具有索引查询功能,我只要有对应的Store写入接口调用就行