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的节点上,否则调度失败。

NodeNamePod被调度的节点的名字。这个值一般由调度器负责设置,一旦设置了这个值,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: true

Pod里最关键的是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的当前状态。有如下情况:

  1. pending:Pod的YAML文件已经提交给了k8s,并且API对象已经被创建并保存在了Etcd当中。但是由于某些原因Pod中的某些容器不能被顺利启动,例如调度失败。
  2. running:Pod成功创建并与节点绑定。包含的容器都已经创建成功并且至少有一个正在运行。
  3. Succeeded:Pod中的所有容器都正常运行完毕并退出。
  4. Failed:Pod中至少有一个容器以不正常的状态退出。
  5. 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会合并它们的修改,但是如果存在冲突,那么就都不会修改冲突的字段


tags: k8s pod