做者:高朋, 科賽網(kesci.com)工程師。
致力於讓深度學習和數據分析走進我的的開放平臺,已完成全部數據分析平臺部署於K8S。
如下是 k8s 的總體架構,在 master 節點上主要是 kube-apiserver(整合了 kube-aggregator),還有 kube-scheduler,以及 kube-controller-manager,包括後端存儲 etcd。 node
其中 kube-apiserver 是一個比較關鍵的部分,並且前期寫得坑不少,致使這一部分雖然看起來是一個 API server 其實代碼很複雜,特別冗餘,並且目前對 kube-apiserver 還要作拆分,可以支持插入第三方的 apiserver,也就是又一個 aggregated apiserver 的 feature,也是和 kube-apiserver 和裏面包的一層 genericserver 揉合在一塊兒了,感受一個大的系統 API server 越寫越挫是一個通病,還好如今 k8s 迷途知返正在調整。linux
Kube-apiserver 能夠是認爲在 generic server 上封裝的一層官方默認的 apiserver,有第三方須要的狀況下,本身也能夠在 generic server 上封裝一層加入到集成模式中,這裏主要介紹 kube-apiserver 的結構。git
kube-apiserver 是一個 restful 服務,請求直接經過 HTTP 請求發送,例如建立一個 ubuntu 的 pod,用如下的 pod.yaml 文件。github
apiVersion: v1
kind:
Pod
metadata:
name: ubuntu1
labels:
name: ubuntu1
spec:
containers:
- name: ubuntu1
image: ubuntu
command: [
"sleep"
,
"1d"
]
複製代碼
執行命令 kubectl create -f ./pod.yaml -v=8,能夠看到對應的 POST 請求以下。json
Request
Body
: {
"apiVersion"
:
"v1"
,
"kind"
:
"Pod"
,
"metadata"
:{
"labels"
:{
"name"
:
"ubuntu1"
},
"name"
:
"ubuntu1"
,
"namespace"
:
"default"
},
"spec"
:{
"containers"
:[{
"command"
:[
"sleep"
,
"1d"
],
"image"
:
"ubuntu"
,
"name"
:
"ubuntu1"
}],
"schedulerName"
:
"default-scheduler"
}}
curl -k -v -XPOST -H
"Content-Type: application/json"
-H
"Accept: application/json"
-H
"User-Agent: kubectl/v1.7.5 (linux/amd64) kubernetes/17d7182"
https:
//localhost:6443/api/v1/namespaces/default/pods
POST https:
//localhost:6443/api/v1/namespaces/default/pods 201 Created in 6 milliseconds
Response
Headers
:
Content
-
Type
: application/json
Content
-
Length
:
1208
Date
:
Wed
,
18
Oct
2017
15
:
04
:
17
GMT
Response
Body
: {
"kind"
:
"Pod"
,
"apiVersion"
:
"v1"
,
"metadata"
:{
"name"
:
"ubuntu1"
,
"namespace"
:
"default"
,
"selfLink"
:
"/api/v1/namespaces/default/pods/ubuntu1"
,
"uid"
:
"9c9af581-b415-11e7-8033-024d1ba659e8"
,
"resourceVersion"
:
"486154"
,
"creationTimestamp"
:
"2017-10-18T15:04:17Z"
,
"labels"
:{
"name"
:
"ubuntu1"
}},
"spec"
:{
"volumes"
:[{
"name"
:
"default-token-p0980"
,
"secret"
:{
"secretName"
:
"default-token-p0980"
,
"defaultMode"
:
420
}}],
"containers"
:[{
"name"
:
"ubuntu1"
,
"image"
:
"ubuntu"
,
"command"
:[
"sleep"
,
"1d"
],
"resources"
:{},
"volumeMounts"
:[{
"name"
:
"default-token-p0980"
,
"readOnly"
:
true
,
"mountPath"
:
"/var/run/secrets/kubernetes.io/serviceaccount"
}],
"terminationMessagePath"
:
"/dev/termination-log"
,
"terminationMessagePolicy"
:
"File"
,
"imagePullPolicy"
:
"Always"
}],
"restartPolicy"
:
"Always"
,
"terminationGracePeriodSeconds"
:
30
,
"dnsPolicy"
:
"ClusterFirst"
,
"serviceAccountName"
:
"default"
,
"serviceAccount"
:
"default"
,
"securityContext"
:{},
"schedulerName"
:
"default-scheduler"
,
"tolerations"
:[{
"key"
:
"node.kubernetes.io/not-ready"
,
"operator"
:
"Exists"
,
"effect"
:
"NoExecute"
,
"tolerationSeconds"
:
300
},{
"key"
:
"node.alpha.kubernetes.io/unreachable"
,
"operator"
:
"Exists"
,
"effect"
:
"NoExecute"
,
"tolerationSeconds"
:
300
}]},
"status"
:{
"phase"
:
"Pending"
,
"qosClass"
:
"BestEffort"
}}
複製代碼
從 url path 裏面能夠看到幾個劃分,path 的分類大概有下面這幾種。 ubuntu
路徑上總體分紅 group, version, resource, 做爲核心 API group 的 core(包括 pod, node 之類的 resource),不帶 group,直接接在 /api/ 後面,其餘的 api group 則接在 /apis 後面。以 pod 爲例,pod 對應的數據類型以下,這個數據結構和 POST 請求中的結構的參數是一致的。
後端
若是是 job 的話則是在,pkg/apis/batch/v2alpha1/types.go,和 API 路徑是對應的。例子當中 kubectl 加上 level 大於 8 的 log 就會打印請求和相應的 body,能夠看到 request body 和上面的數據結構是一致的。這個請求會發送到 apiserver 進行處理而且返回存儲以後的 pod。api
2.2.一、Config跨域
父結構,主要的配置內容,其中有一個結構 RESTOptionsGetter genericregistry.RESTOptionsGetter 是和 API 初始化相關的,這個接口的實現是在 k8s.io/apiserver/pkg/server/options/etcd.go 中的 storageFactoryRestOptionsFactory 實現的,對應的實現函數是緩存
func (f *storageFactoryRestOptionsFactory)
GetRESTOptions
(resource schema.
GroupResource
) (
generic
.
RESTOptions
, error) {
storageConfig, err := f.
StorageFactory
.
NewConfig
(resource)
if
err !=
nil
{
return
generic
.
RESTOptions
{}, fmt.
Errorf
(
"unable to find storage destination for %v, due to %v"
, resource, err.
Error
())
}
ret :=
generic
.
RESTOptions
{
StorageConfig
: storageConfig,
Decorator
:
generic
.
UndecoratedStorage
,
DeleteCollectionWorkers
: f.
Options
.
DeleteCollectionWorkers
,
EnableGarbageCollection
: f.
Options
.
EnableGarbageCollection
,
ResourcePrefix
: f.
StorageFactory
.
ResourcePrefix
(resource),
}
if
f.
Options
.
EnableWatchCache
{
sizes, err :=
ParseWatchCacheSizes
(f.
Options
.
WatchCacheSizes
)
if
err !=
nil
{
return
generic
.
RESTOptions
{}, err
}
cacheSize, ok := sizes[resource]
if
!ok {
cacheSize = f.
Options
.
DefaultWatchCacheSize
}
ret.
Decorator
= genericregistry.
StorageWithCacher
(cacheSize)
}
return
ret,
nil
}
2.2.二、APIGroupInfo
複製代碼
2.2.二、APIGroupInfo
APIGroupInfo 主要定義了一個 API 組的相關信息,觀察一下 APIGroupInfo 是如何初始化的。 在 k8s.io/pkg/master/master.go 當中,每一個 Resource 都要提供本身的 Provider,好比說 storagerest 就在 k8s.io/kubernetes/pkg/registry/storage/rest/storage_storage.go 定義了 NewRESTStorage 方法。而默認的 resource 的 legacy provider 單獨處理。
if
c.
ExtraConfig
.
APIResourceConfigSource
.
AnyResourcesForVersionEnabled
(apiv1.
SchemeGroupVersion
) {
legacyRESTStorageProvider := corerest.
LegacyRESTStorageProvider
{
StorageFactory
: c.
ExtraConfig
.
StorageFactory
,
ProxyTransport
: c.
ExtraConfig
.
ProxyTransport
,
KubeletClientConfig
: c.
ExtraConfig
.
KubeletClientConfig
,
EventTTL
: c.
ExtraConfig
.
EventTTL
,
ServiceIPRange
: c.
ExtraConfig
.
ServiceIPRange
,
ServiceNodePortRange
: c.
ExtraConfig
.
ServiceNodePortRange
,
LoopbackClientConfig
: c.
GenericConfig
.
LoopbackClientConfig
,
}
m.
InstallLegacyAPI
(&c, c.
GenericConfig
.
RESTOptionsGetter
, legacyRESTStorageProvider)
}
複製代碼
而後經過調用k8s.io/kubernetes/pkg/registry/core/rest.LegacyRESTStorageProvider 的 NewLegacyRESTStorage 來初始化基礎對象的 apigroup info,好比初始化 podStorage,serviceStorage 和 nodeStorage 等等。legacy ApiGrouInfo 的 Scheme, ParamaterCodec, NegotiatedSerializer 都是用 "k8s.io/kubernetes/pkg/api" 包下的全局變量初始化的。
Scheme
: api.
Scheme
,
ParameterCodec
: api.
ParameterCodec
,
NegotiatedSerializer
: api.
Codecs
,
複製代碼
而後合併成一個 restStorage 存入 apiGroupInfo 中。
restStorageMap := 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
,
...
複製代碼
舉個例子 podStorage 就是用的 genericregistry.Store,這是一個通用的 etc 輔助結構,把 etcd 抽象成存儲結構。
// REST implements a RESTStorage for pods
type REST
struct
{
*genericregistry.
Store
proxyTransport http.
RoundTripper
}
複製代碼
pkg/api.Codecs 是全局默認的 codec 來自下面這段代碼。
func
NewCodecFactory
(scheme *runtime.
Scheme
)
CodecFactory
{
serializers := newSerializersForScheme(scheme, json.
DefaultMetaFactory
)
return
newCodecFactory(scheme, serializers)
}
複製代碼
默認具體定義了這幾種 serilizer。
func newSerializersForScheme(scheme *runtime.
Scheme
, mf json.
MetaFactory
) []serializerType {
jsonSerializer := json.
NewSerializer
(mf, scheme, scheme,
false
)
jsonPrettySerializer := json.
NewSerializer
(mf, scheme, scheme,
true
)
yamlSerializer := json.
NewYAMLSerializer
(mf, scheme, scheme)
...
複製代碼
並且標準庫的 json 有很嚴重的性能問題,換用了 json-iter 可是有不少標準庫不兼容的問題,性能提高了大概 20% 可是沒辦法和進主線,我嘗試在上面工做的了一段時間,改了兩個問題仍是有錯,因爲時間關係,暫時放棄了這個工做,相關的 issue 在這裏:https://github.com/kubernetes/kubernetes/pull/54289
func
DefaultBuildHandlerChain
(apiHandler http.
Handler
, c *
Config
) http.
Handler
{
handler := genericapifilters.
WithAuthorization
(apiHandler, c.
RequestContextMapper
, c.
Authorizer
, c.
Serializer
)
handler = genericfilters.
WithMaxInFlightLimit
(handler, c.
MaxRequestsInFlight
, c.
MaxMutatingRequestsInFlight
, c.
RequestContextMapper
, c.
LongRunningFunc
)
handler = genericapifilters.
WithImpersonation
(handler, c.
RequestContextMapper
, c.
Authorizer
, c.
Serializer
)
if
utilfeature.
DefaultFeatureGate
.
Enabled
(features.
AdvancedAuditing
) {
handler = genericapifilters.
WithAudit
(handler, c.
RequestContextMapper
, c.
AuditBackend
, c.
AuditPolicyChecker
, c.
LongRunningFunc
)
}
else
{
handler = genericapifilters.
WithLegacyAudit
(handler, c.
RequestContextMapper
, c.
LegacyAuditWriter
)
}
failedHandler := genericapifilters.
Unauthorized
(c.
RequestContextMapper
, c.
Serializer
, c.
SupportsBasicAuth
)
if
utilfeature.
DefaultFeatureGate
.
Enabled
(features.
AdvancedAuditing
) {
failedHandler = genericapifilters.
WithFailedAuthenticationAudit
(failedHandler, c.
RequestContextMapper
, c.
AuditBackend
, c.
AuditPolicyChecker
)
}
handler = genericapifilters.
WithAuthentication
(handler, c.
RequestContextMapper
, c.
Authenticator
, failedHandler)
handler = genericfilters.
WithCORS
(handler, c.
CorsAllowedOriginList
,
nil
,
nil
,
nil
,
"true"
)
handler = genericfilters.
WithTimeoutForNonLongRunningRequests
(handler, c.
RequestContextMapper
, c.
LongRunningFunc
, c.
RequestTimeout
)
handler = genericapifilters.
WithRequestInfo
(handler, c.
RequestInfoResolver
, c.
RequestContextMapper
)
handler = apirequest.
WithRequestContext
(handler, c.
RequestContextMapper
)
handler = genericfilters.
WithPanicRecovery
(handler)
return
handler
}
複製代碼
首先經過 ./staging/src/k8s.io/apiserver/pkg/server/config.go 下的 DefaultBuildHandlerChain 構建 filters。
2.4.一、panic recover
genericfilters.WithPanicRecovery 在 handler 的最外層對出現的 panic 恢復,而且打印每次請求的 log,因此你想觀察 API 請求的狀況能夠 grep wrap.go 就能看到。
2.4.二、request context
apirequest.WithRequestContext 給 request 綁定一個 Context
2.4.三、RequestInfo
跟路 url 提取後續請求須要的 group, version, namespace, verb, resource 等信息。
2.4.四、WithTimeoutForNonLongRunningRequests
限制 API 調用時間,超時處理提早終止 write。
2.4.五、WithCORS
容許跨域訪問。
2.4.六、authentication
在 k8s.io/apiserver/pkg/endpoints/filters/authentication.go 下。WithAuthentication 插入鑑權信息,例如證書鑑權,token 鑑權等,而且從鑑權信息當中獲取 user 信息(多是 service account 也多是外部用戶)user 身份是由這 裏面的幾種方式確認的
2.4.七、authorization
檢查是否有權限進行對應資源的操做。一種是 RBAC 一種是 Node。具體這兩種方式能夠看這個介紹:https://kubernetes.io/docs/admin/authorization/,RBAC 主要是針對服務的,而 Node 模式主要是針對 kubelet 的。
2.4.八、impersonation
讓用戶假裝成其餘用戶,好比 admin 能夠用普通用戶的身份建立資源。
經過 genericapiserver 的 InstallLegacyAPIGroup 就註冊到路由當中。具體的作法就是根據 version, resource, sub resource, verb 等信息構造路由,而後用 go-restful 註冊處理函數。好比說 GET
route := ws.GET(action.
Path
).
To
(handler).
Doc
(doc).
Param
(ws.
QueryParameter
(
"pretty"
,
"If 'true', then the output is pretty printed."
)).
Operation
(
"read"
+namespaced+kind+strings.
Title
(subresource)+operationSuffix).
Produces
(append(storageMeta.
ProducesMIMETypes
(action.
Verb
), mediaTypes...)...).
Returns
(http.
StatusOK
,
"OK"
, producedObject).
Writes
(producedObject)
複製代碼
handler 裏面作的內容就是序列化,而後根據具體的要求(GET DELETE 等)到 etcd 中操做,固然自己還有一層緩存,這取決於 API 的 options 是但願更新仍是直接讀緩存(緩存會比 etcd 舊一些),好比對於 kubelet 會不斷查詢 node 信息,可是 kubelet 自己並不須要最新的信息,這個時候就會從緩存中讀取。
開啓代理 kubectl proxy,就能夠經過 localhost 直接訪問 kube-apiserver HTTP 服務。而後執行 go tool pprof http://localhost:8001/debug/pprof/profile 能夠得到 profile 結果,下圖紅色的部分就是調用耗時最多的部分。
除此以外,kube-apiserver 自己也暴露了不少 prometheus 的 metrics 可是往上如今沒有現成的模板,只能根據本身的需求來在 prometheus 看成作 query。能夠在 k8s.io/apiserver/pkg/endpoints/metrics/metrics.go 裏面看到。 以前也說過,超時間調用時會打 log 的,在代碼中保存了一些 trace 日誌,能夠經過 grep Trace來過濾。Trace[%d] 這樣開頭, %d 是一個 id 能夠看到具體的 trace 信息。