笔者在前边的两篇文章中,详细介绍了在Kubernetes平台调度的基本单位是POD,并且我们也介绍了Deployment这个对象,通过Deployment对象来部署应用,并且我们还可以通过scale这样的命令来非常方便的扩容,可以说Kubernetes为我们运维需要提供高并发流量的互联网应用,提供了完善的平台。
但是,不知道你有没有想过,既然有了容器,镜像,Kubernetes为什么还需要再抽象一个POD这样的对象出来呢?或者换个角度来看,POD给部署在Kubernetes平台上的应用带来了具体哪些好处?我们是否可以在Kubernetes上不使用POD来部署应用,笔者希望通过今天这篇文章,来把这几个问题给讲清楚。
我们在《云计算时代操作系统Kubernetes之Pod(上)》这篇文章中将容器进程和虚拟机中的进程进行了对比,如果对这部分内容不是很清楚的的读者,请回去再仔细看看。容器本质上就是操作系统上一个特殊的进程,这个特殊性主要体现在:容器进程通过设置不同命名空间(Linux操作系统有7种),以及利用内核的Cgroup和镜像提供的文件系统,让容器进程认为自己是运行在宿主机上的唯一的进程。
因此容器本质上就是进程,而容器运行的应用程序就来自于我们提供的镜像,而Kubernetes扮演的角色就类似于操作系统,我们通过kubectl create等命令提交给Kubernetes集群的应用程序任务,就雷雨我们在Windows操作系统上双击可执行文件exe,操作系统会给启动的exe文件创建对应的进程,以及分配系统资源,并启动应用开始执行。从这个角度来看,Kubernetes其实就是未来企业数字化应用部署和运维的企业级虚拟化应用程序部署和管理的操作系统。
而随着多核技术的发展,以及超线程机制,我们的应用程序运行起来后,为了提高运行的效率以及资源的使用率,通常会创建多个线程,这是高并发服务端应用程序的架构设计的核心,因此我们可以看到进程或者线程从来都不是单兵作战(线程可以看成是一个轻量级的进程,另外在Golang等语言中还有协程的概念,),多个线程相互协作,工作完成应用的请求处理。而对于用户来说,如果我们要在Kubernetes平台上部署应用程序,需要有对应的抽象,这样才能顺利将我们的应用程序迁移到Kubernetes平台上。
注意:我们知道操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程,这样在当前线程等待IO的过程中,其它线程可以继续执行。当系统线程较少的时候没有什么问题,但是当线程数量非常多的时候,却产生了问题。一是系统线程会占用非常多的内存空间,二是过多的线程切换会占用大量的系统时间。而协程的出现就是为了解决上述2个问题。协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
我们在介绍容器的时候,特别强调了容器的单进程模型,而单进程本质上不是说容器中只能运行一个进程,而是说容器中PID=1的进程,也就是我们在容器中启动的应用程序,不具备管理进程的能力。这句话看着还是晕啊,你可以想一下,Linux操作系统在诞生支持,大概是V0.11版本的时候,才实现了进程并发,进程并发有个核心的功能是进程管理,我们需要管理进程运行过程中fork的子进程,以及协调主进程和子进程的资源使用,状态同步等,操作系统实现了一套复杂的进程或者线程管理机制,因此我们才能在Linux操作系统上启动多任务,但是很明显咱自己写的SpringCloud应用并没有这样的逻辑。
说到这里你可能会问,PID=1这个进程没有进程管理能力有啥影响啊?我们来举个例子,比如我们部署了订单中心,订单中心在启动的时候,通过docker exec启动了一个Nginx进程,来提供订单服务需要的某些静态资源的访问,这样我们就在一个容器中,启动了两个进程,但是这个Nginx进程从启动一刻开始,就处于散养状态,Nginx进程退出,没有人知道,因此那就没有办法纳入到Kubernetes的管理中,因此在容器化部署方案中,笔者强烈建议大家一个容器中只启动一个进程,在这个进程中把工作做好。
但是问题来了,你刚说过应用程序一般都不是单兵作战的,会启动多个进程或者线程,如果一个容器只运行一个进程,我们的应用还能部署到Kubernetes平台吗?
首先回答这个问题,Kubernetes本身是谷歌多年容器化经验的集大成者,因此我们遇到的问题,其实谷歌很多年前应该都遇到过,并且解决这些问题的思路和方案都已经设计进Kubernetes平台了,因此从这个角度来看,Kubernetes要解决的一个核心的部署问题就是:如何将多个有关系的应用部署到一起?
我们来举个例子来说明一下,假设我们的订单中心由三个子模块构成,模块1负责处理购物车的逻辑,模块2主要用来处理下单,而模块三是一个定时任务,基于业务规则来定期将支付超时的订单关闭,针对这个应用程序来说,如果我们按Kubernetes之前的容器部署方案,比如说Docker Swarm,那么我们就需要将三个分别启动容器实例。
这三个模块有比较亲密的关系,并且在当前的数据中心中是通过localhost来进行通信的,因此我们必须将这三个容器实例运行在一台机器上,假设我们三个容器实例都需要1G的内存,那么当我们有两台工作节点Node1和Node2,其中Node1有2.5G可用的内存空间,而Node2有3G的可用内存空间.
我们在Node1上成功启动了模块1和2,但是当我们启动模块3的时候,由于Node1只剩下0.5G的内存,因此模块3无法运行在Node1,但是模块3又必须和模块1,2运行在同一台机器上,这样就比较尴尬了。
而这个问题的解决,就需要有更加优秀的调度算法,能够识别到这总具有亲密关系的应用,从而给他们统一申请计算资源,回到上边的例子中,如果Docker Swarm具备这种调度计算的能力,那么它就压根不会去考虑Node1,直接选择Node2就满足三个模块对部署的需求了。
回到Kubernetes平台上,你是否已经意识到Kubernetes处理这个问题的方案?没错,就是POD,POD是Kubernetes中资源调度的基本单位,而调度器的工作原理,其实就是基于POD的资源来进行申请和调度的。
而笔者刚才提到的订单中心的例子,我们可以通过在POD中启动三个容器,每个容器是一个进程,资源是按POD进行申请和调度,这个POD会被毫无争议的调度到Node2上,调度器压根就不会考虑Node1,而这种通过localhost进行通信的多个容器实例,我们称他们具备超亲密关系。
虽然说我们可以在一个POD里运行多个容器,按时笔者要再三强调,这只限于具备这种”超亲密“关系的容器,如果你问我是否应该把MYSQL和SpringCloud应用在一个POD中启动起来,我的回答是:千万不要。
先不说这种部署方式是否符合公司的部署规范,就从数据库和应用这两个应用需要使用的资源特征来看,我们更应该把他们放到不同的POD中,而不是同一个POD中的不同容器。很明显我们扩容POD的方式和扩容数据库是不一致的,而数据库是应用具备云原生无状态特征的关键,理论上容器化部署MYSQL要比应用程序复杂很多,因此处理数据库的状态不是那么简单。
在整个Kubernetes对象模型中,POD只是一个”逻辑“的概念,笔者在前边的几篇文章中详细介绍过支持容器的Linux提供的内核机制:7种命名空间,文件系统,和CGroup来做资源限制。
而Kubernetes最终落到每台宿主机上,处理的还是命名空间,CGroup和文件系统,这是决定容器边界的机制,并且容器在操作系统上就是一个被施加了特殊命名空间和资源限制的进程,操作系统上其实并不存在POD这个进程,这是逻辑一词的由来。
那么POD是什么,其实笔者在介绍容器的时候就埋下了伏笔,当时我们并没有说透和虚拟机对应的对象是什么。具体来说,POD就是一组共享了资源的容器,POD里的所有容器,共享相同的网络命名空间和接口,并且可以通过声明挂载相同Volume来交换数据。关于存储卷笔者后续会通过专门的文章来介绍。那么POD是不是就是虚拟机的概念呢?其实笔者想告诉大家的是,无论是从原理,资源使用,功能等角度,容器和虚拟机没有太多的相似之处。笔者读过很多文章,都在讲容器是比虚拟机更加高效的虚拟化技术,并且还把他们画在对比图的相同层,其实这样的理解,不是太准确。因此也不存在把虚拟机迁移到Kubernetes平台。
如果PDO中的容器共享同一组资源,那么问题就来了,谁应该是第一hold主这些命名空间,而让后来的容器实例能够”加入“?具体是谁比较容器,但是如果按这个逻辑走,容器之间就有明确的依赖关系了,比如在订单中心的例子中,如果订单服务必须先启动,然后是订单取消的异步任务容器,那么我们就必须在启动,扩容和失败重启的时候,严格按照这个关系,很明显这是有问题的,引入了依赖。
而Kubernetes解决这个问题的方法,就是每个POD在启动的时候,依赖一个叫做infra的容器来占住资源,这样其他的业务容器就可以加入进来,这样大家就会共享这些命名空间。
具体来说,假如我们的POD中有两个容器A和B,那么POD在启动的时候,首先会创建infras容器,然后容器A和B通过infra容器建立这种亲密的关系,从这个角度来看,Infra容器要非常的轻量级和稳定,占用的资源也不能太多,在Kubernetes平台中,Infras容器是通过一个叫pause的镜像创建的。
资源具体的位置是:k8s.gcr.io/pause,这个容器是用汇编语言编写,占用的空间在100-200k之间,并且永远处于暂停的状态。通过Infra容器hold主命名空间,容器A和B就建立起来关系,因此如果我们查看这是哪个容器在宿主机上命名空间对应的文件,会发现他们指向相同的命名空间文件(也意味着属于相同的命名空间)。
而运行在同一个比说说网络命名空间的容器A和B,可以享受如下的便利:
- 和Infra容器共享网络设备
- 容器A和B可以通过localhost来直接进行通信,效率得到了极大的提升
- 由于IP地址是基于POD,因此容器A和B共享IP地址
- POD的声明周期只和Infra容器相关,和容器A和B没有直接的关系
因此,我们在自己的实际项目中,上云或者上Kubernetes工作的核心,是深刻理解容器的本质,进程这个概念。我们在物理机和虚拟机上部署应用程序,进程会受到systemd或者supervisord的管理,而在容器模型下,一个容器中只有一个进程,而POD只是扮演了资源分配的这个虚拟机的角色。
而POD这个逻辑的概念,更多是和编排相关,如果我们把POD理解成资源分配的”虚拟机“,那么镜像就是这个虚拟机上启动进程的exe文件,我们可以定义initContainer来进行初始化操作,并且定义多个不同的初始化容器,并给他们安排具体启动的顺序,这就编排的具体体现。
好了,今天的内容就这么多了,通过POD的三篇文章的学习,笔者希望大家能够不光知道如何部署应用到Kubernetes集群,也能够知道背后的原理。
接下来,我们具备足够的基础知识后,我们来看看如何通过声明式的方式,也就是YAML文件的方式,来部署和版本更新我们的SpringCloud应用程序,敬请期待!