Pod是Kubernetes进行管理的最小单元,程序要运行必须部署在容器中,而容器必须存在于Pod中
Pod可以认为是容器的封装,一个Pod中可以存在一个或者多个容器
1)Pod=进程组
在Kubernetes里面,Pod实际上正是Kubernetes抽象出来的一个可以类比为进程组的概念
由四个进程共同组成的一个应用Helloworld,在Kubernetes里面,实际上会被定义为一个拥有四个容器的Pod
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在Kubernetes里面并不会把它们放到一个容器里,Kubernetes会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个Pod里面
所以当Kubernetes把Helloworld给拉起来的时候,实际上会看到四个容器,它们共享了某些资源,这些资源都属于Pod,所以我们说Pod在Kubernetes里面只是一个逻辑单位,没有一个真实的东西对应说这个就是Pod。真正起来在物理上存在的东西,就是四个容器。这四个容器或者说是多个容器的组合就叫做Pod
Pod是Kubernetes分配资源的一个单位,因为里面的容器要共享某些资源,所以Pod也是Kubernetes的原子调度单位
2)为什么Pod必须是原子调度单位?
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个Pod里面。具体来说,第一个容器叫做App,就是业务容器,它会写日志文件;第二个容器叫做LogCollector,它会把刚刚App容器写的日志文件转发到后端的ElasticSearch中
两个容器的资源需求是这样的:App容器需要1G内存,LogCollector需要0.5G内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G内存、Node_B:2G内存
假如说现在没有Pod概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把App调度到了Node_A上面,接下来会怎么样呢?这时会发现:LogCollector实际上是没办法调度到Node_A上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度
在Kubernetes里,就直接通过Pod这样一个概念去解决了。因为在Kubernetes里,这样的一个App容器和LogCollector容器一定是属于一个Pod的,它们在调度时必然是以一个Pod为单位进行调度,所以这个问题是根本不存在的
3)Pod里面的容器是超亲密关系
Pod里面的容器是超亲密关系,大概分为以下几类:
4)Infra container(也叫Pause容器)
每个Pod中都可以包含一个或者多个容器,这些容器可以分为两类:
业务容器(用户程序所在的容器):数量可多可少
Infra container:每个Pod都会有的一个根容器
共享网络:
如上图所示,这个Pod里有两个用户容器A和B,还有一个Infra container。Infra container是一个非常小的镜像,大概100~200KB左右,是一个汇编语言写的、永远处于暂停状态的容器
整个Pod里Infra container第一个启动,在Infra container Hold住Network Namespace后,用户容器就可以加入到Infra container的Network Namespace当中了
所以说一个Pod里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于Pod第一次创建的这个Infra container。这就是Pod解决网络共享的一个解法
这也就意味着,对于Pod里的容器A和容器B来说:
而对于同一个Pod里面的所有用户容器来说,它们的进出流量,也可以认为都是通过Infra container完成的
共享存储:
有了Infra container这个设计之后,共享Volume就简单多了:Kubernetes只要把所有Volume的定义都设计在Pod层级即可
这样,一个Volume对应的宿主机目录对于Pod来说就只有一个,Pod里的容器只要声明挂载这个Volume,就一定可以共享这个Volume对应的宿主机目录。比如下面这个例子:
apiVersion: v1
kind: Pod
metadata:
name: two-containers
spec:
restartPolicy: Never
volumes:
- name: shared-data
hostPath:
path: /data
containers:
- name: nginx-container
image: nginx
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: debian-container
image: debian
volumeMounts:
- name: shared-data
mountPath: /pod-data
command: ["/bin/sh"]
args: ["-c", "echo Hello from the debian container > /pod-data/index.html"]
在这个例子中,debian-container和nginx-container都声明挂载了shared-data这个Volume。而shared-data是hostPath类型。所以,它对应在宿主机上的目录就是:/data
。而这个目录,其实就被同时绑定挂载进了上述两个容器当中
这就是nginx-container可以从它的/usr/share/nginx/html
目录中,读取到debian-container生成的index.html文件的原因
Infra container的作用:
5)Pod生命周期
我们一般将Pod对象从创建至终的这段时间范围称为Pod的生命周期,它主要包含下面的过程:
6)容器探测
Kubernetes提供了三种探针来实现容器探测,分别是:
启动探针案例:
ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 10
startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30
periodSeconds: 10
有了启动探针后,应用将会有最多5分钟(30 * 10 = 300s)的时间来完成其启动过程。一旦启动探测成功一次,存活探测任务就会接管对容器的探测,对容器死锁作出快速响应。如果启动探测一直没有成功,容器会在300秒后被杀死,并且根据restartPolicy来执行进一步处理
相关资料:
1)ReplicaSet
ReplicaSet的主要作用是保证一定数量的Pod正常运行,它会持续监听这些Pod的运行状态,一旦Pod发生故障,就会重启或重建。同时它还支持对Pod数量的扩缩容和镜像版本的升降级
ReplicaSet的资源清单文件:
apiVersion: apps/v1 # 版本号
kind: ReplicaSet # 类型
metadata: # 元数据
name: # rs名称
namespace: # 所属命名空间
labels: # 标签
controller: rs
spec: # 详情描述
replicas: 3 # 副本数量
selector: # 选择器,通过它指定该控制器管理哪些pod
matchLabels: # Labels匹配规则
app: nginx-pod
matchExpressions: # Expressions匹配规则
- {key: app, operator: In, values: [nginx-pod]}
template: # 模板,当副本数量不足时,会根据下面的模板创建pod副本
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
在这里面,需要新了解的配置项就是spec下面几个选项:
replicas:指定副本数量,其实就是当前RS创建出来的Pod的数量,默认为1
selector:选择器,它的作用是建立Pod控制器和Pod之间的关联关系,采用的Label Selector机制,在Pod模板上定义Label,在控制器上定义选择器,就可以表明当前控制器能管理哪些Pod了
template:模板,就是当前控制器创建Pod所使用的模板,就是Pod的定义
2)Deployment
Deployment不直接管理Pod,而是通过管理ReplicaSet来间接管理Pod,即:Deployment管理ReplicaSet,ReplicaSet管理Pod。所以Deployment比ReplicaSet功能更加强大
Deployment主要功能有下面几个:
Deployment的资源清单文件:
apiVersion: apps/v1 # 版本号
kind: Deployment # 类型
metadata: # 元数据
name: # rs名称
namespace: # 所属命名空间
labels: # 标签
controller: deploy
spec: # 详情描述
replicas: 3 # 副本数量
revisionHistoryLimit: 3 # 保留历史版本
paused: false # 暂停部署,默认是false
progressDeadlineSeconds: 600 # 部署超时时间(s),默认是600
strategy: # 策略
type: RollingUpdate # 滚动更新策略
rollingUpdate: # 滚动更新
maxSurge: 30% # 最大额外可以存在的副本数,可以为百分比,也可以为整数
maxUnavailable: 30% # 最大不可用状态的Pod的最大值,可以为百分比,也可以为整数
selector: # 选择器,通过它指定该控制器管理哪些pod
matchLabels: # Labels匹配规则
app: nginx-pod
matchExpressions: # Expressions匹配规则
- {key: app, operator: In, values: [nginx-pod]}
template: # 模板,当副本数量不足时,会根据下面的模板创建pod副本
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
3)控制器模型
Kubernetes所有的控制器都遵循一个通用编排模式,即:控制循环(control loop),这里有一段Go语言风格的伪代码,描述这个控制循环:
for {
实际状态 := 获取集群中对象X的实际状态(Actual State)
期望状态 := 获取集群中对象X的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}
在具体实现中,实际状态往往来自于Kubernetes集群本身。比如,kubelet通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据,或者控制器主动收集的它自己感兴趣的信息,这些都是常见实际状态的来源
而期望状态一般来自于用户提交的YAML文件。比如,Deployment对象中Replicas字段的值,这些信息往往都保存在etcd中
以Deployment为例,描述下它对控制器模型的实现:
app: nginx
标签的Pod,然后统计它们的数量,这就是实际状态类似Deployment这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的
控制器对象本身,负责定义被管理对象的期望状态。比如,Deployment里的replicas=2这个字段
而被控制对象的定义,则来自于一个模板。比如,Deployment里的template字段。可以看到,Deployment这个template字段里的内容,跟一个标准的Pod对象的API定义丝毫不差。而所有被这个Deployment管理的Pod实例,其实都是根据这个template字段的内容创建出来的
DaemonSet类型的控制器可以保证在集群中的每一台(或指定)节点上都运行一个副本。一般适用于日志收集、节点监控等场景。也就是说,如果一个Pod提供的功能是节点级别的(每个节点都需要且只需要一个),那么这类Pod就适合使用DaemonSet类型的控制器创建
DaemonSet创建的Pod有如下三个特征:
DaemonSet的资源清单文件:
apiVersion: apps/v1 # 版本号
kind: DaemonSet # 类型
metadata: # 元数据
name: # rs名称
namespace: # 所属命名空间
labels: # 标签
controller: daemonset
spec: # 详情描述
revisionHistoryLimit: 3 # 保留历史版本
updateStrategy: # 更新策略
type: RollingUpdate # 滚动更新策略
rollingUpdate: # 滚动更新
maxUnavailable: 1 # 最大不可用状态的Pod的最大值,可以为百分比,也可以为整数
selector: # 选择器,通过它指定该控制器管理哪些pod
matchLabels: # Labels匹配规则
app: nginx-pod
matchExpressions: # Expressions匹配规则
- {key: app, operator: In, values: [nginx-pod]}
template: # 模板,当副本数量不足时,会根据下面的模板创建pod副本
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
1)Job
Job主要用于负责**批量处理(一次要处理指定数量任务)短暂的一次性(每个任务仅运行一次就结束)**任务。Job特点如下:
Job的资源清单文件:
apiVersion: batch/v1 # 版本号
kind: Job # 类型
metadata: # 元数据
name: # rs名称
namespace: # 所属命名空间
labels: # 标签
controller: job
spec: # 详情描述
completions: 1 # 指定job需要成功运行Pods的次数。默认值:1
parallelism: 1 # 指定job在任一时刻应该并发运行Pods的数量。默认值:1
activeDeadlineSeconds: 30 # 指定job可运行的时间期限,超过时间还未结束,系统将会尝试进行终止
backoffLimit: 6 # 指定job失败后进行重试的次数。默认是6
manualSelector: true # 是否可以使用selector选择器选择pod,默认是false
selector: # 选择器,通过它指定该控制器管理哪些pod
matchLabels: # Labels匹配规则
app: counter-pod
matchExpressions: # Expressions匹配规则
- {key: app, operator: In, values: [counter-pod]}
template: # 模板,当副本数量不足时,会根据下面的模板创建pod副本
metadata:
labels:
app: counter-pod
spec:
restartPolicy: Never # 重启策略只能设置为Never或者OnFailure
containers:
- name: counter
image: busybox:1.30
command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 2;done"]
关于重启策略设置的说明:
2)CronJob
CronJob控制器以Job控制器资源为其管控对象,并借助它管理Pod资源对象,Job控制器定义的作业任务在其控制器资源创建之后便会立即执行,但CronJob可以以类似于Linux操作系统的周期性任务作业计划的方式控制其运行时间点及重复运行的方式。也就是说,CronJob可以在特定的时间点(反复的)去运行Job任务
CronJob的资源清单文件:
apiVersion: batch/v1beta1 # 版本号
kind: CronJob # 类型
metadata: # 元数据
name: # rs名称
namespace: # 所属命名空间
labels: # 标签
controller: cronjob
spec: # 详情描述
schedule: # cron格式的作业调度运行时间点,用于控制任务在什么时间执行
concurrencyPolicy: # 并发执行策略,用于定义前一次作业运行尚未完成时是否以及如何运行后一次的作业
failedJobHistoryLimit: # 为失败的任务执行保留的历史记录数,默认为1
successfulJobHistoryLimit: # 为成功的任务执行保留的历史记录数,默认为3
startingDeadlineSeconds: # 启动作业错误的超时时长
jobTemplate: # job控制器模板,用于为cronjob控制器生成job对象;下面其实就是job的定义
metadata:
spec:
completions: 1
parallelism: 1
activeDeadlineSeconds: 30
backoffLimit: 6
manualSelector: true
selector:
matchLabels:
app: counter-pod
matchExpressions: 规则
- {key: app, operator: In, values: [counter-pod]}
template:
metadata:
labels:
app: counter-pod
spec:
restartPolicy: Never
containers:
- name: counter
image: busybox:1.30
command: ["bin/sh","-c","for i in 9 8 7 6 5 4 3 2 1; do echo $i;sleep 20;done"]
需要重点解释的几个选项:
schedule: cron表达式,用于指定任务的执行时间
*/1 * * * *
<分钟> <小时> <日> <月份> <星期>
分钟值从0到59
小时值从0到23
日值从1到31
月值从1到12
星期值从0到6,0代表星期日
多个时间可以用逗号隔开,范围可以用连字符给出,*可以作为通配符,/表示每...
concurrencyPolicy:
1)工作原理
首先,StatefulSet的控制器直接管理的是Pod。这是因为,StatefulSet里的不同Pod实例,不再像ReplicaSet中那样都是完全一样的,而是有了细微区别的。比如,每个Pod的hostname、名字等都是不同的、携带了编号的。而StatefulSet区分这些实例的方式,就是通过在Pod的名字里加上事先约定好的编号
其次,Kubernetes通过Headless Service,为这些有编号的Pod,在DNS服务器中生成带有同样编号的DNS记录。只要StatefulSet能够保证这些Pod名字里的编号不变,那么Service里类似于web-0.nginx.default.svc.cluster.local
这样的DNS记录也就不会变,而这条记录解析出来的Pod的IP地址,则会随着后端Pod的删除和再创建而自动更新。这当然是Service机制本身的能力,不需要StatefulSet操心
最后,StatefulSet还为每一个Pod分配并创建一个同样编号的PVC。这样,Kubernetes就可以通过Persistent Volume机制为这个PVC绑定上对应的PV,从而保证了每一个Pod都拥有一个独立的Volume
在这种情况下,即使Pod被删除,它所对应的PVC和PV依然会保留下来。所以当这个Pod被重新创建出来之后,Kubernetes会为它找到同样编号的PVC,挂载这个PVC对应的Volume,从而获取到以前保存在Volume里的数据
2)partition
partition这种更新策略的含义是,若当前StatefulSet的副本数为5个,则Pod名为pod-0~pod-4,那么此时定义partition=4,就意味着我要更新大于等于4的Pod,而只有pod-4的ID是大于等于4的,所以只有pod-4会被更新,其它不会,这就是金丝雀更新。若后期发现pod-4更新后,工作一切正常,那么就可以调整partition=0,这样只要ID大于等于0的pod都将被更新
Service会对提供同一个服务的多个Pod进行聚合,并且提供一个统一的入口地址。通过访问Service的入口地址就能访问到后面的Pod服务
Service在很多情况下只是一个概念,真正起作用的其实是kube-proxy服务进程,每个Node节点上都运行着一个kube-proxy服务进程。当创建Service的时候会通过API Server向etcd写入创建的Service的信息,而kube-proxy会基于监听的机制发现这种Service的变动,然后它会将最新的Service信息转换成对应的访问规则
1)kube-proxy工作模式
kube-proxy目前支持三种工作模式:
userspace模式
userspace模式下,kube-proxy会为每一个Service创建一个监听端口,发向Cluster IP的请求被Iptables规则重定向到kube-proxy监听的端口上,kube-proxy根据LB算法选择一个提供服务的Pod并和其建立链接,以将请求转发到Pod上
该模式下,kube-proxy充当了一个四层负载均衡器的角色。由于kube-proxy运行在userspace中,在进行转发处理时会增加内核和用户空间之间的数据拷贝,虽然比较稳定,但是效率比较低
iptables模式
iptables模式下,kube-proxy为Service后端的每个Pod创建对应的iptables规则,直接将发向Cluster IP的请求重定向到一个Pod IP
该模式下kube-proxy不承担四层负责均衡器的角色,只负责创建iptables规则。该模式的优点是较userspace模式效率更高,但不能提供灵活的LB策略,当后端Pod不可用时也无法进行重试
ipvs模式
ipvs模式和iptables类似,kube-proxy监控Pod的变化并创建相应的ipvs规则。ipvs相对iptables转发效率更高。除此以外,ipvs支持更多的LB算法
2)Service类型
Service有以下4种类型:
spec.clusterIP
为None来创建Headless Service3)Endpoints
Endpoints是Kubernetes中的一个资源对象,存储在etcd中,用来记录一个Service对应的所有Pod的访问地址,它是根据Service配置文件中selector描述产生的
一个Service由一组Pod组成,这些Pod通过Endpoints暴露出来,Endpoints是实际服务的端点集合。换句话说,Service和Pod之间的联系是通过Endpoints实现的
只有处于Running状态,且readinessProbe检查通过的Pod,才会出现在Service的Endpoints列表里。并且,当某一个Pod出现问题时,Kubernetes会自动把它从Service里摘除掉
# 查看endpoints
[root@k8s-master ~]# kubectl get endpoints -n dev
NAME ENDPOINTS AGE
service-clusterip 10.244.1.22:80,10.244.2.15:80,10.244.2.16:80 5m10s
Service对集群之外暴露服务的主要方式有两种:NotePort和LoadBalancer,但是这两种方式,都有一定的缺点:
基于这种现状,Kubernetes提供了Ingress资源对象,Ingress只需要一个NodePort或者一个LB就可以满足暴露多个Service的需求。工作机制大致如下图表示:
实际上,Ingress相当于一个7层的负载均衡器,是Kubernetes对反向代理的一个抽象,它的工作原理类似于Nginx,可以理解成在Ingress里建立诸多映射规则,Ingress Controller通过监听这些配置规则并转化成Nginx的反向代理配置 , 然后对外部提供服务。在这里有两个核心概念:
Ingress(以Nginx为例)的工作原理如下:
一个Nginx Ingress Controller提供的服务,其实是一个可以根据Ingress对象和被代理后端Service的变化,来自动进行更新的Nginx负载均衡器
Kuberentes通过Extended Resource来支持自定义资源,比如GPU。为了让调度器知道这种自定义资源在各Node上的数量,需要的Node里添加自定义资源的数量。实际上,这些信息并不需要人工去维护,所有的硬件加速设备的管理都通过Device Plugin插件来支持,也包括对该硬件的Extended Resource进行上报的逻辑
上报资源信息:
Device Plugin通过gRPC与本机kubelet连接 -> Device Plugin定期向kubelet汇报设备信息,比如GPU的数量 -> kubelet向API Server发送的心跳中,以Extended Reousrce的方式加上这些设备信息,比如GPU的数量
Pod分配一个GPU的流程:
Pod申明需要一个GPU -> 调度器找到GPU数量满足条件的Node -> Pod绑定到对应的Node上 -> kubelet发现需要拉起一个Pod,且该Pod需要GPU -> kubelet向Device Plugin发起Allocate()
请求 -> Device Plugin根据kubelet传递过来的需求,找到这些设备对应的设备路径和驱动目录,并返回给kubelet -> kubelet将这些信息追加在创建Pod所对应的CRI请求中 -> 容器创建完成之后,就会出现这个GPU设备(设备路径+驱动目录)
推荐阅读:
《Kubernetes权威指南 第4版》
Kubernetes学习笔记(一):Pod详解:Pod配置、Pod生命周期、Pod调度、容器设计模式
Kubernetes学习笔记(二):Pod控制器详解:资源元信息、ReplicaSet、Deployment、DaemonSet、Job、CronJob