Chimission's Notes

聊一聊 输入 kubectl apply 时,k8s在背后做了什么

2022-07-04

介绍了一下k8s是如何通过命令行启动pod :-) 这是一篇旧文,迁移到此

如果我们想在kubernetes集群中创建3个相同nginx镜像的pod,官方推荐的做法是定义一个更高一级抽象的deployment, 通过这个deployment来统一管理一组pod,并且使用声明式命令apply代替 指令式命令create来创建deployment对象 配置文件deployment.yml如下, 来自kubernetes官网

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:

- name: nginx
  image: nginx:1.14.2
  ports:
- containerPort: 80

shell 中我们敲下 kubectl apply -f deployment.yml 后,k8s 就会为我们在 etcd 中生成deploymentpod 对象并将 pod 通过一系列的算法自动调度到集群节点上启动,这一切都是 k8s 自动帮我们完成的,我们只需要提供一份配置文件即可。k8s 在背后究竟是怎么做到的呢,让我们一探究竟。本篇文章省略了很多技术细节,一方面这些展开讲述细节需要大量的篇幅,另一方面作者也没有完全掌握这些细节,所以我们会放在后续的系列文章中继续探讨。

构造请求

k8s 集群使用 REST API进行通信, 请求会以HTTP的方式发给 kube-apiserver 进行处理。当我们是在shell 中输入 kubectl apply -f deployment.yml时,本地的 kubectl 会解析 yaml 中的配置根据其内容构造相应对象的 HTTP 请求参数。首先 kubectl会检查有没有语法错误(比如创建不支持的资源或使用格式错误的镜像名称),出现错误后会直接返回不会发送到 kube-apiserver 以节省网络负载。通过检查后 kubectl 就会构造出 HTTP 请求发送给 kube-apiserver

2 认证,鉴权,准入控制

现在请求已经发送给了 kube-apiserverkube-apiserver 接下来会判断这个请求的发起者是否合法,即请求发起者对应的用户信息是否存储 k8s 集群中,此过程称为认证 Authenticationk8s 提供了多种认证方式,这里我们不做过多的讨论,如果认证没有通过则会直接返回失败的错误信息,通过了就会进入一步 鉴权

虽然我们的身份已经得到了 k8s 的认可,但是身份 identity 和许可 permission 并不是一个概念,就像mysql账号有的有读写权限而有的只有读取权限一样。此时 kube-apiserver 会检查用户的权限是否可以进行相应的操作,对应我们文章中的命令就是创建 deployment 的权限,这里 k8s 也提供了多种方式进行鉴权不再赘述。

好了 kube-apiserver 确认请求发起者有相应的权限,这样就可以执行创建 deployment 的动作了吗。 很“不幸”还有最后一步, 准入控制Kubernetes 准入控制器是控制和强制使用集群的一种插件。我们可以把它看作是拦截(已认证)API 请求的拦截器,它可以更改请求对象,甚至完全拒绝请求。这是可以配置的插件,也就是说你通过这套机制自己开发一套插件部署在集群中来控制请求的行为。k8s官方提供了很多“内置”的准入控制器

3 etcd

终于我们的请求被验证通过,kube-apiserver 会在 etcd (服务发现的后端,存储了集群的状态及其配置)中创建我们的Deployment对象, 创建过程中出现的任何错误都会被捕获,最后 kube-apiserver 会构造 HTTP 响应返回给客户端,我们在输入完命下回车之后看到的信息就是 kubectl 得到 HTTP 响应解析后的信息。注意此时我们部署的 Deployment 对象现在虽然保存在于 etcd 中,但是它还没有被部署到真正的 Node 上。

4 控制循环 (Control loops)

接下来的步骤对于请求调用者来说都是异步执行的,因为请求的响应已经在上一步得到了。

我们已经创建了Deployment,但是并没有创建涉及 Deployment 所依赖的资源拓扑(此例子中就是ReplicaSetPod )这其实是k8s通过内置控制器 Controller自动帮我们创建的。

Controller 是一个用于将系统状态从当前状态调谐到期望状态的异步脚本。所有内置的 Controller 都通过组件 kube-controller-manager 并行运行,每种 Controller 都负责一种具体的控制流程。

