一,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的存储也应该是一个map,key应该是场景名称,val为func
//会与官方结构有所差别
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中的模块更细,层次更深,以及还要考虑线程安全问题
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写入接口调用就行