详解 Kubernetes 中的 Pod

2022-04-05 18:33:51   最后更新: 2022-04-05 18:33:51   访问数量:646




 

前面的文章中,我们相信介绍了 Kubernetes 的组成和架构,并且搭建出了一个基础的 Kubernetes 集群。

 

但我们对于 Kubernetes 最基础的 Pod 的了解仍然十分有限,本文我们就来详细介绍和讲解一下 Kubernetes 最核心的抽象 -- Pod。

 

 

在操作系统中,程序往往并非是单兵作战的,如果我们执行 pstree 命令,就可以看到进程是以成组的方式运行的,这才是最常见的状态。

 

想想我们的线上服务,各个服务之间也有着复杂的种种关系,即便是在单机上,也不乏这样需要成组调度的进程,这些进程间错综复杂的“关系”,对于一个进程即一个镜像的 Docker 抽象来说,是很难去处理的,这就需要在此之上进一步的抽象,来解决成组调度的问题。于是 Pod 应运而生。

 

事实上,Pod 只是 Kubernetes 中的一层逻辑概念,Kubernetes 调度的仍然是基础的容器,只是经过我们的配置,Kubernetes 将一些容器看作一个 Pod,从而能够统一调度,进而让他们处于同一个 Linux Namespace 中。

 

 

3.1 传统部署面临的挑战

 

传统通过 docker 镜像部署的方法是很难处理 Linux Namespace 的共享问题的。

 

假设有 A、B 两个容器,我们想让他们共享网络和 Volume Namespace,那么就需要县启动其中的一个:

 

$ docker run B

 

然后再在另一个启动时加入参数:

 

$ docker run --net=B --volumes-from=B --name=A image-A

 

这样一来,docker B 必须先启动,docker A 后启动,这两个本是一组的两个容器却不得不有了启动的先后,让他们的关系从平等关系变成了拓扑关系,如果规模扩大,这样的问题就会变得无比复杂而难以处理。

 

3.2 Kubernetes 的解决方案 -- Infra 容器

 

Kubernetes 解决上述问题靠的是引入 Infra 容器:

 

 

 

Infra 容器是 Pod 中隐式声明的容器,它先于其他容器的启动,它的主要工作就是分配并占用 Pod 容器中需要共享的 Linux Namespace,于是,容器 A 与容器 B 就可以真正做到平等了,他们都共享 Infra 容器的 Linux Namespace 即可,于是:

 

  • Pod 中各容器可以直接使用 localhost 进行通信,因为他们共享了 Infra 容器的 Net Namespace。
  • 各容器看到的网络设备与 Infra 容器看到的完全一致。
  • Pod 的生命周期只与 Infra 容器一致,而与容器 A 及容器 B 无关。

 

例如下面的 Pod 配置:

 

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"]

 

 

在这个配置中,声明了共享的 Volume Namespace:位于宿主机 /data 路径的 shared-data,在 Pod 启动之初,Infra 容器先行启动,并且分配了该 Volume Namespace,而在这之后启动的两个容器只要加入这个 Volume Namespace 并且挂载到镜像指定目录即可。Infra 容器的存在成功让两个容器得以解耦。

 

如果在 Pod 中配置和开启了 PID Namespace 的共享,就可以在容器中看到一个名为“/pause”的进程,它就是 Infra 的容器进程。

 

3.3 实例

 

考虑一个 java 应用应该如何被部署到云服务器上的呢?在传统的 Docker 部署模式下,我们可以看到 java 程序是以 tomcat 进程的方式运行起来的。而打包有 java 代码的 war 包则仅仅是 tomcat webapps 目录下的一部分。

 

于是,基于 Docker 的部署方案有两种选择:

 

  1. 每次上线前,将打包好的 war 包放到 tomcat 基础镜像中的 webapps 目录下,然后再打一个新的镜像,这个新镜像用来在线上部署;
  2. 所有的宿主机都外挂一个分布式存储系统,然后映射到磁盘上的一个指定路径,并且在 Tomcat 镜像启动时挂载到 Tomcat 中。

 

方案 1 看起来很方便,但一来我们会感觉到显然每次上线前都要进行的镜像打包工作是不必要的,二来,在遇到问题需要回滚前,首先需要将要回滚到的 war 包放到 Tomcat 镜像的 webapps 目录然后重新打镜像,之后再部署,这对于线上问题的处理也太过繁琐。

 

而方案 2 虽然免去了反复打包的困扰,但我们不得不维护一个分布式存储系统,看起来也毫无必要。

 

