apiserver是整个kubernetes的核心模块,做的事情多,代码量也较大。市面上已经有不少apiserver代码解读的文章了,但问题在于,由于k8s的代码变化很快,想写一篇长久能用的未必能做到。所以,我参照了《Kubernetes权威指南》(HP中国出的)和浙大SEL实验室的一些文章,先把我看到的东西记下来,待后观是否有用。
kubernetes源代码版本1.2.0
先简单讲讲整个代码的目录结构
| 目录 | 说明 |
| ----------- | ---------------------------------------- |
| api | 输出接口文档用 |
| build | 构建脚本 |
| cluster | 适配不同I层的云,例如亚马逊AWS,微软Azure,谷歌GCE的集群启动脚本 |
| cmd | 所有的二进制可执行文件入口代码,例如apiserver/scheduler/kubelet |
| contrib | 项目贡献者 |
| docs | 文档,包括了用户文档、管理员文档、设计、新功能提议 |
| example | 使用案例 |
| Godeps | 项目中依赖使用的Go第三方包,例如docker客户端SDK,rest等 |
| hack | 工具箱,各种编译、构建、测试、校验的脚本都在这里面 |
| hooks | git提交前后触发的脚本 |
| pkg | 项目代码主目录,cmd的只是个入口,这里是所有的具体实现 |
| plugin | 插件,k8s认为调度器是插件的一部分,所以调度器的代码在这里 |
| release | 应该是Google发版本用的? |
| test | 测试相关的工具 |
| third_party | 一些第三方工具,应该不是强依赖的? |
| www | UI,不过已经被移动到新项目了 |
可以看到,关键实现代码都放在pkg这个目录下。对于apiserver这种跨度很广的组件而言,唯一有效的阅读方式估计就是
![输入图片说明]( https://static.oschina.net/upl ... g.png "在这里输入图片标题")
apiserver是k8s系统中所有对象的增删查改盯的http/restful式服务端,其中盯是指watch操作。数据最终存储在分布式一致的etcd存储内,apiserver本身是无状态的,提供了这些数据访问的认证鉴权、缓存、api版本适配转换等一系列的功能。
对于http服务和使用go语言实现方式,可以看go-restful的 文档 和 例子 ,对这个有基本的了解,这个文档对入门者和一知半解者极为有效!
![输入图片说明]( https://static.oschina.net/upl ... 4.png "在这里输入图片标题")
古人有言,程序就是算法 数据结构,搞懂了数据结构,整个程序的处理过程就明白了一半。对于apiserver的任何一个api请求来说,上图说明了所有的数据结构关系。
k8s放在etcd内的存储对象是api.Pod对象(无版本),从不同版本的请求路径标识来操作,例如 api/v1
,最后获取到的是不同版本,例如 v1.Pod
的json文本。这里就经历了几个过程,包括
其中用于处理业务数据的关键数据结构是APIGroupVersion,里面的几个成员变量的作用是:
| 成员 | 作用 |
| ------------ | ---------------------------------------- |
| GroupVersion | 包含 api/v1
这样的string,用于标识这个实例 |
| Serializer | 对象序列化和反序列化器 |
| Converter | 这是一个强大的数据结构,这里放的是个接口,本体在 /pkg/conversion/conversion.go
,几乎可以转换任意一种对象到另一种,只要你事先注入了相应的转换函数 |
| Storage | 这个map的key,用于对象的url,value是一个rest.Storage结构,用于对接etcd存储,在初始化注册时,会把这个map化开,化为真正的rest服务到存储的一条龙服务 |
| 文件 | 主要数据结构/函数 | 用途 |
| ---------------------------------------- | -------------------- | ------------------- |
| kubernetes/cmd/kube-apiserver/apiserver.go | | 入口 |
| kubernetes/cmd/kube-apiserver/app/options/options.go | struct APIServer | 启动选项 |
| kubernetes/cmd/kube-apiserver/apiserver.go | func Run | 初始化一些客户端、启动master对象 |
| kubernetes/pkg/genericapiserver/genericapiserver.go | func Run | 启动安全和非安全的http服务 |
![输入图片说明]( https://static.oschina.net/upl ... v.png "在这里输入图片标题")
k8s采用 ApiGroup 来管理所有的api分组和版本升级,目前有的API分组包括
/api/v1
,但这个路径不是固定的,v1是当前的版本。与之相对应的代码里面的 apiVersion
字段的值是 v1
。 /apis/extensions/$VERSION
,相对应的代码里面的 apiVersion: extensions/$VERSION
(例如当前的 apiVersion: extensions/v1beta1
)。这里提供的API对象今后有可能会被移动到别的组内。 在 这个文档 里面讲述了实现ApiGroup的几个目标,包括api分组演化,对旧版API的向后兼容(Backwards compatibility),包括用户可以自定义自己的api等。接下来我们看看他么是怎么初始化注册的,这里都是缩减版代码,去掉了其他部分。
kubernetes/pkg/master/master.go
go func New(c *Config) (*Master, error) { m.InstallAPIs(c) }
- 根据Config往 APIGroupsInfo
内增加组信息,然后通过 InstallAPIGroups
进行注册
go func (m *Master) InstallAPIs(c *Config) { if err := m.InstallAPIGroups(apiGroupsInfo); err != nil { glog.Fatalf("Error in registering group versions: %v", err) } }
APIGroupVersion
这个关键数据结构,然后进行注册 ```go
func (s *GenericAPIServer) installAPIGroup(apiGroupInfo *APIGroupInfo) error {
apiGroupVersion, err := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
if err := apiGroupVersion.InstallREST(s.HandlerContainer); err != nil {
return fmt.Errorf("Unable to setup API %v: %v", apiGroupInfo, err)
}
}
```
go kubernetes/pkg/apiserver/apiserver.go
```go
type APIGroupVersion struct {
Storage map[string]rest.Storage
Root string
// GroupVersion is the external group version
GroupVersion unversioned.GroupVersion
}
```
实际注册的Storage的map如下:
kubernetes/pkg/master/master.go
m.v1ResourcesStorage = map[string]rest.Storage{ "pods": podStorage.Pod, "pods/attach": podStorage.Attach, "pods/status": podStorage.Status, "pods/log": podStorage.Log, "pods/exec": podStorage.Exec, "pods/portforward": podStorage.PortForward, "pods/proxy": podStorage.Proxy, "pods/binding": podStorage.Binding, "bindings": podStorage.Binding,
那么,这里的 map[string]rest.Storage
最后是怎么变成一个具体的API来提供服务的呢?例如这么一个URL:
GET /api/v1/namespaces/{namespace}/pods/{name}
k8s使用的一个第三方库 github.com/emicklei/go-restful
,里面提供了一组核心的对象,看 例子
| 数据结构 | 功能 | 在k8s内的位置 |
| ------------------ | ---------------------------------------- | ---------------------------------------- |
| restful.Container | 代表一个http rest服务对象,包括一组restful.WebService | genericapiserver.go - GenericAPIServer.HandlerContainer |
| restful.WebService | 由多个restful.Route组成,处理这些路径下所有的特殊的MIME类型等 | api_installer.go - NewWebService() |
| restful.Route | 路径——处理函数映射map | api_installer.go - registerResourceHandlers() |
go kubernetes/pkg/apiserver/api_installer.go
go func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*unversioned.APIResource, error) { }
最终的API注册过程是在这个函数中完成的,把一个rest.Storage对象转换为实际的getter, lister等处理函数,并和实际的url关联起来。
上面已经基本厘清了从http请求 -> restful.Route -> rest.Storage这条线路,那rest.Storage仅仅是一个接口,有何德何能,可以真正的操作etcd呢?
![输入图片说明]( https://static.oschina.net/upl ... y.png "在这里输入图片标题")
这段也是牵涉到多个文件,但还比较清晰,首先,所有的对象都有增删改查这些操作,如果为Pod单独搞一套,Controller单独搞一套,那代码会非常重复,不可复用,所以存储的关键目录是在这里:
kubernetes/pkg/registry/generic/etcd/etcd.go
这个文件定义了所有的对etcd对象的操作,get,list,create等,但具体的对象是啥,这个文件不关心;etcd客户端地址,这个文件也不关心。这些信息都是在具体的PodStorage对象创建的时候注入的。以Pod为例子,文件在:
kubernetes/pkg/registry/pod/etcd/etcd.go
这里的 NewStorage
方法,把上述的信息注入了etcd里面去,生成了PodStorage这个对象。
// REST implements a RESTStorage for pods against etcd type REST struct { *etcdgeneric.Etcd proxyTransport http.RoundTripper }
由于PodStorage.Pod是一个REST类型,而REST类型采用了Go语言的struct匿名内部成员,天然就拥有Get, List等方法。
kubernetes/pkg/apiserver/api_installer.go
最后在这里把PodStorage转换成了Getter对象,并最终注册到 ApiGroup
里面去。