Published at 2021-06-01 | Last Update 2021-06-01
本文基于 2019 年的一篇文章 What happens when … Kubernetes edition! 梳理了 k8s 创建 pod(及其 deployment/replicaset)的整个过程, 整理了每个重要步骤的代码调用栈,以在实现层面加深对整个过程的理解。
原文参考的 k8S 代码已经较老(v1.8
/v1.14
以及当时的 master
),且部分代码
链接已失效;本文代码基于 v1.21
。
由于内容已经不与原文一一对应(有增加和删减),因此标题未加 “[译]” 等字样。感谢原作者(们)的精彩文章。
篇幅太长,分成了几部分:
NewKubectlCommand // staging/src/k8s.io/kubectl/pkg/cmd/cmd.go
|-matchVersionConfig = NewMatchVersionFlags()
|-f = cmdutil.NewFactory(matchVersionConfig)
| |-clientGetter = matchVersionConfig
|-NewCmdRun(f) // staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
| |-Complete // staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
| |-Run(f) // staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
| |-validate parameters
| |-generators = GeneratorFn("run")
| |-runObj = createGeneratedObject(generators) // staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
| | |-obj = generator.Generate() // -> staging/src/k8s.io/kubectl/pkg/generate/versioned/run.go
| | | |-get pod params
| | | |-pod = v1.Pod{params}
| | | |-return &pod
| | |-mapper = f.ToRESTMapper() // -> staging/src/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go
| | | |-f.clientGetter.ToRESTMapper() // -> staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go
| | | |-f.Delegate.ToRESTMapper() // -> staging/src/k8s.io/kubectl/pkg/cmd/util/kubectl_match_version.go
| | | |-ToRESTMapper // -> staging/src/k8s.io/cli-runtime/pkg/resource/builder.go
| | | |-delegate() // staging/src/k8s.io/cli-runtime/pkg/resource/builder.go
| | |--actualObj = resource.NewHelper(mapping).XX.Create(obj)
| |-PrintObj(runObj.Object)
|
|-NewCmdEdit(f) // kubectl edit 命令
|-NewCmdScale(f) // kubectl scale 命令
|-NewCmdCordon(f) // kubectl cordon 命令
|-NewCmdUncordon(f)
|-NewCmdDrain(f)
|-NewCmdTaint(f)
|-NewCmdExecute(f)
|-...
敲下 kubectl
命令后,它首先会做一些客户端侧的验证。
如果命令行参数有问题,例如,镜像名为空或格式不对,
这里会直接报错,从而避免了将明显错误的请求发给 kube-apiserver,减轻了后者的压力。
此外,kubectl 还会检查其他一些配置,例如
--dry-run
)所有查询或修改 K8s 资源的操作都需要与 kube-apiserver 交互,后者会进一步和 etcd 通信。
因此,验证通过之后,kubectl 接下来会创建发送给 kube-apiserver 的 HTTP 请求。
创建 HTTP 请求用到了所谓的
generator
(文档)
,它封装了资源的序列化(serialization)操作。
例如,创建 pod 时用到的 generator 是 BasicPod
:
// staging/src/k8s.io/kubectl/pkg/generate/versioned/run.go
type BasicPod struct{}
func (BasicPod) ParamNames() []generate.GeneratorParam {
return []generate.GeneratorParam{
{Name: "labels", Required: false},
{Name: "name", Required: true},
{Name: "image", Required: true},
...
}
}
每个 generator 都实现了一个 Generate()
方法,用于生成一个该资源的运行时对象(runtime object)。
对于 BasicPod
,其实现为:
func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, error) {
pod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{ // metadata 字段
Name: name,
Labels: labels,
...
},
Spec: v1.PodSpec{ // spec 字段
ServiceAccountName: params["serviceaccount"],
Containers: []v1.Container{
{
Name: name,
Image: params["image"]
},
},
},
}
return &pod, nil
}
有了 runtime object 之后,kubectl 需要用合适的 API 将请求发送给 kube-apiserver。
K8s 用 API group 来管理 resource API。 这是一种不同于 monolithic API(所有 API 扁平化)的 API 管理方式。
具体来说,同一资源的不同版本的 API,会放到一个 group 里面。
例如 Deployment 资源的 API group 名为 apps
,最新的版本是 v1
。这也是为什么
我们在创建 Deployment 时,需要在 yaml 中指定 apiVersion: apps/v1
的原因。
生成 runtime object 之后,kubectl 就开始 搜索合适的 API group 和版本:
// staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
obj := generator.Generate(params) // 创建运行时对象
mapper := f.ToRESTMapper() // 寻找适合这个资源(对象)的 API group
然后创建一个正确版本的客户端(versioned client),
// staging/src/k8s.io/kubectl/pkg/cmd/run/run.go
gvks, _ := scheme.Scheme.ObjectKinds(obj)
mapping := mapper.RESTMapping(gvks[0].GroupKind(), gvks[0].Version)
这个客户端能感知资源的 REST 语义。
以上过程称为版本协商。在实现上,kubectl 会
扫描 kube-apiserver 的 /apis
路径
(OpenAPI 格式的 schema 文档),获取所有的 API groups。
出于性能考虑,kubectl 会
缓存这份 OpenAPI schema,
路径是 ~/.kube/cache/discovery
。想查看这个 API discovery 过程,可以删除这个文件,
然后随便执行一条 kubectl 命令,并指定足够大的日志级别(例如 kubectl get ds -v 10
)。
现在有了 runtime object,也找到了正确的 API,因此接下来就是 将请求真正发送出去:
// staging/src/k8s.io/kubectl/pkg/cmd/cmd.go
actualObj = resource.
NewHelper(client, mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager).
Create(o.Namespace, false, obj)
发送成功后,会以恰当的格式打印返回的消息。
前面其实有意漏掉了一步:客户端认证。它发生在发送 HTTP 请求之前。
用户凭证(credentials)一般都放在 kubeconfig 文件中,但这个文件可以位于多个位置, 优先级从高到低:
--kubeconfig <file>
$KUBECONFIG
~/.kube
。这个文件中存储了集群、用户认证等信息,如下面所示:
apiVersion: v1
clusters:
- cluster:
certificate-authority: /etc/kubernetes/pki/ca.crt
server: https://192.168.2.100:443
name: k8s-cluster-1
contexts:
- context:
cluster: k8s-cluster-1
user: default-user
name: default-context
current-context: default-context
kind: Config
preferences: {}
users:
- name: default-user
user:
client-certificate: /etc/kubernetes/pki/admin.crt
client-key: /etc/kubernetes/pki/admin.key
有了这些信息之后,客户端就可以组装 HTTP 请求的认证头了。支持的认证方式有几种: