程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

Kubernetes | 为什么需要 Pod?Pod 是什么?

发表于 2020-06-20 | 分类于 Kubernetes | 0 | 阅读次数 1668

Pod,而不是容器,才是 Kubernetes 项目中的最小编排单位,也是 Kubernetes 项目中最小的 API 对象。容器(Container)只是 Pod 属性里的一个普通的字段。

1. 为什么需要 Pod

为什么需要 Pod 这一块的内容是对「极客时间《深入剖析 Kubernetes》专栏-13|为什么我们需要 Pod」的摘抄,因为张磊老师写的太好了,一点都省不得删,就几乎都保留了。

1.1. 调度层面

首先容器的本质是什么?容器的本质是进程。在一个真正的操作系统里,进程并不是“孤苦伶仃”地独自运行的,而是以进程组的方式,“有原则地”组织在一起。比如,系统中有一个叫做 rsyslogd 的程序,它负责的是 Linux 操作系统里的日志处理。可以看到,rsyslogd 的主程序 main,和它要用到的内核日志模块 imklog 等,同属于 879 进程组。这些进程相互协作,共同完成 rsyslogd 程序的职责。

容器是未来云计算系统中的进程,容器镜像就是这个系统里的 ".exe" 安装包,kubernetes 就是操作系统。

上述提到的“进程”,比如,rsyslogd 对应的 imklog,imuxsock 和 main,严格意义上来说,其实是 Linux 操作系统语境下的“线程”。这些线程,或者说,轻量级进程之间,可以共享文件、信号、数据内存、甚至部分代码,从而紧密协作共同完成一个程序的职责。所以同理,我提到的“进程组”,对应的也是 Linux 操作系统语境下的“线程组”。

而 Kubernetes 项目所做的,其实就是将“进程组”的概念映射到了容器技术中,并使其成为了这个云计算“操作系统”里的“一等公民”。在 Borg 项目的开发和实践过程中,Google 公司的工程师们发现,他们部署的应用,往往都存在着类似于“进程和进程组”的关系。更具体地说,就是这些应用之间有着密切的协作关系,使得它们必须部署在同一台机器上。而如果事先没有“组”的概念,像这样的运维关系就会非常难以处理。

还是以前面的 rsyslogd 为例子。已知 rsyslogd 由三个进程组成:一个 imklog 模块,一个 imuxsock 模块,一个 rsyslogd 自己的 main 函数主进程。这三个进程一定要运行在同一台机器上,否则,它们之间基于 Socket 的通信和文件交换,都会出现问题。现在,我要把 rsyslogd 这个应用给容器化,由于受限于容器的“单进程模型”,这三个模块必须被分别制作成三个不同的容器。

容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个 PID=1 进程的子进程。可是,用户编写的应用,并不能够像正常操作系统里的 init 进程或者 systemd 那样拥有进程管理的功能。

而在这三个容器运行的时候,它们设置的内存配额都是 1 GB。假设我们的 Kubernetes 集群上有两个节点:node-1 上有 3 GB 可用内存,node-2 有 2.5 GB 可用内存。当其中两个容器被调度到 node-2 上运行的时候,剩下的一个容器无法在 node-2 上运行,那么又需要对 node-2 上运行的两个容器进行撤回等操作。而假设将 3 个容器统一进行调度,那么这三个容器就不会被调度到 node-2 上,而是只会被调度到 node-1 上。

因此到了 Kubernetes 项目里,采用 Pod 作为 Kubernetes 里的原子调度单位,pod 中可包含多个容器。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。这样,Kubernetes 项目在调度时,自然就会去选择可用内存等于 3 GB 的 node-1 节点进行绑定,而根本不会考虑 node-2。

像这样容器间的紧密协作,我们可以称为“超亲密关系”。这些具有“超亲密关系”容器的典型特征包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或者 Socket 文件进行本地通信、会发生非常频繁的远程调用、需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。

1.2. 容器设计模式层面

除了上述的调度原因之外,Pod 在 Kubernetes 项目里还有更重要的意义,那就是:容器设计模式。

首先来看一下 Pod 的实现原理,Pod 最重要的一个事实就是:它只是一个逻辑概念。Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。Pod 其实是一组共享了某些资源的容器而已。具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。

也就是说,一个有 A 和 B 两个容器的 Pod,其实就等同于一个容器 A 共享另一个容器 B 的网络和 Volume。这可以通过如下命令实现:

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

但是,如果真这样做的话,容器 B 就必须比容器 A 先启动,这样一个 Pod 里的多个容器就不是对等关系,而是拓扑关系了。所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。这样的组织关系,可以用下面这样一个示意图来表达:

如上图所示,这个 Pod 里有两个用户容器 A 和 B,还有一个 Infra 容器。很容易理解,在 Kubernetes 项目里,Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。而在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的 Network Namespace 当中了。所以,如果你查看这些容器在宿主机上的 Namespace 文件,它们指向的值一定是完全一样的。这也就意味着,对于 Pod 里的容器 A 和容器 B 来说:

  • 它们可以直接使用 localhost 进行通信;
  • 它们看到的网络设备跟 Infra 容器看到的完全一样;
  • 一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
  • 当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;
  • Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。

而对于同一个 Pod 里面的所有用户容器来说,它们的进出流量,也可以认为都是通过 Infra 容器完成的。这一点很重要,因为将来如果你要为 Kubernetes 开发一个网络插件时,应该重点考虑的是如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。这就意味着,如果你的网络插件需要在容器里安装某些包或者配置才能完成的话,是不可取的:Infra 容器镜像的 rootfs 里几乎什么都没有,没有你随意发挥的空间。当然,这同时也意味着你的网络插件完全不必关心用户容器的启动与否,而只需要关注如何配置 Pod,也就是 Infra 容器的 Network Namespace 即可。

有了这个设计之后,共享 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 文件的原因。


上述就是 Pod 的实现原理,那么了解了 Pod 的实现原理之后,下面来讨论一下容器设计模式。

Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。为了能够掌握这种思考方式,我们以使用它来描述一些用单个容器难以解决的问题。

  1. WAR 包与 Web 服务器

    现在有一个 Java Web 应用的 WAR 包,它需要被放在 Tomcat 的 webapps 目录下运行起来。假如现在只能用 Docker 来做这件事情,那该如何处理这个组合关系呢?

    • 一种方法是,把 WAR 包直接放在 Tomcat 镜像的 webapps 目录下,做成一个新的镜像运行起来。可是,这时候,如果你要更新 WAR 包的内容,或者要升级 Tomcat 镜像,就要重新制作一个新的发布镜像,非常麻烦。
    • 另一种方法是,你压根儿不管 WAR 包,永远只发布一个 Tomcat 容器。不过,这个容器的 webapps 目录,就必须声明一个 hostPath 类型的 Volume,从而把宿主机上的 WAR 包挂载进 Tomcat 容器当中运行起来。不过,这样你就必须要解决一个问题,即:如何让每一台宿主机,都预先准备好这个存储有 WAR 包的目录呢?这样来看,你只能独立维护一套分布式存储系统了。

    实际上,有了 Pod 之后,这样的问题就很容易解决了。我们可以把 WAR 包和 Tomcat 分别做成镜像,然后把它们作为一个 Pod 里的两个容器“组合”在一起。这个 Pod 的配置文件如下所示:

    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: {}
    

    在这个 Pod 中,我们定义了两个容器,第一个容器使用的镜像是 geektime/sample:v2,这个镜像里只有一个 WAR 包(sample.war)放在根目录下。而第二个容器则使用的是一个标准的 Tomcat 镜像。需要注意的是,WAR 包容器的类型不再是一个普通容器,而是一个 Init Container 类型的容器。这个 Init Container 类型的 WAR 包容器启动后,我执行了一句"cp /sample.war /app",把应用的 WAR 包拷贝到 /app 目录下,然后退出。

    在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。

    而后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。接下来就,Tomcat 容器,同样声明了挂载 app-volume 到自己的 webapps 目录下。所以,等 Tomcat 容器启动时,它的 webapps 目录下就一定会存在 sample.war 文件:这个文件正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。

    像这样,我们就用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题。实际上,这个所谓的“组合”操作,正是容器设计模式里最常用的一种模式,它的名字叫:sidecar。比如,在我们的这个应用 Pod 中,Tomcat 容器是我们要使用的主容器,而 WAR 包容器的存在,只是为了给它提供一个 WAR 包而已。所以,我们用 Init Container 的方式优先运行 WAR 包容器,扮演了一个 sidecar 的角色。

    sidecar 指的就是我们可以在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进程(主容器)之外的工作。

  2. 容器的日志收集

    这个应用就是相当于不断地把容器的日志文件保存下来。

    这时,我就可以把一个 Pod 里的 Volume 挂载到应用容器的 /var/log 目录上。然后,我在这个 Pod 里同时运行一个 sidecar 容器,它也声明挂载同一个 Volume 到自己的 /var/log 目录上。这样,接下来 sidecar 容器就只需要做一件事儿,那就是不断地从自己的 /var/log 目录里读取日志文件,转发到 MongoDB 或者 Elasticsearch 中存储起来。这样,一个最基本的日志收集工作就完成了。

上述展示的更多是 Pod 对 Volume 的应用,但不要忘记,Pod 的另一个重要特性是,它的所有容器都共享同一个 Network Namespace。这就使得很多与 Pod 网络相关的配置和管理,也都可以交给 sidecar 完成,而完全无须干涉用户容器。这里最典型的例子莫过于 Istio 这个微服务治理项目了。

Kubernetes 社区曾经把“容器设计模式”这个理论,整理成了一篇小论文,你可以点击链接浏览: https://www.usenix.org/conference/hotcloud16/workshop-program/presentation/burns

1.3. 总结

为什么需要 Pod 呢?主要是从两面考虑:

  • 个人理解是调度层面。容器的本质是进程,在 Linux 系统中进程存在进程组这么一个概念。假如将运行的进行搬到 kubernetes 中的话,那么一个进程会对应一个容器,那么我们对这些进程的调度也应该是保持原进程租的特性,所以按照容器单独进行调度的话会存在一定问题,所以应该也按照组的方式进行调度。因此Kubernetes 提出了 Pod,Pod 中可以运行多个容器,这就相当于按照原先进程租的方式进行调度了。
  • 个人理解是容器的设计模式。假如不采用 Pod 而是采用仍采用容器的方式的话,那么在某些场景会存在弊端,比如前面提到的 Web 和 WAR,而采用 Pod 之后,我们可以让 Pod 可以共享一个 Volume 等,那么这个问题就迎刃而解了。

2. Pod 本质

以下内容是对「极客时间《深入剖析 Kubernetes》专栏-13|为什么我们需要 Pod」的摘抄,因为这段写的太爽了,所以一个字都不敢动。

事实上,直到现在,仍有很多人把容器跟虚拟机相提并论,他们把容器当做性能更好的虚拟机,喜欢讨论如何把应用从虚拟机无缝地迁移到容器中。但实际上,无论是从具体的实现原理,还是从使用方法、特性、功能等方面,容器与虚拟机几乎没有任何相似的地方;也不存在一种普遍的方法,能够把虚拟机里的应用无缝迁移到容器中。因为,容器的性能优势,必然伴随着相应缺陷,即:它不能像虚拟机那样,完全模拟本地物理机环境中的部署方法。

实际上,一个运行在虚拟机里的应用,哪怕再简单,也是被管理在 systemd 或者 supervisord 之下的一组进程,而不是一个进程。这跟本地物理机上应用的运行方式其实是一样的。这也是为什么,从物理机到虚拟机之间的应用迁移,往往并不困难。

可是对于容器来说,一个容器永远只能管理一个进程。更确切地说,一个容器,就是一个进程。这是容器技术的“天性”,不可能被修改。所以,将一个原本运行在虚拟机里的应用,“无缝迁移”到容器中的想法,实际上跟容器的本质是相悖的。

这也是当初 Swarm 项目无法成长起来的重要原因之一:一旦到了真正的生产环境上,Swarm 这种单容器的工作方式,就难以描述真实世界里复杂的应用架构了。

所以,你现在可以这么理解 Pod 的本质:Pod 扮演的是传统部署环境里“虚拟机”的角色,而容器,则是这个“虚拟机”里运行的用户程序。Pod 这样的设计,可以让用户从传统环境(虚拟机环境)向 Kubernetes(容器环境)的迁移,更加平滑。而如果你能把 Pod 看成传统环境里的“机器”、把容器看作是运行在这个“机器”里的“用户程序”,那么很多关于 Pod 对象的设计就非常容易理解了。因此,凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。这些属性的共同特征是,它们描述的是“机器”这个整体,而不是里面运行的“程序”。比如,配置这个“机器”的网卡(即:Pod 的网络定义),配置这个“机器”的磁盘(即:Pod 的存储定义),配置这个“机器”的防火墙(即:Pod 的安全定义)。更不用说,这台“机器”运行在哪个服务器之上(即:Pod 的调度)。

所以下一次,当你需要把一个运行在虚拟机里的应用迁移到 Docker 容器中时,一定要仔细分析到底有哪些进程(组件)运行在这个虚拟机里。然后,你就可以把整个虚拟机想象成为一个 Pod,把这些进程分别做成容器镜像,把有顺序关系的容器,定义为 Init Container。这才是更加合理的、松耦合的容器编排诀窍,也是从传统应用架构,到“微服务架构”最自然的过渡方式。

另外,Pod 这个概念,提供的是一种编排思想,而不是具体的技术方案。所以,如果愿意的话,你完全可以使用虚拟机来作为 Pod 的实现,然后把用户容器都运行在这个虚拟机里。比如,Mirantis 公司的 virtlet 项目就在干这个事情。甚至,你可以去实现一个带有 Init 进程的容器项目,来模拟传统应用的运行方式。

巨人的肩膀

  1. 极客时间.《深入剖析Kubernetes》.张磊
卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/156
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 云原生 # Kubernetes
Kubernetes | Kubernetes 集群部署
Java HashMap 的那么多为什么
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2025 程序锅
0%