Kubernetes — 为何咱们须要Pod?

 

不过,我相信你在学习和使用 Kubernetes 项目的过程当中,已经不止一次地想要问这样一个问题:为何咱们会须要 Pod?html

是啊,咱们在前面已经花了不少精力去解读 Linux 容器的原理、分析了 Docker 容器的本质,终于,“Namespace 作隔离,Cgroups 作限制,rootfs 作文件系统”这样的“三句箴言”能够朗朗上口了,为何 Kubernetes 项目又忽然搞出一个 Pod 来呢?java

要回答这个问题,咱们仍是要一块儿回忆一下我曾经反复强调的一个问题:容器的本质究竟是什么?node

你如今应该能够不假思索地回答出来:容器的本质是进程。nginx

没错。容器,就是将来云计算系统中的进程;容器镜像就是这个系统里的“.exe”安装包。那么 Kubernetes 呢? 你应该也能马上回答上来:Kubernetes 就是操做系统! 很是正确。 如今,就让咱们登陆到一台 Linux 机器里,执行一条以下所示的命令:web

pstree -g

  

这条命令的做用,是展现当前系统中正在运行的进程的树状结构。它的返回结果以下所示:docker

systemd(1)-+-accounts-daemon(1984)-+-{gdbus}(1984)
           | `-{gmain}(1984)
           |-acpid(2044)
          ...      
           |-lxcfs(1936)-+-{lxcfs}(1936)
           | `-{lxcfs}(1936)
           |-mdadm(2135)
           |-ntpd(2358)
           |-polkitd(2128)-+-{gdbus}(2128)
           | `-{gmain}(2128)
           |-rsyslogd(1632)-+-{in:imklog}(1632)
           |  |-{in:imuxsock) S 1(1632)
           | `-{rs:main Q:Reg}(1632)
           |-snapd(1942)-+-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)

  

不难发现,在一个真正的操做系统里,进程并非“孤苦伶仃”地独自运行的,而是以进程组的方式,“有原则地”组织在一块儿。好比,这里有一个叫做 rsyslogd 的程序,它负责的是 Linux 操做系统里的日志处理。能够看到,rsyslogd 的主程序 main,和它要用到的内核日志模块 imklog 等,同属于 1632 进程组。这些进程相互协做,共同完成 rsyslogd 程序的职责。apache

注意:我在本篇中提到的“进程”,好比,rsyslogd 对应的 imklog,imuxsock 和 main,严格意义上来讲,实际上是 Linux 操做系统语境下的“线程”。这些线程,或者说,轻量级进程之间,能够共享文件、信号、数据内存、甚至部分代码,从而紧密协做共同完成一个程序的职责。因此同理,我提到的“进程组”,对应的也是 Linux 操做系统语境下的“线程组”。这种命名关系与实际状况的不一致,是 Linux 发展历史中的一个遗留问题。设计模式

项目所作的,其实就是将“进程组”的概念映射到了容器技术中,并使其成为了这个云计算“操做系统”里的“一等公民”。api

Kubernetes 项目之因此要这么作的缘由,我在前面介绍 Kubernetes 和 Borg 的关系时曾经提到过:在 Borg 项目的开发和实践过程当中,Google 公司的工程师们发现,他们部署的应用,每每都存在着相似于“进程和进程组”的关系。更具体地说,就是这些应用之间有着密切的协做关系,使得它们必须部署在同一台机器上。tomcat

而若是事先没有“组”的概念,像这样的运维关系就会很是难以处理。

我仍是之前面的 rsyslogd 为例子。已知 rsyslogd 由三个进程组成:一个 imklog 模块,一个 imuxsock 模块,一个 rsyslogd 本身的 main 函数主进程。这三个进程必定要运行在同一台机器上,不然,它们之间基于 Socket 的通讯和文件交换,都会出现问题。

如今,我要把 rsyslogd 这个应用给容器化,因为受限于容器的“单进程模型”,这三个模块必须被分别制做成三个不一样的容器。而在这三个容器运行的时候,它们设置的内存配额都是 1 GB。

 

再次强调一下:容器的“单进程模型”,并非指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是由于容器里 PID=1 的进程就是应用自己,其余的进程都是这个 PID=1 进程的子进程。但是,用户编写的应用,并不可以像正常操做系统里的 init 进程或者 systemd 那样拥有进程管理的功能。好比,你的应用是一个 Java Web 程序(PID=1),而后你执行 docker exec 在后台启动了一个 Nginx 进程(PID=3)。但是,当这个 Nginx 进程异常退出的时候,你该怎么知道呢?这个进程退出后的垃圾收集工做,又应该由谁去作呢?

 

假设咱们的 Kubernetes 集群上有两个节点:node-1 上有 3 GB 可用内存,node-2 有 2.5 GB 可用内存。

这时,假设我要用 Docker Swarm 来运行这个 rsyslogd 程序。为了可以让这三个容器都运行在同一台机器上,我就必须在另外两个容器上设置一个 affinity=main(与 main 容器有亲密性)的约束,即:它们俩必须和 main 容器运行在同一台机器上。

而后,我顺序执行:“docker run main”“docker run imklog”和“docker run imuxsock”,建立这三个容器。

这样,这三个容器都会进入 Swarm 的待调度队列。而后,main 容器和 imklog 容器都前后出队并被调度到了 node-2 上(这个状况是彻底有可能的)。

 

但是,当 imuxsock 容器出队开始被调度时,Swarm 就有点懵了:node-2 上的可用资源只有 0.5 GB 了,并不足以运行 imuxsock 容器;但是,根据 affinity=main 的约束,imuxsock 容器又只能运行在 node-2 上。

这就是一个典型的成组调度(gang scheduling)没有被妥善处理的例子。

在工业界和学术界,关于这个问题的讨论可谓旷日持久,也产生了不少可供选择的解决方案。 好比,Mesos 中就有一个资源囤积(resource hoarding)的机制,会在全部设置了 Affinity 约束的任务都达到时,才开始对它们统一进行调度。而在 Google Omega 论文中,则提出了使用乐观调度处理冲突的方法,即:先无论这些冲突,而是经过精心设计的回滚机制在出现了冲突以后解决问题。

但是这些方法都谈不上完美。资源囤积带来了不可避免的调度效率损失和死锁的可能性;而乐观调度的复杂程度,则不是常规技术团队所能驾驭的。

可是,到了 Kubernetes 项目里,这样的问题就迎刃而解了:Pod 是 Kubernetes 里的原子调度单位。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。

 

因此,像 imklog、imuxsock 和 main 函数主进程这样的三个容器,正是一个典型的由三个容器组成的 Pod。Kubernetes 项目在调度时,天然就会去选择可用内存等于 3 GB 的 node-1 节点进行绑定,而根本不会考虑 node-2。

像这样容器间的紧密协做,咱们能够称为“超亲密关系”。这些具备“超亲密关系”容器的典型特征包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或者 Socket 文件进行本地通讯、会发生很是频繁的远程调用、须要共享某些 Linux Namespace(好比,一个容器要加入另外一个容器的 Network Namespace)等等。

这也就意味着,并非全部有“关系”的容器都属于同一个 Pod。好比,PHP 应用容器和 MySQL 虽然会发生访问关系,但并无必要、也不该该部署在同一台机器上,它们更适合作成两个 Pod。

不过,相信此时你可能会有第二个疑问: 对于初学者来讲,通常都是先学会了用 Docker 这种单容器的工具,才会开始接触 Pod。

而若是 Pod 的设计只是出于调度上的考虑,那么 Kubernetes 项目彷佛彻底没有必要非得把 Pod 做为“一等公民”吧?这不是故意增长用户的学习门槛吗?

 

没错,若是只是处理“超亲密关系”这样的调度问题,有 Borg 和 Omega 论文珠玉在前,Kubernetes 项目确定能够在调度器层面给它解决掉。

不过,Pod 在 Kubernetes 项目里还有更重要的意义,那就是:容器设计模式。

为了理解这一层含义,我就必须先给你介绍一下Pod+的实现原理。