但在 Kubernetes 看来,问题却很容易解决:

 

apiVersion: v1 kind: Pod metadata: name: java-web spec: volumes: - name: app-volume emptyDir: {} initContainers: - image: techlog/smaple:v0.0.1 name: war command: ["cp", "/sample.war", "/app"] volumeMounts: - mountPath: /app name: app-volume containers: - image: techlog/tomcat:7.0 name: tomcat command: ["sh", "-c", "/root/apache-tomcat-7.0.42-v2/bin/start.sh"] volumeMounts: - name: app-volume ports: - containerPort: 8080 hostPort: 8001

 

 

在 Pod 的 spec 定义中,initContainers 是先于 containers 中定义的镜像先行启动的镜像列表。同时,不同于 containers 中不保证启动顺序的启动,initContainers 中定义的容器是保证按照列表顺序顺次启动的。

 

基于上述定义,我们的 war 包只需要每次放到宿主机的固定位置然后被复制到容器的指定路径即可,再也不用反复执行打镜像的操作了,世界是不是都清爽了呢?

 

这个例子中的 sample 镜像就充当了一个 sidecar 的角色,所谓的 sidecar,就是一种将应用程序的功能划分为单独进程的设计模式。类似的,我们也可以将日志收集、上报等功能划分出来作为一个 sidecar 容器单独启动起来,从而让整个系统更为清晰。

 

 

Pod 是 Kubernetes 的最小调度单位,而 Container 是 Pod 的最小组成单位。我们知道,容器其实就是单个启动的进程,而 Pod 则是要统一部署和调度的一组进程,因此,配置 Pod 时,需要考虑什么属性是进程组维度的,什么属性是进程维度的。

 

以下字段在 Pod 配置中是非常重要的:

 

NodeSelector

 

供用户将 Pod 与 Node 绑定的字段:

 

apiVersion: v1 kind: pod ... spec: nodeSelector: disktype: ssd

 

 

这样一来,containers 中只有打了这个指定的键值对 “disktype: ssd” 的容器才会在节点上运行。

 

HostAliases

 

用来在 Pod 的 hosts 文件(/etc/hosts)中添加的内容:

 

apiVersion: v1 kind: pod ... spec: hostAliases: - ip: "10.0.0.1" hostnames: - "foo.remote" - "bar.remote" - ip: "127.0.0.1" hostnames: - "foo.local" - "bar.local"

 

 

在 pod 启动后,/etc/hosts 文件中便会出现:

 

10.0.0.1 foo.remote bar.remote 127.0.0.1 foo.local bar.local

 

 

Linux Namespace 相关配置

 

想要在 Pod 中开启共享哪种 Linux Namespace 资源,只要在 spec 中配置相应的开关即可,例如:

 

  • shareProcessNamespace: true -- 开启 PID NameSpace 共享。也可以通过配置 hostPID 实现;
  • hostIPC: true
  • hostNetwork: true

 

ImagePullPolicy

 

镜像拉取策略,可以选择:

 

  • Always -- 每次启动前都拉取一次;
  • Never -- 从不拉取镜像;
  • IfNotPresent -- 只有不存在的时候拉取。

 

Lifecycle

 

Pod 允许用户定义容器状态变化时触发的钩子,例如:

 

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: ["/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done"]

 

 

它定义了容器启动前与结束前要做的事。

 

 

一个 Pod 的生命周期也就是这个 API 对象的 status,有以下五种:

 

  1. Pending -- API 对象已经成功创建,并且保存在 etcd 中,但 Pod 中的某些容器创建、调度不成功。
  2. Running -- Pod 调度成功,且所有包含的容器都已经创建成功,至少有一个容器正在运行。
  3. Secceeded -- 所有容器都已经正常运行完毕并退出。
  4. Failed -- 至少有一个容器以非 0 返回码的错误状态退出。
  5. Unknown -- Pod 状态异常,可能是与 Kubernetes 的 Master 节点通信出现了问题。

 

当需要进一步排查引起相应状态的原因时,我们需要关注 Pod 的 Events 以及细分的状态字段 Conditions,它包括:

 

  1. PodScheduled -- Pod 已经被调度;
  2. Ready -- Pod 已就绪;
  3. Initialized -- Pod 已完成初始化;
  4. Unschedulable -- Pod 无法被调度。

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

linux 使用及配置相关






linux      技术贴      docker      沙盒      kubernetes      k8s      虚 拟化     


京ICP备2021035038号