kube-controller-manager
所有的控制器都由kube-controller-manager这个组件管理,在kubernetes项目的pkg/controller目录下可以查看所有的控制器:
BUILD deployment podautoscaler
OWNERS disruption podgc
apis doc.go replicaset
bootstrap endpoint replication
certificates endpointslice resourcequota
client_builder_dynamic.go endpointslicemirroring serviceaccount
clusterroleaggregation garbagecollector statefulset
controller_ref_manager.go history storageversiongc
controller_ref_manager_test.go job testutil
controller_utils.go lookup_cache.go ttl
controller_utils_test.go namespace ttlafterfinished
cronjob nodeipam util
daemon nodelifecycle volume
这些控制器之所以被统一放在pkg/controller目录下,就是因为它们被遵循Kubernetes项目中的一个通用编排模式,即:控制循环(control loop)。
for {
实际状态 := 获取集群中对象x的实际状态(Actual State)
期望状态 := 获取集群中对象x的期望状态(Desired State)
if 实际状态 == 期望状态 { // 第三步,执行对比,也被称之为Reconcile Loop(调谐循环)
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}
1. Deployment/ReplicaSet
接下来,以 Deployment 为例,我和你简单描述一下它对控制器模型的实现:
- Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
- Deployment 对象的 Replicas 字段的值就是期望状态;
- Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod。
|
|
类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。 这就是为什么,在所有 API 对象的 Metadata 里,都有一个字段叫作 ownerReference,用于保存当前这个 API 对象的拥有者(Owner)的信息。 对于Deployment管理的Pod来说,它的ownerReference其实是ReplicaSet。 Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。
Pod的"水平扩展/收缩"(horizontal scaling out/in) 举个例子,如果你更新了 Deployment 的 Pod 模板(比如,修改了容器的镜像),那么 Deployment 就需要遵循一种叫作“滚动更新”(rolling update)的方式,来升级现有的容器。 而这个能力的实现,依赖的是 Kubernetes 项目中的一个非常重要的概念(API 对象):ReplicaSet。 ReplicaSet 的结构非常简单,我们可以通过这个 YAML 文件查看一下:
其实,Deployment、ReplicaSet和Pod之间,是层层控制的关系。其中,ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。 这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。 而Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。
- Deployment通过修改它所控制的ReplicaSet的Pod副本个数来实现"水平扩展/收缩" 可以通过直接修改Deployment的yaml,或者使用下面的命令实现:
|
|
- 滚动更新就相对要复杂些 接下来,我们以下面这个yaml来演示"滚动更新"的过程
|
|
创建nginx-deployment,并使用–record参数来记录每次操作所执行的命令(会在metadata.annotations的kubernetes.io/change-cause中记录下每次的操作):
在返回结果中,我们可以看到三个状态字段,它们的含义如下所示。
- READY: 通过健康检查的Pod的个数,可能包含就版本的Pod。
- UP-TO-DATE: 当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;
- AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数。
- CURRENT: 当前处于 Running 状态的 Pod 的个数;
可以使用kubectl rollout statu命令来实时查看Deployment对象的状态变化: $ kubectl rollout status deployment/nginx-deployment
查看该Deployment所控制的ReplicaSet:
可以看到,在用户提交了一个Deployment对象后,Deployment Controller就会立即创建一个ReplicaSet,这个ReplicaSet的名字,有Deployment的名字和一个随机字符串组成。 将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。Deployment每执行一次滚动更新,都会新建一个新的ReplicaSet,新的ReplicaSet就绪后才会删除旧的ReplicaSet。 可以通过spec.strategy字段来控制滚动更新的策略,例如:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
...
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
maxSurge 指定的是除了 DESIRED 数量之外,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod。值还可以是百分比。 maxUnavailable 指的是,在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。值还可以是百分比。
可以通过直接修改Deployment的yaml,或者使用下面的命令实现滚动更新: $ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
可以使用下面的命令把整个Deployment回滚到上一个版本: $ kubectl rollout undo deployment/nginx-deployment
进一步地,如果我想回滚到更早之前的版本,要怎么办呢? 首先,我需要使用 kubectl rollout history 命令,查看该Deployment 变更的所有历史版本。 如果在创建Deployment的时候指定了–record选项,就不需要再指定–revision=<版本号>来查看具体的变更细节了。 $ kubectl rollout history deployment/nginx-deployment 然后,就可以使用–to-revision=<版本号>来执行回滚了。 $ kubectl rollout history deployment/nginx-deployment –to-revision=2
如何实现对Deployment的多次更新操作,最后只生成一个ReplicaSet呢? 首先,使用如下的命令,暂停该Deployment: $ kubectl rollout pause deployment/nginx-deployment 在对Deployment的所有修改都完成之后,再恢复该Deployment: $ kubectl rollout resume deploy/nginx-deployment
可以通过spec.revisionHistoryLimit字段来设置Deployment能保留的最大ReplicaSet的个数。
2. StatefulSet
如果实例之间存在依赖关系,比如:主从关系、主备关系,这个时候就无法再使用Deployment了。
k8s是第一个解决有状态应用(比如数据库)这个编排难题的,它使用了StatefulSet这个API对象,它把真实世界里的应用状态,抽象为两种情况:
- 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。 拓扑状态是通过Headless Service来维护的。
- 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。 存储状态是通过PVC(Persistent Volume Claim)来维护的。
所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。
StatefulSet会使用到HeadlessService,而HeadlessService会给它所代理的Pod的IP地址绑定如下格式的DNS记录:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?
|
|
从上面的StatefulSet的yaml中可以看出,它和Deployment的区别是,多了一个serviceName=nginx的字段。 这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用名为 nginx的 这个 Headless Service 来保证 Pod 的“可解析身份”。所以在创建StatefulSet之前,需要先创建Headless Service
执行如下操作:
这时候,如果你手比较快的话,还可以通过 kubectl 的 -w 参数,即:Watch 功能,实时查看 StatefulSet 创建两个有状态实例的过程:
通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:-。 而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。
更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。
当这两个 Pod 都进入了 Running 状态之后,你就可以使用kubectl exec命令来查看到它们各自唯一的“网络身份”了。
接下来,我们再试着以 DNS 的方式,访问一下这个 Headless Service:
$ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.1.7
$ nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.2.7
从 nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是 web-1 的 IP 地址。
这时候,如果你在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉: 然后,再在当前 Terminal 里 Watch 一下这两个 Pod 的状态变化,就会发现一个有趣的现象:
可以看到,当我们把这两个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。
通过这种严格的对应规则(而不是使用hash算法生成一个字符串来标识),StatefulSet 就保证了 Pod 网络标识的稳定性。
所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service 的话,你就会发现原来的域名还能用:
|
|
不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。
为了维护存储状态,需要用到PVC,所以将上面的StatefulSet的yaml改为下面这个样子:
|
|
注意,不能单独的创建PVC之后,在挂载在StatefulSet中。而是使用StatefulSet的volumeClaimTemplates字段来维护PVC。
这样,凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段,而且使用与这个 Pod 完全一致的编号(<PVC名字>-<StatefulSet名字>-<编号>
)。
所以在部署StatefulSet之前,需要先让运维人员创建好PV。除非你的 Kubernetes 集群运行在公有云上,这样 Kubernetes 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的 PV。
只更新序号大于或者等于2的Pod:
|
|
3. DaemonSet
DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:
- 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
- 每个节点上只有一个这样的 Pod 实例;
- 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。
这个机制听起来很简单,但 Daemon Pod 的意义确实是非常重要的。我随便给你列举几个例子:
- 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
- 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录;
- 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。
更重要的是,跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早。
这个乍一听起来可能有点儿奇怪。但其实你来想一下:如果这个 DaemonSet 正是一个网络插件的 Agent 组件呢?这个时候,整个 Kubernetes 集群里还没有可用的容器网络,所有 Worker 节点的状态都是 NotReady(NetworkReady=false)。这种情况下,普通的 Pod 肯定不能运行在这个集群上。
而DaemonSet 的“过人之处”,其实就是依靠 Toleration 实现的。
这样,哪怕这个Worker节点的状态是NotReady,也可以部署普通的Pod。
|
|
这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod。这个镜像的功能非常实用:通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch 中。
Docker 容器里应用的日志(这里只限stdout和stderr),默认会保存在宿主机的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件里,所以这个目录正是 fluentd 的搜集目标。
如果Docker容器将日志放在某个文件中,上面的方案就不可用了, 这个时候就需要使用sidecar把日志文件的数据重新打印到stdout和stderr上才行。
那么,DaemonSet 又是如何保证每个 Node 上有且只有一个被管理的 Pod 呢? DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。而检查的结果,可能有这么三种情况:
- 没有这种 Pod,那么就意味着要在这个 Node 上创建这样一个 Pod;
- 有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉;
- 正好只有一个这种 Pod,那说明这个节点是正常的。
其中,删除节点(Node)上多余的 Pod 非常简单,直接调用 Kubernetes API 就可以了。但是,如何在指定的 Node 上创建新 Pod 呢? 可以使用nodeSelector,但是建议使用nodeAffinity,因为nodeSelector将来会被nodeAffinity所取代。
其中:
- requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity;
- 这个 Pod,将来只允许运行在“metadata.name”是“node-geektime”的节点上。
Kubernetes v1.7 之后添加了一个 API 对象,名叫 ControllerRevision,专门用来记录某种 Controller 对象的版本。比如,你可以通过如下命令查看 fluentd-elasticsearch 对应的 ControllerRevision:
4. job和cronjob
本文发表于 0001-01-01,最后修改于 0001-01-01。
本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。