概述 #
apiserver是K8S几个核心组件里面最重要的一个,它对内封装了对etcd的访问,对外提供REST接口给其他组件使用。其他的组件,比如属于核心组件的controller manager、kubelet、scheduller等,本质上都是watch在apiserver上的服务,其工作本质上就是通过apiserver监听对应资源的变更情况,然后根据自己的业务来完成相关的操作。从这里可以看到,apiserver的重要性比其他K8S核心组件更高,只要apiserver异常,整个系统都无法正常运行了。
apiserver要完成的核心工作包括如下几件事情:
- 提供REST接口给各种客户端调用,这些接口可以针对不同的K8S资源(pod、service等)进行CRUD操作,外加还可以进行监听其变化的操作。
- 封装了对etcd的访问操作,K8S的其他组件并不会直接操作etcd,都是通过apiserver。其原因在于,K8S里面的资源定义,都有一个对应的版本号,比如在定义一个StatefulSet的时候,其yaml文件中需要指定apps/v1beta1这个版本号,同样的资源在不同版本的实现中可能会有差异。而对于保存到etcd的数据而言,这些版本号相关的meta信息,是不需要etcd这个只负责存储的组件去关心的,这些都是在apiserver中完成。
- 既然前面提到了版本号,那么在向etcd进行读写操作的时候,就涉及到不同版本之间数据转换的问题,这个操作也是由apiserver来完成的。
所以,看上去apiserver只是负责调用etcd完成CRUD操作,可是实际上它的设计考量还是很多。
go-restful框架简述 #
由于apiserver中使用go-restful这个框架来提供REST接口,所以先提前做一些了解。
核心数据结构 #
来回顾一下前面提到的apiserver要做的事情,以此为出发点来看看apiserver相关的实现。
当通过kubectl客户端向apiserver发出请求时,可以打开日志级别,看到其中详细的流程,比如执行:
kubectl get pod -n kube-system -v=8 tengine-ingress-controller-smsnz
能看到流程中相关的日志是:
I0928 16:58:25.826473 66127 loader.go:357] Config loaded from file /home/nemo/.kube/config
I0928 16:58:25.842393 66127 round_trippers.go:414] GET https://apiserver:6443/api/v1/namespaces/kube-system/pods/tengine-ingress-controller-smsnz
I0928 16:58:25.842424 66127 round_trippers.go:421] Request Headers:
I0928 16:58:25.842436 66127 round_trippers.go:424] Accept: application/json
I0928 16:58:25.842445 66127 round_trippers.go:424] User-Agent: kubectl/v1.9.3 (linux/amd64) kubernetes/c5cea03
I0928 16:58:25.857103 66127 round_trippers.go:439] Response Status: 200 OK in 14 milliseconds
I0928 16:58:25.857116 66127 round_trippers.go:442] Response Headers:
I0928 16:58:25.857121 66127 round_trippers.go:445] Audit-Id: 5c973d4f-faba-415f-89fa-f4f3dd3b2bf7
I0928 16:58:25.857126 66127 round_trippers.go:445] Content-Type: application/json
I0928 16:58:25.857131 66127 round_trippers.go:445] Date: Fri, 28 Sep 2018 08:58:25 GMT
I0928 16:58:25.857433 66127 request.go:873] Response Body: {"kind":"Pod","apiVersion":"v1","metadata":{"name":"tengine-ingress-controller-smsnz","generateName":"tengine-ingress-controller-","namespace":"kube-system","selfLink":"/api/v1/namespaces/kube-system/pods/tengine-ingress-controller-smsnz","uid":"617f505d-c160-11e8-b8fb-6c92bf6b2d28","resourceVersion":"115930452","creationTimestamp":"2018-09-26T07:47:13Z","labels":{"controller-revision-hash":"1353751459","k8s-app":"tengine-ingress-controller","pod-template-generation":"11504"},"annotations":{"prometheus.io/port":"10254","prometheus.io/scrape":"true"},"ownerReferences":[{"apiVersion":"extensions/v1beta1","kind":"DaemonSet","name":"tengine-ingress-controller","uid":"22d6ca2b-f794-11e7-831e-042758486066","controller":true,"blockOwnerDeletion":true}]},"spec":{"volumes":[{"name":"nginx-log","hostPath":{"path":"/home1/system/ingress/nginx/log","type":""}},{"name":"logsender-log","hostPath":{"path":"/home1/system/ingress/logsender/log","type":""}},{"name":"logsender-plugin-log","hostPath":{"path":"/home1/system/ingre [truncated 3760 chars]
从这里能看到:
- 向地址为apiserver:6443发起请求(这里为了保护隐私apiserver已经被我替换了相关的域名):“GET https://apiserver:6443/api/v1/namespaces/kube-system/pods/tengine-ingress-controller-smsnz”,其中的路径”/api/v1/namespaces/kube-system/pods/tengine-ingress-controller-smsnz“中有如下层次的划分:
- api:api表,还有另外的/apis路径,则是其它的api注册路径了。
- v1:版本号。
- /namespaces/kube-system/pods/tengine-ingress-controller-smsnz:查询名字空间kube-system下的pods资源tengine-ingress-controller-smsnz信息。
可以看到,前面的请求路径中,请求的表、版本号、 名字空间、资源名称等都可以变化的,而apiserver提供的REST接口就要根据这些参数来做处理。
-
其次,apiserver后面的存储后端是etcd,因此在处理一个请求时,要转换为对应的etcd请求,在拿到etcd数据之后再组装返回给客户端。
-
最后,etcd返回的数据需要根据客户端的需求,序列化为json、yaml等格式进行转换给客户端。
综上,apiserver做的事情归纳起来就是:
- 如何根据不同的版本、资源来注册对应的REST处理函数?
- 如何将对应资源的请求映射为针对某个etcd数据的访问?
- 如何将etcd返回的数据根据不同的序列化格式序列化返回给客户端?
以下按照这几个主线分别展开讨论。
注册REST处理函数 #
apiserver中将不同的资源,使用一个接口Storage进行抽象:
(vendor/k8s.io/apiserver/pkg/registry/rest/rest.go)
type Storage interface {
// New returns an empty object that can be used with Create and Update after request data has been put into it.
// This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object)
New() runtime.Object
}
可以认为每一个k8s资源,都是一个Storage接口的具体实现。除此之外,还有另外一些接口继承了Storage的实现,比如:
(vendor/k8s.io/apiserver/pkg/registry/rest/rest.go)
type Creater interface {
// New returns an empty object that can be used with Create after request data has been put into it.
// This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object)
New() runtime.Object
// Create creates a new version of a resource. If includeUninitialized is set, the object may be returned
// without completing initialization.
Create(ctx genericapirequest.Context, obj runtime.Object, createValidation ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error)
}
可以看到,Creater接口除了实现了Storage的New函数之外,还有一个Create接口需要实现。这是因为,不同的资源可以进去的CRUDW操作是不同的,除了Creater接口之外,还有Lister、Watcher接口的,对应的就是相关的操作。
因此,一个具体的k8s资源,需要实现以下的接口:
- New接口:用于创建相应的资源。
- 其他操作接口:用于进行相关的操作。
以pod资源来看,其对应的Storage就是pkg/registry/core/pod/storage/中的REST结构体:
(pkg/registry/core/pod/storage/storage.go)
// REST implements a RESTStorage for pods
type REST struct {
*genericregistry.Store
proxyTransport http.RoundTripper
}
可以看到这个结构体继承自genericregistry.Store,这个Store结构体中提供了以上这些函数的成员,这里就不贴出来了。
而在具体代码中,又是怎么知道这个Storage接口具体实现了哪些操作呢,很简单通过类型断言:
(vendor/k8s.io/apiserver/pkg/endpoints/installer.go)
// what verbs are supported by the storage, used to know what verbs we support per path
creater, isCreater := storage.(rest.Creater)
namedCreater, isNamedCreater := storage.(rest.NamedCreater)
lister, isLister := storage.(rest.Lister)
在存储不同版本的API注册信息时,用到的是APIGroupInfo结构体:
- VersionedResourcesStorageMap map[string]map[string]rest.Storage:保存不同版本号的资源注册信息。如"v1" -> “pods -> rest.Storage”,这里的rest.Storage就是前面分析过的Storage接口。
APIGroupInfo在NewLegacyRESTStorage中进行初始化,其中有一段就是注册各种资源对应的Storage接口:
restStorageMap := map[string]rest.Storage{
"pods": podStorage.Pod,
"pods/attach": podStorage.Attach,
// ....
apiGroupInfo.VersionedResourcesStorageMap["v1"] = restStorageMap
接下来,在函数installAPIResources中,会遍历APIGroupInfo中的所有版本号,创建其对应的APIGroupVersion结构体,再调用InstallREST函数将这些APIGroupVersion依次注册,最终这个注册动作会走到registerResourceHandlers函数中。这个函数的核心功能总结如下:
- 根据传入的rest.Storage接口,拿到该接口分别实现了哪些操作,将这些操作注册到一个叫actions的数组中。
- 遍历actions数组,将它们转换go-restful的路由相关结构体,最终注册到go-restful的WebService中。
- 最后该函数返回的WebService结构体了,在外部再将该WebService结构体注册到Container中,这样就完成了某类资源的REST接口注册。
etcd访问 #
前面提到过,任何一个资源的访问,apiserver只是代理,最终会访问到存储在etcd服务中的数据,接下来就看看如何把资源请求与etcd数据访问结合在一起的。