Pod是Kubernetes系统的基础单元,是资源对象模型中可由用户创建或部署的最小组件,也是在Kubernetes系统上运行容器化应用的资源对象。
Docker推荐采用单容器单进程的方式运行,但由于容器间的隔离机制,各容器进程间又无法实现IPC(Inter-Process Communication)通信。这就导致功能相关的容器之间通信困难,比如主容器与负责日志收集的容器之间的通信。而Pod资源抽象正是用来解决此类问题的组件,Pod对象是一组容器的集合,这些容器共享Network、UTS(UNIX Time-sharing System)及IPC名称空间,因此具有相同的域名、主机名和网络接口,并可通过IPC直接通信。
为一个Pod对象中的各容器提供网络名称空间等共享机制的是底层基础容器pause。
尽管Pod支持运行多个容器,但作为最佳实践,除非多个进程之间具有密切的关系,否则都应该将其构建到多个Pod中,这样多个Pod可被调度至多个不同的主机运行,提高了资源利用率,也便于规模的伸缩。
多个进程之间具有密切的关系时,一般按照边车模型来组织多个容器,边车即为Pod的主应用容器提供协同的辅助应用容器,典型的应用场景是将主应用容器中的日志使用agent收集至日志服务器中时,可以将agent运行为辅助应用容器。
Pod的配置清单举例:
apiVersion: v1
kind: Pod
metadata:
name: pod-example
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
其中spec字段下,containers为及其子字段name为必选项,image在手动场景Pod时必选,在但在被高级别管理资源如Deployment控制时可选,因为这个字段可能会被覆盖。
Pod的核心功能是运行容器,而 通过image.imagePullPolicy可以自定义镜像的获取策略。
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
imagePullPolicy: Always
对于标签为“latest”的镜像文件,其默认的镜像获取策略即为“Always”,而对于其他标签的镜像,其默认策略则为“IfNotPresent”。
在Pod中暴露端口与为Docker容器暴露端口的意义不一样:
在Docker的网络模型中,使用默认网络的容器化应用需通过NAT机制将其“暴露”(expose)到外部网络中才能被其他节点之上的容器客户端所访问;
而在K8S中,各Pod的IP地址已经处于同一网络平面,无论是否为容器暴露端口,都不会影响集群中其他节点之上的Pod客户端对其进行访问,所以暴露的端口只是信息性数据,而且显式指定容器端口也方便调用。
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
ports:
- name: http
containerPort: 80
protocol: TCP
这里的配置指定暴露容器上的TCP端口80,并将其命名为http。
Pod对象的IP地址仅在当前集群内可达,它们无法直接接收来自集群外部客户端的请求流量,尽管它们的服务可达性不受工作节点边界的约束,但依然受制于集群边界。如何让集群外部访问到Pod对象,将在后面学习。
command字段能够指定不同于镜像默认运行的应用程序,并且可以同时使用args字段进行参数传递,它们将覆盖镜像中的默认定义。不过,如果仅为容器定义了args字段,那么它将作为参数传递给镜像中默认指定运行的应用程序;如果仅为容器定义了command字段,那么它将覆盖镜像中定义的程序及参数,并以无参数方式运行应用程序。
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
imagePullPolicy: Never
command: ["/bin/sh"]
args: ["-c", "while true; do sleep 30; done"]
环境变量也是向容器化应用传递配置的一种方式,向Pod对象中的容器环境变量传递数据的方法有两种:env和envFrom,这里只介绍第一种方式,第二种方式将在介绍ConfigMap和Secret资源时进行说明。环境变量通常由name和value字段构成。
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
env:
- name: REDIS_HOST
value: do.macOS
- name: LOG_LEVEL
value: info
标签选择器可以对附带标签的资源对象进行挑选,并进行所需要的操作。一个对象可拥有不止一个标签,而同一个标签也可被添加至多个资源之上。
可以为资源附加多个不同纬度的标签以实现灵活的资源分组管理功能,例如,版本标签、环境标签、分层架构标签等,用于交叉标识同一个资源所属的不同版本、环境及架构层级等。
定义标签示例:
apiVersion: v1
kind: Pod
metadata:
name: pod-example
labels:
env: qa
tier: frontend
资源创建后,在kubectl get pods
命令中添加--show-labels
选项就可显示lables信息。
-L
选项可增加对应的列信息。
直接管理活动对象的标签:
kubectl label pods/pod-example release=beta
为pod-example添加了release=beta,如果要修改已经存在的减值对,需要添加--overwrite
选项。
标签选择器用于表达标签的查询条件或选择标准,Kubernetes API目前支持两个选择器:基于
Kubernetes的诸多资源对象必须以标签选择器的方式关联到Pod资源对象,例如Service、Deployment和ReplicaSet类型的资源等,可以在spec字段通过嵌套的“selector”字段来指定选择器,有两种方式:
selector:
matchLabels:
component: redis
matchExpressions:
- {key: tier, operator: In, values: [cache]}
- {key: environment, operator: Exists, values:}
标签之外,Pod与其他各种资源还能使用资源注解(annotation),也是键值类型的数据,不过它不能用于标签及挑选Kubernetes对象,仅可用于为资源提供“元数据”信息。另外,注解中的元数据不受字符数量的限制,可以为结构化或非结构化形式,而且对字符类型也没有限制。
Annotation中放置构建、发行或镜像相关的信息,指向日志、监控、分析或审计仓库的地址,或者由客户端库或工具程序生成的用于调试目的的信息:如名称、版本、构建信息等信息。
查看资源注解
使用kubectl get -o yaml
和kubectl describe
命令均能显示资源的注解信息。
kubectl describe pods pod-example | grep "Annotations"
管理资源注解
在配置清单中定义annotations:
apiVersion: v1
kind: Pod
metadata:
name: pod-example
annotations:
created-by: "cluster admin"
追加annotations:
kubectl annotate pods pod-example created-by2="admin"
Pod对象总是应该处于其生命进程中以下几个Phase(阶段)之一:
Pod的创建过程是指Pod自身及其主容器及其辅助容器创建的过程。
除了创建应用容器(主容器及其辅助容器)之外,用户还可以为Pod对象定义其生命周期中的多种行为,如用于初始化的容器、存活性探测及就绪性探测等
用于初始化的容器(init container)是应用程序的主容器启动之前要运行的容器,常用于为主容器执行一些预置操作,典型的应用如:
在资源清单中通过initContainers字段定义:
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
initContainers:
- name: init-something
image: busybox
command: ['sh', '-c', 'sleep 10']
Kubernetes为容器提供了两种生命周期钩子:
钩子函数的实现方式有“Exec”和“HTTP”两种,前一种在钩子事件触发时直接在当前容器中运行由用户定义的命令,后一种则是在当前容器中向某URL发起HTTP请求。钩子函数定义在容器的spec.lifecycle字段。
容器程序发生崩溃或容器申请超出限制的资源等原因都可能会导致Pod对象的终止,此时是否应该重建该Pod对象则取决于其重启策略(restartPolicy)属性的定义。
容器在重启失败后,之后的重启将有一段时间的延迟,且延迟时间越来越长,依次为10秒、20秒、40秒、80秒、160秒、300秒。
如果在等待进程终止的过程中,kubelet或容器管理器发生了重启,那么终止操作会重新获得一个满额的删除宽限期并重新执行删除操作。
kubelet可基于存活性探测判定何时需要重启一个容器。可通过spec.containers.livenessProbe定义,支持三种探测方法:
exec类型的探针通过在目标容器中执行由用户自定义的命令来判定容器的健康状态,若命令状态返回值为0则表示“成功”通过检测,其它值均为“失败”状态。它只有一个可用属性“command”,用于指定要执行的命令,示例:
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec-demo
labels:
test: liveness-exec-demo
spec:
containers:
- name: liveness-exec-demo
image: busybox
args: ["/bin/sh", "-c", " touch /tmp/healthy;sleep 60; rm -rf /tmp/healthy;sleep 600"]
livenessProbe:
exec:
command: ["test", "-e", "/tmp/healthy"]
这段配置清单基于busybox镜像启动一个容器,并执行args定义的命令,此命令在容器启动时创建/tmp/healthy文件,并于60秒之后将其删除。存活性探针运行“test -e/tmp/healthy”命令检查/tmp/healthy文件的存在性,若文件存在则返回状态码0,表示成功通过测试。
所以60秒后使用describe命令可以看到容器被重启的event。
httpGet方式是向目标容器发起一个HTTP GET请求,根据其响应码进行结果判定,2xx或3xx时表示检测通过。
可配置字段有:
示例
apiVersion: v1
kind: Pod
metadata:
name: liveness-http-demo
labels:
test: liveness-http-demo
spec:
containers:
- name: liveness-http-demo
image: nginx:1.12-alpine
ports:
- name: http
containerPort: 80
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", " echo Healthy > /usr/share/nginx/html/healthz"]
livenessProbe:
httpGet:
path: /healthz
port: http
scheme: HTTP
这个配置清单通过postStart hook创建了一个专用于httpGet测试的页面文件healthz。而为httpGet探测指定的路径为“/healthz”,地址默认为Pod IP,端口使用了容器中定义的端口名称http。
启动容器后健康检查是正常的,但执行如下命令删除healthz页面后,可在event中看到Container liveness-http-demo failed liveness probe, will be restarted
。
kubectl exec liveness-http-demo rm /usr/share/nginx/html/healthz
一般应为HTTP探测操作定义专用的URL路径,此URL路径对应的Web资源应该以轻量化的方式在内部对应用程序的各关键组件进行全面检测以确保它们可正常向客户端提供完整的服务。
基于TCP的存活性探测用于向容器的特定端口发起TCP请求并尝试建立连接,连接建立成功即为通过检测。相比较来说,它比基于HTTP的探测要更高效、更节约资源,但精准度较低。
可配置字段有:
举例:
spec:
containers:
- name: liveness-tcp-demo
image: nginx:1.12-alpine
livenessProbe:
tcpSocket:
port: 80
对于配置了liveness的pod,通过describe命令可以看到类似这样的信息,有delay、timeout等配置,由于之前没有指定所以都为默认值:
Liveness: tcp-socket :80 delay=0s timeout=1s period=10s #success=1 #failure=3
另外,liveness检测仅对当前服务有效,比如但后端服务(如数据库或缓存服务)导致故障时,重启当前服务并不能解决问题,但它却会被一次次重启,直到后端服务恢复正常为止。
Pod对象启动后,容器应用通常需要一段时间才能完成其初始化过程,例如加载配置或数据,甚至有些程序还需要预热的过程。因此应该避免在Pod对象启动后立即让其处理客户端请求,而是等待容器初始化工作执行完成并转为Ready状态,尤其是存在其他提供相同服务的Pod对象的场景更是如此。
就绪性探测是用来判断容器就绪与否的周期性操作,探测操作返回“success”状态时,就认为容器已经就绪。
与liveness探测类似,它也支持三种方式,但定义时使用的属性名为readinessProbe。
举例:
apiVersion: v1
kind: Pod
metadata:
name: readiness-tcp-demo
labels:
test: readiness-tcp-demo
spec:
containers:
- name: readiness-tcp-demo
image: nginx:1.12-alpine
readinessProbe:
tcpSocket:
port: 80
未定义就绪性探测的Pod对象在Pod进入“Running”状态后将立即就绪。生产实践中,必须为需要时间进行初始化容器以及关键性Pod资源中的容器定义就绪性探测。
K8S中可由容器或Pod请求或消费的“计算资源”是指CPU和内存,其中CPU属于可压缩(compressible)型资源,可按需收缩,而内存则是不可压缩型资源,对其执行收缩操作可能会导致无法预知的问题。
目前资源隔离属于容器级别,所以CPU和内存资源的配置需要在Pod中的容器上进行,支持两种属性:
在K8S中,1个单位的CPU相当于虚拟机上的1颗虚拟CPU(vCPU)或物理机上的一个超线程(Hyperthread,或称为一个逻辑CPU),它支持分数计量方式,一个核心(1 core)相当于1000个微核心(millicores),因此500m相当于是0.5个核心。内存的计量方式与日常使用方式相同,默认单位是字节,也可以使用E(Ei)、P(Pi)、T(Ti)、G(Gi)、M(Mi)和K(Ki)作为单位后缀。
apiVersion: v1
kind: Pod
metadata:
name: stress-demo
spec:
containers:
- name: stress-demo
image: ikubernetes/stress-ng
command: ["/usr/bin/stress-ng", "-m 1", "-c 1", "--metrics-brief"]
resources:
requests:
memory: "128Mi"
cpu: "200m"
以上的配置清单定义了容器的资源需求为128M内存、200m(0.2)个CPU核心。它运行stress-ng(一个多功能系统压力测工具)镜像启动一个进程(-m 1)进行内存性能压力测试,再启动一个专用的CPU压力测试进程(-c 1)。
然后使用kubectl exec stress-demo -- top
命令来查看资源的使用情况,在我的电脑(6核,内存16G)上显示的内存占用为262m,CPU占用2*17%(约等于2/6,因为两个测试线程分布于两个CPU核心以满载的方式运行),都远高于requests中定义的值,这是因为当前资源充裕,一旦
资源紧张时,节点仅保证容器有五分之一个CPU核心可用,对于有着6个核心的节点来说,它的占用率约为3.33%,多占用的资源会被压缩。内存为非可压缩型资源,所以此Pod在内存资源紧张时可能会因OOM被杀死(killed)。
如果没有定义requests,那么在CPU资源紧张时,可能会被其它Pod压缩至极低的水平,甚至会达到Pod不能够被调度运行的境地,而不可压缩型的内存资源,则可能因OOM导致进程被杀死。因此在Kubernetes系统上运行关键型业务相关的Pod时必须使用requests属性为容器定义资源的确保可用量。
集群中的每个节点拥有的CPU和内存资源是固定的,Kubernetes的调度器在调度Pod时,会根据容器的requests属性来判定哪些节点可接收运行当前的Pod资源,而对于一个节点的资源来说,每运行一个Pod对象,其requests中定义的请求量都要被预留,直到给所有Pod对象分配完为止。
通过定义资源需求可以保证容器的最少资源量,如果要限制容器使用资源的上限,则需要定义资源限制。
如果定义了资源限制,则容器进程无法获得超出其CPU配额的可用时间,而进程申请分配超出其limits定义的内存资源时,它将被OOM killer杀死。
Kubernetes允许节点资源对limits的过载使用,这意味着节点无法同时满足其上的所有Pod对象以资源满载的方式运行。于是就需要确定Pod对象的优先级,在内存资源紧缺时,先终止低优先级的Pod对象。
Pod对象的优先级是根据requests和limits属性确定的,分为三个级别或QoS(Quality of Service):
以上只适用于内存资源紧缺时,CPU资源无法得到满足时,Pod仅仅是暂时获取不到相应的资源而已。
《Kubernetes实战进阶》 马永亮著