首先,关于 Pod 最重要的一个事实是:它只是一个逻辑概念。 也就是说,Kubernetes 真正处理的,仍是宿主机操做系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。

那么,Pod 又是怎么被“建立”出来的呢?

答案是:Pod,实际上是一组共享了某些资源的容器。 具体的说:Pod 里的全部容器,共享的是同一个 Network Namespace,而且能够声明共享同一个 Volume。 那这么来看的话,一个有 A、B 两个容器的 Pod,不就是等同于一个容器(容器 A)共享另一个容器(容器 B)的网络和 Volume 的玩儿法么?

这好像经过 docker run --net --volumes-from 这样的命令就能实现嘛,好比:

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 文件(这个 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 里的多个容器。为了可以掌握这种思考方式,你就应该尽可能尝试使用它来描述一些用单个容器难以解决的问题。

第一个最典型的例子是: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 中,咱们定义了两个容器,第一个容器使用的镜像是 geektim/sample:v2,这个镜像里只有一个 WAR 包(sample.war)放在根目录下。而第二个容器则使用的是一个标准的 Tomcat 镜像。

不过,你可能已经注意到,WAR 包容器的类型再也不是一个普通容器,而是一个 Init Container 类型的容器。 在 Pod 中,全部 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。而且,Init Container 容器会按顺序逐一启动,而直到它们都启动而且退出了,用户容器才会启动。

因此,这个 Init Container 类型的 WAR 包容器启动后,我执行了一句"cp /sample.war /app",把应用的 WAR 包拷贝到 /app 目录下,而后退出。

然后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。

接下来就很关键了。Tomcat 容器,一样声明了挂载 app-volume 到本身的 webapps 目录下。

因此,等 Tomcat 容器启动时,它的 webapps 目录下就必定会存在 sample.war 文件:这个文件正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。

像这样,咱们就用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题。

实际上,这个所谓的“组合”操做,正是容器设计模式里最经常使用的一种模式,它的名字叫:sidecar。

顾名思义,sidecar 指的就是咱们能够在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进程(主容器)以外的工做。

好比,在咱们的这个应用 Pod 中,Tomcat 容器是咱们要使用的主容器,而 WAR 包容器的存在,只是为了给它提供一个 WAR 包而已。因此,咱们用 Init Container 的方式优先运行 WAR 包容器,扮演了一个 sidecar 的角色。

 

第二个例子,则是容器的日志收集

好比,我如今有一个应用,须要不断地把日志文件输出到容器的 /var/log 目录中。

这时,我就能够把一个 Pod 里的 Volume 挂载到应用容器的 /var/log 目录上。 而后,我在这个 Pod 里同时运行一个 sidecar 容器,它也声明挂载同一个 Volume 到本身的 /var/log 目录上。

这样,接下来 sidecar 容器就只须要作一件事儿,那就是不断地从本身的 /var/log 目录里读取日志文件,转发到 MongoDB 或者 Elasticsearch 中存储起来。这样,一个最基本的日志收集工做就完成了。 跟第一个例子同样,这个例子中的 sidecar 的主要工做也是使用共享的 Volume 来完成对文件的操做。

但不要忘记,Pod 的另外一个重要特性是,它的全部容器都共享同一个 Network Namespace。这就使得不少与 Pod 网络相关的配置和管理,也均可以交给 sidecar 完成,而彻底无须干涉用户容器。这里最典型的例子莫过于 Istio 这个微服务治理项目了。 Istio 项目使用 sidecar  容器完成微服务治理的原理,我在后面很快会讲解到。

 备忘

Pod,其实是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这个虚拟机里运行的用户程序。

 k8s.gcr.io/pause 的存在使得一个pod中的全部镜像能够:

  • 它们能够直接使用 localhost 进行通讯;
  • 它们看到的网络设备跟 Infra 容器看到的彻底同样;
  • 一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
  • 固然,其余的全部网络资源,都是一个 Pod 一份,而且被该 Pod 中的全部容器共享;
  • Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
相关文章
相关标签/搜索