做者|张磊 阿里云容器平台高级技术专家,CNCF 官方大使web
容器的基本概念后端
咱们知道 Pod 是 Kubernetes 项目里面一个很是重要的概念,也是很是重要的一个原子调度单位,可是为何咱们会须要这样一个概念呢?在使用容器 Docker 的时候,也没有这个说法。其实,若是想要理解 Pod,首先要理解容器,因此来回顾一下容器的概念:设计模式
容器的本质其实是一个进程,是一个视图被隔离,资源受限的进程。api
容器里面 PID=1 的进程就是应用自己,这意味着管理虚拟机等于管理基础设施,由于咱们是在管理机器,但管理容器却等于直接管理应用自己。这也是以前说过的不可变基础设施的一个最佳体现,这个时候,你的应用就等于你的基础设施,它必定是不可变的。微信
在以上面的例子为前提的状况下,Kubernetes 又是什么呢?不少人都说 Kubernetes 是云时代的操做系统,这个很是有意思,由于若是以此类推,容器镜像就是这个操做系统的软件安装包,它们之间是这样的一个类比关系。网络
真实操做系统里的例子less
若是说 Kubernetes 就是操做系统的话,那么不妨看一下真实的操做系统的例子。运维
例子里面有一个程序叫作 Helloworld,这个 Helloworld 程序其实是由一组进程组成的,须要注意一下,这里说的进程实际上等同于 Linux 中的线程。ssh
由于 Linux 中的线程是轻量级进程,因此若是从 Linux 系统中去查看 Helloworld 中的 pstree,将会看到这个 Helloworld 其实是由四个线程组成的,分别是 {api、main、log、compute}。也就是说,四个这样的线程共同协做,共享 Helloworld 程序的资源,组成了 Helloworld 程序的真实工做状况。分布式
这是操做系统里面进程组或者线程组中一个很是真实的例子,以上就是进程组的一个概念。
那么你们不妨思考一下,在真实的操做系统里面,一个程序每每是根据进程组来进行管理的。Kubernetes 把它类比为一个操做系统,好比说 Linux。针对于容器咱们前面提到能够类比为进程,就是前面的 Linux 线程。那么 Pod 又是什么呢?实际上 Pod 就是咱们刚刚提到的进程组,也就是 Linux 里的线程组。
进程组概念
说到进程组,首先建议你们至少有个概念上的理解,而后咱们再详细的解释一下。
仍是前面那个例子:Helloworld 程序由四个进程组成,这些进程之间会共享一些资源和文件。那么如今有一个问题:假如说如今把 Helloworld 程序用容器跑起来,你会怎么去作?
固然,最天然的一个解法就是,我如今就启动一个 Docker 容器,里面运行四个进程。但是这样会有一个问题,这种状况下容器里面 PID=1 的进程该是谁? 好比说,它应该是个人 main 进程,那么问题来了,“谁”又负责去管理剩余的 3 个进程呢?
这个核心问题在于,容器的设计自己是一种“单进程”模型,不是说容器里只能起一个进程,因为容器的应用等于进程,因此只能去管理 PID=1 的这个进程,其余再起来的进程实际上是一个托管状态。 因此说服务应用进程自己就具备“进程管理”的能力。
好比说 Helloworld 的程序有 system 的能力,或者直接把容器里 PID=1 的进程直接改为 systemd,不然这个应用,或者是容器是没有办法去管理不少个进程的。由于 PID=1 进程是应用自己,若是如今把这个 PID=1 的进程给 kill 了,或者它本身运行过程当中死掉了,那么剩下三个进程的资源就没有人回收了,这个是很是严重的一个问题。
反过来,若是真的把这个应用自己改为了 systemd,或者在容器里面运行了一个 systemd,将会致使另一个问题:使得管理容器再也不是管理应用自己了,而等因而管理 systemd,这里的问题就很是明显了。好比说我这个容器里面 run 的程序或者进程是 systemd,那么接下来,这个应用是否是退出了?是否是 fail 了?是否是出现异常失败了?其实是没办法直接知道的,由于容器管理的是 systemd。这就是为何在容器里面运行一个复杂程序每每比较困难的一个缘由。
这里再帮你们梳理一下:因为容器其实是一个“单进程”模型,因此若是你在容器里启动多个进程,只有一个能够做为 PID=1 的进程,而这时候,若是这个 PID=1 的进程挂了,或者说失败退出了,那么其余三个进程就会天然而然的成为孤儿,没有人可以管理它们,没有人可以回收它们的资源,这是一个很是很差的状况。
注意:Linux 容器的“单进程”模型,指的是容器的生命周期等同于 PID=1 的进程(容器应用进程)的生命周期,而不是说容器里不能建立多进程。固然,通常状况下,容器应用进程并不具有进程管理能力,因此你经过 exec 或者 ssh 在容器里建立的其余进程,一旦异常退出(好比 ssh 终止)是很容易变成孤儿进程的。
反过来,其实能够在容器里面 run 一个 systemd,用它来管理其余全部的进程。这样会产生第二个问题:实际上没办法直接管理个人应用了,由于个人应用被 systemd 给接管了,那么这个时候应用状态的生命周期就不等于容器生命周期。这个管理模型其实是很是很是复杂的。
Pod = “进程组”
在 Kubernetes 里面,Pod 实际上正是 Kubernetes 项目为你抽象出来的一个能够类比为进程组的概念。
前面提到的,由四个进程共同组成的一个应用 Helloworld,在 Kubernetes 里面,实际上会被定义为一个拥有四个容器的 Pod,这个概念你们必定要很是仔细的理解。
就是说如今有四个职责不一样、相互协做的进程,须要放在容器里去运行,在 Kubernetes 里面并不会把它们放到一个容器里,由于这里会遇到两个问题。那么在 Kubernetes 里会怎么去作呢?它会把四个独立的进程分别用四个独立的容器启动起来,而后把它们定义在一个 Pod 里面。
因此当 Kubernetes 把 Helloworld 给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于 Pod,因此咱们说 Pod 在 Kubernetes 里面只有一个逻辑单位,没有一个真实的东西对应说这个就是 Pod,不会有的。真正起来在物理上存在的东西,就是四个容器,这四个容器,或者说是多个容器的组合就叫作 Pod。而且还有一个概念必定要很是明确,Pod 是 Kubernetes 分配资源的一个单位,由于里面的容器要共享某些资源,因此 Pod 也是 Kubernetes 的原子调度单位。
上面提到的 Pod 设计,也不是 Kubernetes 项目本身想出来的, 而是早在 Google 研发 Borg 的时候,就已经发现了这样一个问题。这个在 Borg paper 里面有很是很是明确的描述。简单来讲 Google 工程师发如今 Borg 下面部署应用时,不少场景下都存在着相似于“进程与进程组”的关系。更具体的是,这些应用以前每每有着密切的协做关系,使得它们必须部署在同一台机器上而且共享某些信息。
以上就是进程组的概念,也是 Pod 的用法。
为何 Pod 必须是原子调度单位?
可能到这里你们会有一些问题:虽然了解这个东西是一个进程组,可是为何要把 Pod 自己做为一个概念抽象出来呢?或者说能不能经过调度把 Pod 这个事情给解决掉呢?为何 Pod 必须是 Kubernetes 里面的原子调度单位?
下面咱们经过一个例子来解释。
假如如今有两个容器,它们是紧密协做的,因此它们应该被部署在一个 Pod 里面。具体来讲,第一个容器叫作 App,就是业务容器,它会写日志文件;第二个容器叫作 LogCollector,它会把刚刚 App 容器写的日志文件转发到后端的 ElasticSearch 中。
两个容器的资源需求是这样的:App 容器须要 1G 内存,LogCollector 须要 0.5G 内存,而当前集群环境的可用内存是这样一个状况:Node_A:1.25G 内存,Node_B:2G 内存。
假如说如今没有 Pod 概念,就只有两个容器,这两个容器要紧密协做、运行在一台机器上。但是,若是调度器先把 App 调度到了 Node_A 上面,接下来会怎么样呢?这时你会发现:LogCollector 其实是没办法调度到 Node_A 上的,由于资源不够。其实此时整个应用自己就已经出问题了,调度已经失败了,必须去从新调度。
以上就是一个很是典型的成组调度失败的例子。英文叫作:Task co-scheduling 问题,这个问题不是说不能解,在不少项目里面,这样的问题都有解法。
好比说在 Mesos 里面,它会作一个事情,叫作资源囤积(resource hoarding):即当全部设置了 Affinity 约束的任务都达到时,才开始统一调度,这是一个很是典型的成组调度的解法。
因此上面提到的“App”和“LogCollector”这两个容器,在 Mesos 里面,他们不会说马上调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,由于须要等待。因为须要等,还会有外一个状况会出现,就是产生死锁,即互相等待的一个状况。这些机制在 Mesos 里都是须要解决的,也带来了额外的复杂度。
另外一种解法是 Google 的解法。它在 Omega 系统(就是 Borg 下一代)里面,作了一个很是复杂且很是厉害的解法,叫作乐观调度。好比说:无论这些冲突的异常状况,先调度,同时设置一个很是精妙的回滚机制,这样通过冲突后,经过回滚来解决问题。这个方式相对来讲要更加优雅,也更加高效,可是它的实现机制是很是复杂的。这个有不少人也能理解,就是悲观锁的设置必定比乐观锁要简单。
而像这样的一个 Task co-scheduling 问题,在 Kubernetes 里,就直接经过 Pod 这样一个概念去解决了。由于在 Kubernetes 里,这样的一个 App 容器和 LogCollector 容器必定是属于一个 Pod 的,它们在调度时必然是以一个 Pod 为单位进行调度,因此这个问题是根本不存在的。
再次理解 Pod
在讲了前面这些知识点以后,咱们来再次理解一下 Pod,首先 Pod 里面的容器是“超亲密关系”。
这里有个“超”字须要你们理解,正常来讲,有一种关系叫作亲密关系,这个亲密关系是必定能够经过调度来解决的。
好比说如今有两个 Pod,它们须要运行在同一台宿主机上,那这样就属于亲密关系,调度器必定是能够帮助去作的。可是对于超亲密关系来讲,有一个问题,即它必须经过 Pod 来解决。由于若是超亲密关系赋予不了,那么整个 Pod 或者说是整个应用都没法启动。
什么叫作超亲密关系呢?大概分为如下几类:
像以上几种关系都属于超亲密关系,它们都是在 Kubernetes 中会经过 Pod 的概念去解决的。
如今咱们理解了 Pod 这样的概念设计,理解了为何须要 Pod。它解决了两个问题:
Pod 要解决的问题
像 Pod 这样一个东西,自己是一个逻辑概念。那在机器上,它到底是怎么实现的呢?这就是咱们要解释的第二个问题。
既然说 Pod 要解决这个问题,核心就在于如何让一个 Pod 里的多个容器之间最高效的共享某些资源和数据。
由于容器之间本来是被 Linux Namespace 和 cgroups 隔开的,因此如今实际要解决的是怎么去打破这个隔离,而后共享某些事情和某些信息。这就是 Pod 的设计要解决的核心问题所在。
因此说具体的解法分为两个部分:网络和存储。
1.共享网络
第一个问题是 Pod 里的多个容器怎么去共享网络?下面是个例子:
好比说如今有一个 Pod,其中包含了一个容器 A 和一个容器 B,它们两个就要共享 Network Namespace。在 Kubernetes 里的解法是这样的:它会在每一个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。
Infra container 是一个很是小的镜像,大概 100~200KB 左右,是一个汇编语言写的、永远处于“暂停”状态的容器。因为有了这样一个 Infra container 以后,其余全部容器都会经过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
因此说一个 Pod 里面的全部容器,它们看到的网络视图是彻底同样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次建立的这个 Infra container。这就是 Pod 解决网络共享的一个解法。
在 Pod 里面,必定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。因此你们看到的都是一份,而其余全部网络资源,都是一个 Pod 一份,而且被 Pod 中的全部容器共享。这就是 Pod 的网络实现方式。
因为须要有一个至关于说中间的容器存在,因此整个 Pod 里面,必然是 Infra container 第一个启动。而且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为何在 Kubernetes 里面,它是容许去单独更新 Pod 里的某一个镜像的,即:作这个操做,整个 Pod 不会重建,也不会重启,这是很是重要的一个设计。
2.共享存储
第二问题:Pod 怎么去共享存储?Pod 共享存储就相对比较简单。
好比说如今有两个容器,一个是 Nginx,另一个是很是普通的容器,在 Nginx 里放一些文件,让我能经过 Nginx 访问到。因此它须要去 share 这个目录。我 share 文件或者是 share 目录在 Pod 里面是很是简单的,实际上就是把 volume 变成了 Pod level。而后全部容器,就是全部同属于一个 Pod 的容器,他们共享全部的 volume。
好比说上图的例子,这个 volume 叫作 shared-data,它是属于 Pod level 的,因此在每个容器里能够直接声明:要挂载 shared-data 这个 volume,只要你声明了你挂载这个 volume,你在容器里去看这个目录,实际上你们看到的就是同一份。这个就是 Kubernetes 经过 Pod 来给容器共享存储的一个作法。
因此在以前的例子中,应用容器 App 写了日志,只要这个日志是写在一个 volume 中,只要声明挂载了一样的 volume,这个 volume 就能够马上被另一个 LogCollector 容器给看到。以上就是 Pod 实现存储的方式。
如今咱们知道了为何须要 Pod,也了解了 Pod 这个东西究竟是怎么实现的。最后,以此为基础,详细介绍一下 Kubernetes 很是提倡的一个概念,叫作容器设计模式。
举例
接下来将会用一个例子来给你们进行讲解。
好比我如今有一个很是常见的一个诉求:我如今要发布一个应用,这个应用是 JAVA 写的,有一个 WAR 包须要把它放到 Tomcat 的 web APP 目录下面,这样就能够把它启动起来了。但是像这样一个 WAR 包或 Tomcat 这样一个容器的话,怎么去作,怎么去发布?这里面有几种作法。
可是这时会发现一个问题:这种作法必定须要维护一套分布式存储系统。由于这个容器可能第一次启动是在宿主机 A 上面,第二次从新启动就可能跑到 B 上去了,容器它是一个可迁移的东西,它的状态是不保持的。因此必须维护一套分布式存储系统,使容器无论是在 A 仍是在 B 上,均可以找到这个 WAR 包,找到这个数据。
注意,即便有了分布式存储系统作 Volume,你还须要负责维护 Volume 里的 WAR 包。好比:你须要单独写一套 Kubernetes Volume 插件,用来在每次 Pod 启动以前,把应用启动所需的 WAR 包下载到这个 Volume 里,而后才能被应用挂载使用到。
这样操做带来的复杂程度仍是比较高的,且这个容器自己必须依赖于一套持久化的存储插件(用来管理 Volume 里的 WAR 包内容)。
InitContainer
因此你们有没有考虑过,像这样的组合方式,有没有更加通用的方法?哪怕在本地 Kubernetes 上,没有分布式存储的状况下也能用、能玩、能发布。
实际上方法是有的,在 Kubernetes 里面,像这样的组合方式,叫作 Init Container。
仍是一样一个例子:在上图的 yaml 里,首先定义一个 Init Container,它只作一件事情,就是把 WAR 包从镜像里拷贝到一个 Volume 里面,它作完这个操做就退出了,因此 Init Container 会比用户容器先启动,而且严格按照定义顺序来依次执行。
而后,这个关键在于刚刚拷贝到的这样一个目的目录:APP 目录,其实是一个 Volume。而咱们前面提到,一个 Pod 里面的多个容器,它们是能够共享 Volume 的,因此如今这个 Tomcat 容器,只是打包了一个 Tomcat 镜像。但在启动的时候,要声明使用 APP 目录做为个人 Volume,而且要把它们挂载在 Web APP 目录下面。
而这个时候,因为前面运行过了一个 Init Container,已经执行完拷贝操做了,因此这个 Volume 里面已经存在了应用的 WAR 包:就是 sample.war,绝对已经存在这个 Volume 里面了。等到第二步执行启动这个 Tomcat 容器的时候,去挂这个 Volume,必定能在里面找到前面拷贝来的 sample.war。
因此能够这样去描述:这个 Pod 就是一个自包含的,能够把这一个 Pod 在全世界任何一个 Kubernetes 上面都顺利启用起来。不用担忧没有分布式存储、Volume 不是持久化的,它必定是能够公布的。
因此这是一个经过组合两个不一样角色的容器,而且按照一些像 Init Container 的编排方式,统一去打包这样一个应用,把它用 Pod 来去作的很是典型的一个例子。像这样的一个概念,在 Kubernetes 里面就是一个很是经典的容器设计模式,叫作:“Sidecar”。
容器设计模式:Sidecar
什么是 Sidecar?就是说其实在 Pod 里面,能够定义一些专门的容器,来执行主业务容器所须要的一些辅助工做,好比咱们前面举的例子,其实就干了一个事儿,这个 Init Container,它就是一个 Sidecar,它只负责把镜像里的 WAR 包拷贝到共享目录里面,以便被 Tomcat 可以用起来。
其它有哪些操做呢?好比说:
这种作法一个很是明显的优点就是在于其实将辅助功能从个人业务容器解耦了,因此我就可以独立发布 Sidecar 容器,而且更重要的是这个能力是能够重用的,即一样的一个监控 Sidecar 或者日志 Sidecar,能够被全公司的人共用的。这就是设计模式的一个威力。
Sidecar:应用与日志收集
接下来,咱们再详细细化一下 Sidecar 这样一个模式,它还有一些其余的场景。
好比说前面提到的应用日志收集,业务容器将日志写在一个 Volume 里面,而因为 Volume 在 Pod 里面是被共享的,因此日志容器 —— 即 Sidecar 容器必定能够经过共享该 Volume,直接把日志文件读出来,而后存到远程存储里面,或者转发到另一个例子。如今业界经常使用的 Fluentd 日志进程或日志组件,基本上都是这样的工做方式。
Sidecar:代理容器
Sidecar 的第二个用法,能够称做为代理容器 Proxy。什么叫作代理容器呢?
假如如今有个 Pod 须要访问一个外部系统,或者一些外部服务,可是这些外部系统是一个集群,那么这个时候如何经过一个统一的、简单的方式,用一个 IP 地址,就把这些集群都访问到?有一种方法就是:修改代码。由于代码里记录了这些集群的地址;另外还有一种解耦的方法,即经过 Sidecar 代理容器。
简单说,单独写一个这么小的 Proxy,用来处理对接外部的服务集群,它对外暴露出来只有一个 IP 地址就能够了。因此接下来,业务容器主要访问 Proxy,而后由 Proxy 去链接这些服务集群,这里的关键在于 Pod 里面多个容器是经过 localhost 直接通讯的,由于它们同属于一个 network Namespace,网络视图都同样,因此它们俩通讯 localhost,并无性能损耗。
因此说代理容器除了作了解耦以外,并不会下降性能,更重要的是,像这样一个代理容器的代码就又能够被全公司重用了。
Sidecar:适配器容器
Sidecar 的第三个设计模式 —— 适配器容器 Adapter,什么叫 Adapter 呢?
如今业务暴露出来的 API,好比说有个 API 的一个格式是 A,可是如今有一个外部系统要去访问个人业务容器,它只知道的一种格式是 API B ,因此要作一个工做,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你能够经过一个 Adapter 帮你来作这层转换。
有个例子:如今业务容器暴露出来的监控接口是 /metrics,访问这个容器的 metrics 的 URL 就能够拿到了。但是如今,这个监控系统升级了,它访问的 URL 是 /health,我只认得暴露出 health 健康检查的 URL,才能去作监控,metrics 不认识。那这个怎么办?那就须要改代码了,但能够不去改代码,额外写一个 Adapter,用来把全部对 health 的这个请求转发给 metrics 就能够了,因此这个 Adapter 对外暴露的是 health 这样一个监控的 URL,这就能够了,你的业务就又能够工做了。
这样的关键,还在于 Pod 之中的容器是经过 localhost 直接通讯的,因此没有性能损耗,而且这样一个 Adapter 容器能够被全公司重用起来,这些都是设计模式给咱们带来的好处。
Pod 与容器设计模式是 Kubernetes 体系里面最重要的一个基础知识点,但愿读者可以仔细揣摩和掌握。在这里,我建议你去从新审视一下以前本身公司或者团队里使用 Pod 方式,是否是或多或少采用了所谓“富容器”这种设计呢?这种设计,只是一种过渡形态,会培养出不少很是很差的运维习惯。我强烈建议你逐渐采用容器设计模式的思想,对富容器进行解耦,将它们拆分红多个容器组成一个 Pod。这也正是当前阿里巴巴“全面上云”战役中正在全力推动的一项重要的工做内容。
阿里巴巴云原生微信公众号(ID:Alicloudnative)关注微服务、Serverless、容器、Service Mesh等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术公众号。
本文为云栖社区原创内容,未经容许不得转载。