pod是k8s的最小调度单元
- 容器是进程,pod是进程组,而k8s就是分布式的systemd
有多个进程必须调度在同一台机器的场景,而pod就是把多个进程封装起来作为一个原子调度单位。
在一个真正的操作系统里,进程并不是"孤苦伶仃"地独立运行的,而是以进程组的方式,“有原则地"组织在一起。
类似的,在k8s中,pod相当于进程组,docker容器相当于进程,进程组中的进程间是公用同一个文件系统和网络的,那k8s是如何实现多个docker容器共享同一个文件系统和网络呢?
假设有A、B两个容器,可以先运行B容器,然后使用命令docker run --net=B --volumes-from=B --name=A image-A ...运行A容器,这样A就会共享B的文件系统和网络了。
但是,这有个问题,就是A必须在B之后执行,如果解决这个问题呢?
所以,在Kubernetes项目里,Pod的实现需要使用一个中间容器,这个容器叫作Infra容器。在这个Pod中,Infra容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过Join Network Namespace的方式,与Infra容器关联在一起。而对于同一个Pod里面的所有用户容器来说,它们的进出流量,也可以认为都是通过Infra容器完成的。所以网络插件其实是和infra容器打交道的,由于infra容器的rootfs几乎什么都没有,所以网络插件无法在infra容器中存放任何的配置,只能关注于Infra容器的Network Namespace。
infra容器的镜像是k8s.gcr.io/pause,这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有100~200KB。
- pod的常用字段
https://github.com/kubernetes/api/blob/master/core/v1/types.go 下的Pod结构体中包含了所有Pod字段。
- shareProcessNamespace pod内的所有容器间共享PID命名空间(默认是不共享的)
由于busybox容器开启了stdin和tty,我们就可以使用kubectl attach命令就可以直接进入busybox容器,而不用重复使用-t -i选项了。
可以看到,在这个容器里,我们不仅可以看到它本身的ps ax指令,还可以看到nginx容器的进程,以及Infra容器的/pause进程。这就意味着,整个Pod里的每个容器的进程,对于所有容器来说都是可见的
- pod内共享宿主机的Namespace
- 对于需要预先执行的操作,可以在pod中的initContainers中定义
|
|
在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
- ImagePullPolicy
- Always(默认): 每次创建Pod都重新拉取一次镜像。
- Never: 永远不会主动拉取这个镜像
- IfNotPresent: 只在宿主机上不存在这个镜像时才拉取
- lifecycle(Container Lifecycle Hooks)
|
|
- postStart: 在容器启动后,立刻执行一个指定的操作。需要明确的是,postStart定义的操作,虽然是在Docker容器ENTRYPOINT执行之后,但它并不严格保证顺序(非阻塞的)。也就是说,在postStart启动时,ENTRYPOINT有可能还没有结束。
- preStop: 容器被杀死之前(比如,收到了SIGKILL信号)。而需要明确的是,preStop操作的执行,是阻塞的。所以,它会阻塞当前容器杀死进程,直到这个Hook定义操作完成之后,才允许容器被杀死,这跟postStart不一样。
- Pod对象在Kubernetes中的生命周期
- Pending: 这个状态意味着,Pod的YAML文件已经提交给了Kubernetes,API对象已经被创建并保存在Etcd当中。但是,这个Pod里有些容器因为某些原因而不能被顺利创建。比如,调度不成功。
- Running: 这个状态下,Pod已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
- Succeeded: 这个状态意味着,Pod里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
- Failed: 这个状态下,Pod里至少有一个容器以不正常的状态(非0的返回码)退出。这个状态的出现,意味着你得想办法Debug这个容器得应用,比如查看Pod的Events和日志。
- Unknown: 这是一个异常状态,意味着Pod的状态不能持续地被kubelet汇报给kube-apiserver,这很有可能是主从节点(Master和kubelet)间的通信出现了问题。
Pod对象的Status字段,还可以再细化出一组Conditions。这些细分状态的值包括:PodScheduled、Ready、Initialized,以及Unschedulable。它们主要用于描述造成当前Status的具体原因是什么。比如,Pod当前的Status是Pending,对应的Condition是Unschedulable,这就意味着它的调度出现了问题。
- 探针和restartPolicy
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在这个Pod中,我们定义了一个有趣的容器。它在启动之后做的第一件事,就是在/tmp目录下创建了一个healthy文件,以此作为自己已经正常运行的标志。而30s过后,它会把这个文件删除掉。
于此同时,我们定义了一个这样的livenessProbe(健康检查)。它的类型是exec,这意味着,它会在容器启动后,在容器里面执行一条我们指定的命令,比如:“cat /tmp/healthy”。
这时,如果这个文件存在,这条命令的返回值就是0,Pod就会认为这个容器不仅已经启动,而且是健康的。这个健康检查,在容器启动5s后开始执行(initialDelaySeconds:5),每5s执行一次(periodSeconds: 5)。
现在,让我们来具体实践一下这个过程。
首先,创建这个Pod:
$ kubectl create -f test-liveness-exec.yaml
然后,查看这个Pod的状态:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 0 10s
可以看到,由于已经通过了健康检查,这个Pod就进入了Running状态。
而30s之后,我们再查看一下Pod的Events:
$ kubectl describe pod test-liveness-exec
你会发现,这个Pod在Events报告了一个异常:
FirstSeen LastSeen Count From SubobjectPath Type
--------- -------- ----- ---- ------------- -----
2s 2s 1 {kubelet worker0} spec.containers{liveness} Warning
显然,这个健康检查探查到/tmp/healthy已经不存在了,所以它报告容器是不健康的。那么接下来会发生什么呢?
我们不妨再次查看一下这个Pod的状态:
$ kubectl get pod test-livenes-exec
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 1m
这时我们发现,Pod并没有进入Failed状态,而是保持了Running状态。这是为什么呢?
其实,如果你注意到RESTARTS字段从0到1的变化,就明白原因了:这个异常的容器已经被Kubernetes重启了 。在这个过程中,Pod保持Running状态不变。
需要注意的是:Kubernetes中并没有Docker的Stop语义。所以虽然是Restart(重启),但实际却是重新创建了容器。
这个功能就是Kubernetes里的Pod恢复机制,叫作restartPolicy。它是Pod的Spec部分的一个标准字段(pod.spec.restartPolicy)。
- Always(默认):任何时候这个容器发生了异常,它一定会被重新创建
- OnFailure: 只在容器异常时才自动重启容器;
- Never: 从来不重启容器
pod的重启遵循两个基本的设计原则:
- 只要Pod的restartPolicy指定的策略允许重启异常的容器(比如:Always),那么这个Pod就会保持Running状态,并进行容器重启。否则,Pod就会进入Failed状态。
- 对于包含多个容器的Pod,只要它里面所有的容器都计入异常状态后,Pod才会进入Failed状态。在此之前,Pod都是Running状态。此时,Pod的READY字段会显示正常容器的个数。
假设一个Pod里只有一个容器,然后这个容器异常退出了。那么,只有当restartPolicy=Never时,这个Pod才会进入Failed状态。而其他情况下,由于Kubernetes都可以重启这个机器,所以Pod的状态保持Running不变。
而如果这个Pod有多个容器,仅有一个容器异常退出,它就始终保持Running状态,哪怕即使restartPolicy=Never。只有当所有容器也异常退出之后,这个Pod才会进入Failed状态。
- readniessProbe探针(探测该Pod能否被Service访问到)
readinessProbe检查结果的成功与否,决定的这个Pod是不是能被通过Service的方式访问到,而并不影响Pod的生命周期。
- NodeName 一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调度,调度的结果就是赋值的节点名字。所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到。
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. 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:
3. job和cronjob
本文发表于 0001-01-01,最后修改于 0001-01-01。
本站永久域名「 jiavvc.top 」,也可搜索「 极客油画 」找到我。

