Pod是k8s中最小的API对象,也可以说Pod是k8s项目的原子调度单位。
为什么需要Pod
经过容器概念中的学习,我们能知道了容器是“Namespace做隔离,Cgroups做限制,rootfs做文件系统”的一个特殊的进程。
简单的说,容器是一个进程,那么管理调度这些容器的k8s也可以说是一个操作系统。而在一个真正的操作系统当中,进程不是单独存在的,而是以进程组的方式组织在一起的,因为进程也需要相互协作来共同完成某个功能。
k8s就是将进程组的概念映射到了容器技术中,因为应用之间也存在这样的关系,需要紧密协作,并且这些应用必须部署在同一台机器上。如果使用容器将这些应用一个一个部署的话,可能会出现,部署了一部分之后,机器的资源无法继续部署后续的应用,但是这些应用又必须在同一台机器上的情况。k8s解决了这个问题:Pod是k8s中的原子调度单位,这个Pod是具有紧密协作的容器组成的一个整体,是统一计算资源的,会直接选择能够满足需要的机器去部署。
紧密协作的典型特征包括但不限于:互相之间发生直接的文件交换、使用localhost或者Socket文件进行本地通信、发生频繁的远程调用、共享某些Linux Namespace等等。
解决紧密协作关系的容器是一个原因,但不是主要原因,因为这一点可以在调度器层面就解决掉。Pod在k8s中更重要的意义就是:容器设计模式。这一点在后面继续说明,现在需要先介绍一下Pod的实现原理。
Pod的实现原理
Pod最重要的一个事实是:它只是一个逻辑概念。k8s真正处理的,还是宿主机系统上Linux的Namespace和Cgroups,并不存在一个所谓的Pod的边界或者隔离环境。
Pod是一组共享了某些资源的容器。具体的说,Pod里容器能够共享同一个Network Namespace并且可以共享同一个Volume。
一个又A,B两个容器的Pod,就是A,B共享相同的网络和Volume。这一点使用docker就能够完成
$ docker run --net=B --volumes-from=B --name=A image-A ...但是这样B就必须比A先启动,Pod里的容器就不是对等关系,而是拓扑关系了。所以在k8s的Pod中,引入了一个中间容器Infra,这个容器永远是Pod中最先创建的容器,其它用户定义的容器,都是通过Join Network Namespace的方式与Infra容器关联在一起。
所以可以确定,每个Pod中都要又一个Infra容器,因此这个容器占用的资源一定要非常少。事实也是如此,这个容器使用的镜像是k8s.gcr.io/pause,这是一个汇编编写的,永远暂停状态的容器。Pod中用户定义的容器就是加入这个容器的Network Namespace和Volume中。
对于Pod中的容器来说:
- 可以直接使用localhost通信。
- 它们看到的网络设备与Infra容器看到的一样。
- 一个Pod只有一个IP地址,也就是这个Pod的Network Namespace对应的IP地址。
- 所有的网络资源都是一个Pod一份,由该Pod中的所有容器共享
- Pod的生命周期只与Infra容器一致,与其它容器无关。
总之就是Pod即使调度的最小单位,也是资源分配的最小单位,一个Pod中的所有容器共享资源。
因此,如果需要为Pod开发网络插件,应该考虑的是如何配置这个Pod的Network Namespace,而不是某个容器如何使用。
因为这个设计,共享Volume就很简单了,k8s只要将所有的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"]在这里两个容器都声明挂载shared-data的Volume,而这个Volume在宿主机上的目录就是/data,所有这里的两个容器nginx-container和debian-container都可以共享/data目录。
介绍了这些,接下来就重新说明一下容器设计模式。
容器设计模式
Pod这种超亲密容器的设计思想,即使用户希望在一个容器中运行多个功能不相关的应用的时候,应该优先考虑是否能够被描述成一个Pod里的多个容器。
例子1:WAR包与Web服务器
一个WAR包要放到Tomcat下的webapps目录下。
使用docker要完成这个功能:
- 将整体制作成一个镜像,但是只要更新tomcat或者WAR包就需要重新制作新的发布镜像。
- 只发布tomcat镜像,挂载宿主机上的目录到webapps,WAR包放在宿主机上的挂载目录。但是这样需要保证每一台宿主机都预先准备好挂载到webapps的目录。
这样处理都很麻烦。而使用Pod之后就很容易了。将tomcat和WAR包都制作成镜像,然后作为一个Pod中的容器组合到一起。 然后共享一个Volume,这样就可以将WAR包放在tomcat的webapps目录下了,如下配置:
apiVersion: v1
kind: Pod
metadata:
name: javaweb-2
spec:
initContainers:
- image: geektime/sample:v2
name: war
command: ["cp", "/sample.war", "/app"]
volumeMounts:
- mountPath: /app
name: app-volume
containers:
- image: geektime/tomcat:7.0
name: tomcat
command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
volumeMounts:
- mountPath: /root/apache-tomcat-7.0.42-v2/webapps
name: app-volume
ports:
- containerPort: 8080
hostPort: 8001
volumes:
- name: app-volume
emptyDir: {}需要注意的是,WAR包的容器类型不是普通容器,而是一个initContainer类型的容器。在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
所以在tomcat启动之前,WAR包就已经复制到了其webapps目录下。
这种WAR包和tomcat容器的组合操作,就是容器设计模式中的sidecar,指在一个Pod中启动一个辅助容器(WAR包容器),来完成独立于主容器(tomcat容器)之外的工作。
例子2:容器的日志收集
一个应用,不断将日志输出到/var/log目录中。于是就把一个Pod的Volume挂载到应用容器的/var/log目录下。 再启动一个sidecar容器,将同一个volume挂载到自己的/var/log目录下,于是它就能够读取另一个应用产生的日志并不断将其转发到Mongodb或者Elasticsearch中存储起来。
这样就完成了一个最基本的日志收集工作。
小结
容器与虚拟机的不同在于,容器运行的只是一个进程,而虚拟机中的进程哪怕再简单也是运行再systemd下的一组进程。本地物理机与虚拟机的运行方式是一样的,所以物理机到虚拟机的应用迁移并不困难。
但是将一个虚拟机的引用无缝迁移到容器中是跟容器的本质相悖的。所以,Pod的本质可以这么理解:
Pod扮演虚拟机的角色,而容器则是这个虚拟机中运行的用户程序。
整个虚拟机想象成为一个 Pod,把这些进程分别做成容器镜像,把有顺序关系的容器,定义为 Init Container。这才是更加合理的、松耦合的容器编排诀窍,也是从传统应用架构,到“微服务架构”最自然的过渡方式。
Pod的基本概念
Pod是k8s中的最小编排单位,这个设计落在API对象的设计上,容器就成为了一个普通的字段。那么设计上就需要考虑哪些属性属于Pod,哪些属性属于容器。
上一节中说明过,Pod扮演的是传统部署环境中虚拟机的角色,那么与之类比,凡是调度、网络、存储以及安全相关的属性,基本上都是Pod级别的。因为这些属性共同的特征是描述的是这个“机器”的整体,而不是运行的某个“程序”。
Pod的重要字段
接下来就介绍一些Pod中的重要字段。
NodeSelector:是一个供用户将Pod与Node进行绑定的字段。例如:
apiVersion: v1
kind: Pod
...
spec:
nodeSelector:
disktype: ssd上面的配置意味着这个Pod只能运行在携带了”disktype: ssd”的labels的节点上,否则调度失败。
NodeName:Pod被调度的节点的名字。这个值一般由调度器负责设置,一旦设置了这个值,k8s就会认为这个Pod已经经过了调度,调度的结果就是赋值的节点的名字。测试的时候可以用这个字段来骗过调度器、
HostAliases:定义了Pod的hosts文件的内容。例如:
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...在这个Pod启动之后,在/etc/hosts文件中将会出现
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote对Pod的hosts只能使用这种方式,否则对Pod删除重建后,kubelet会自动覆盖掉被修改的内容。
与Namespace相关的属性,也是Pod级别的。例如下面声明了shareProcessNamespace: true,说明Pod中的容器共享PID Namespace。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true这里定义了两个容器,nginx和开启了stdin与tty的shell容器。tty是与用户交互的终端窗口,接收用户的输入并输出结果,接收输入需要开启标准输入流stdin。
然后启动这个容器
$ kubectl create -f nginx.yaml可以连接到shell容器的tty上:
$ kubeclt exec -ti nginx -c shell sh然后执行ps aux命令可以看到Pod中的所有容器进程。
所有的容器也可以共享宿主机的Namespace,这也是Pod级别。如下,这样就能够直接使用宿主机的网络,与宿主机进行IPC通信,看到宿主机的进程。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: truePod里最关键的是containers字段,定义了Pod中容器的信息。这个字段下还有一些属性:Image镜像,command启动命令,workingDir工作目录,ports开放的端口,volumeMounts挂载的Volume等等都是containers的重要字段。其中还有一些需要额外注意的字段:
- ImagePullPolicy :定义了镜像的拉取策略。默认是Always,每次创建Pod都尝试去远程仓库拉取一次镜像(远程仓库与本地仓库的镜像不同)。或者镜像的名字是类似与
nginx或者nginx:latest的时候,默认也是Always(因为要使用最新的镜像)。为Never或者IfNotPresent的时候就永远不会或者本地不存在时拉取镜像。 - Lifecycle:这是容器生命周期的钩子函数,在容器状态发生改变的时候触发。例如下面的例子当中,就会在容器启动之后(ENTRYPOINT执行但不一定执行完毕)和容器停止之前执行设定的命令。
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]postStart执行超时或者失败,信息会记录在Event中,并且Pod也会失败。而preStop会阻塞当前容器杀死进程,直到其执行完毕。上面的例子中,容器启动后打印一串欢迎信息到/usr/share/message,退出之前会执行nginx -s quit实现了容器的优雅退出。
Pod的生命周期的变化,体现在API对象的Status部分,pod.status.phase字段就是Pod的当前状态。有如下情况:
- pending:Pod的YAML文件已经提交给了k8s,并且API对象已经被创建并保存在了Etcd当中。但是由于某些原因Pod中的某些容器不能被顺利启动,例如调度失败。
- running:Pod成功创建并与节点绑定。包含的容器都已经创建成功并且至少有一个正在运行。
- Succeeded:Pod中的所有容器都正常运行完毕并退出。
- Failed:Pod中至少有一个容器以不正常的状态退出。
- Unknown:这是一个异常状态。
status还可以再细分出condition,来获取更多的状态信息。通过状态能够迅速判断出Pod的运行情况并对异常情况进行跟踪和分析。
Pod预设置
也就是模板,让运维人员提前编写好PodPreset,这样开发人员就只需要编写一个简单的、基本的POD YAML,就可以提交了。
一个PodPreset的例子如下:
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}现在开发人员提交一个POD YAML
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80先创建PodPreset,再创建Pod
$ kubectl create -f preset.yaml
$ kubectl create -f pod.yaml那么最终看到的创建的Pod的API对象如下:
$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
annotations:
podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
containers:
- name: website
image: nginx
volumeMounts:
- mountPath: /cache
name: cache-volume
ports:
- containerPort: 80
env:
- name: DB_PORT
value: "6379"
volumes:
- name: cache-volume
emptyDir: {}Note
PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。
如果定义了同时作用于多个Pod的PodPreset,那么k8s会合并它们的修改,但是如果存在冲突,那么就都不会修改冲突的字段。