比如我们本次使用到的 Deployment Controller

k8setcd中新创建了一个Deployment对象, Deployment Controller会监听( ListAndWatch )到这个事件之后然后检查Deployment这个对象的期望状态,和实际状态作对比,比如这次检查到相关联的对象ReplicaSet(因为本质上 Deployment 是通过控制 ReplicaSet 来控制 Pod 的)没有被创建,Deployment Controller 就会创建关联的 ReplicaSet ,创建 ReplicaSet 之后 Deployment Controller 的并不会检查对应管理的Pod,这是ReplicaSet Controller的工作。

ReplicaSet ControllerDeployment Controller 工作类似,ReplicaSet Controller 监听的是ReplicaSet 这个对象, 当ReplicaSet 被创建时就会检查这个 ReplicaSet 对象对应的期望状态,创建 Pod 对象。

这里也可以看出Deployment并不是直接管理Pod,而是通过 ReplicaSet,即 Deployment 管理ReplicaSetReplicaSet管理Pod

实际上 Control loops 的细节有很多,包括 实现监听的Informer机制,内部工作队列 WorkQueue, 本地缓存等等,如果全部展开如要大量的篇幅,而且作者也并没有完全掌握内部细节,我会在后续系列文章再次总结。

而此时我们也只是在 etcd 中创建了 Deployment,ReplicaSetPod 这3个对象,还没有在实际 Node 中部署。

5 调度 (Scheduler)

接下来到了调度环节。

当所有的 Controller 正常运行后,etcd 中就会保存一个 Deployment、一个 ReplicaSet 和 三个 Pod, 并且可以通过 kube-apiserver 查看到。这时如果你在shellget pod查看刚才的pod状态 你会看到Pending状态(调度中,即它们还没有被调度到集群中合适的 Node 上)。

k8s是依靠Scheduler这个组件完成调度操作的。Scheduler 组件运行在集群控制平面上,工作方式与其他 Controller 相同:监听事件并调谐状态。具体来说, Scheduler 的作用是过滤 PodSpecNodeName 字段为空的 Pod 并尝试将其调度到合适的节点。Scheduler 会经过一系列的比如资源限制(cpu,内存)等算法首先选出一批符合条件的 Node, 然后通过第二轮算法(列如负载均衡情况)给 Node 打分,将 Pod 调度最高分的 Node 上,调度器就会将Pod对象的nodeName字段的值,修改为上述Node的名字。度器对一个 Pod 调度成功,实际上就是将它的 spec.nodeName 字段填上调度结果的节点名字。

不可避免的,这里也包含了很多细节,我们也会在后续文章中详细讨论。

6 Kubelet

终于到了激动人心的真正的容器启动环节。

我们来总结一下已经完成的任务:

  1. HTTP 请求通过了认证、授权和准入控制阶段;
  2. 一个 DeploymentReplicaSet 和三个 Pod 被持久化到 etcd
  3. 最后每个 Pod 都被调度到合适的节点。

到目前为止,所有的工作仅仅只是针对保存在 etcd 中的资源对象,接下来的步骤涉及到在工作节点之间运行具体的容器,这是分布式系统 Kubernetes 的关键因素。这些事情都是由 Kubelet 完成的。

Kubernetes 集群中,每个 Node 节点上都会启动一个 Kubelet 服务进程,该进程用于处理 Scheduler 下发到本节点的 Pod 并管理其生命周期。这意味着它将处理 PodContainer Runtime 之间所有的转换逻辑,包括挂载卷、容器日志、垃圾回收等操作。

我们可以把 Kubelet 当成一种特殊的 Controller,它每隔 20 秒(可以自定义)向 kube-apiserver 查询 Pod,过滤 NodeName 与自身所在节点匹配的 Pod 列表。

当检测到新的 pod 对象还没有在 Node 上创建时,Kubelet进行一些前置操作,然后通过 CRI(Container Runtime Interface) 创建 pause 容器,通过 CNI (Container Network Interface)Pod 设置网络,最后通过CRI拉取我们文件定义中的 nginx 镜像,创建并启动起来!

总结


最终我们的集群上会运行三个容器,这三个容器可能分布在不同的Node上,而这一切只需要我们编写一份文章开头的yml配置文件,Amazing!