笔者最近在和团队沟通如何将应用部署到K8S的时候,有同学提出是不是可以直接把Jar包部署进去,乍一看这个问题的答案应该是可行,实际上忽略了Kubernetes平台产品的根本驱动力,不了解的同学可以看笔者的前边文章。
Kubernetes是为了解决大量基于容器部署的应用管理复杂度而开发的,因此到目前位置,部署到Kubernetes中的应用都是以容器的方式运行,至于说这个容器的平台是Docker还是其他Kubernetes支持的容器选项,这个取决于用户自己,因此我们必须非常确定,目前Kubernetes上的应用都是运行在容器中。
为了顺利将我们的第一个应用程序部署到K8S集群,我们需要对容器先有个深入的了解,这篇文章主要围绕容器这个概念来展开。
【什么是容器?】
在笔者的文章《云计算时代的操作系统Kubernetes介绍》中,笔者介绍了在微服务架构下,如果我们让多个微服务实例运行在同一台机器上,可能会出现版本依赖的冲突,简单说就是微服务A需要版本为1.1的某个jar的依赖,而微服务B由于迭代的比较快,需要版本为V1.2的相同jar的依赖,这样就会产生依赖冲突。解决这个问题的办法是,我们可以给每个微服务提供独立的运行华宁,比如让两个微服务运行在不同的机器上。
当组成应用的微服务数量不多的时候,我们通过给每个微服务提供一台独占的虚拟机看起来问题不大,但是随着团队对微服务进行重构,大的服务会被切分的更小,微服务的数量开始增长,而随着这个数量的增长,我们总会触及到一个turning point,我们已经无法在硬件资源和微服务需要的虚拟机数量之间达成平衡。
通过大量的虚拟机实例来部署微服务实例不光需要大量的硬件资源,这些虚拟机需要管理和维护,这就意味着我们部署的虚拟机数量越多,需要的系统运维和管理人员也越多。随着微服务架构的流行,部署上百个微服务实例已经司空见惯了,为了能够应对数量越来越多的微服务架构部署场景,我们急迫的需要虚拟机这种部署方式的另外一种选项,而容器化部署方式就是另外一种选项。
如果你从来没有接触过容器,你可能会问,容器化的部署方式和虚拟机部署方式有什么不同?容器化部署方式给应用提供了进程级别的隔离,由于这种进程级别的隔离具体的实现方式,因此容器化部署比起来虚拟机,需要的额外资源更少。反过来看,也就意味着我们可以在一台机器上部署更多的应用,资源的使用率因为密度的增大,也得到了提升,从管理的角度,费用也相应的降级,简直是多赢啊。
在容器化部署模式下,应用的实例其实直接运行在宿主机上,只是这些运行应用的进程被做了特殊的处理,因此每个应用需要的额外开销就很少,比如每个应用不需要有自己独享的操作系统,因为所有容器都共享宿主机的操作系统内核,也不需要为操作系统上的系统进程花费额外的开销,因为这些也都是共享宿主机操作系统。
虽然说应用的容器进程都运行在同一台宿主机上,但是如同笔者刚才提到的,这些进程被进行了特殊处理,因此容器进程其实有自己隔离环境,虽然说这种隔离环境比起来虚拟机,要弱很多。具体来说,运行在容器中的应用程序,通过施加了障眼法,会觉得自己是这台机器上的唯一一个进程,具体这个障眼法是如何施加的,我们后续有专门的讨论。
为了让大家更加清晰的了解容器是运行在宿主机上的一种特殊进程这个概念,笔者准备如下的示意图:
如上图所示,使用虚拟机部署的方式,考虑到硬件成本的问题,我们其实很难为每个微服务实例都创建一个独享的虚拟机,而是将多个服务打包部署到同一个虚拟机实例上。但是由于容器进程这种方式几乎没有额外的开销,那就意味着我们可以为每个服务的实例都创建一个独享的进程,事实上,我们不应该将多个应用部署到同一个容器中,这会造成管理容器中的多个应用的进程变的异常困难,还记得我前边说过每个进程都是自己独享的容器中的1号进程,如果我们在一个容器中部署两个应用,那么会有两个进程在一个容器中,而不论是谁先启动,都具备进程历的能力,因为这是操作系统的能力。
因此所有基于Kubernetes部署的应用程序都必须遵守一个基本的规则,一个容器中只运行一个程序(一个进程)。
除了节省资源之外,容器比起虚拟机来说,启动的速度更快,因为不需要初始化操作系统等这些额外的工作所占用的时间,容器只要将应用启动起来就可以了。
最后,从隔离级别的角度来看,容器的隔离级别比起虚拟机要差很多。当我们在虚拟机中运行应用程序,每个虚拟机都有自己独享的操作系统以及内核,而虚拟机通过硬件虚拟层和宿主机进行交互,宿主机将自己的硬件资源划分为一个一个的小块,供虚拟机使用,如下图所示,运行在虚拟机中的应用程序如果要执行系统调用,会先将调用请求发给虚拟机的内核,虚拟机的内核将要执行的执行通过硬件虚拟层传给宿主机来执行,然后将结果返回给虚拟机内核,然后返回给虚拟机中的部署的应用程序。
对于容器来说,如果要进行系统调用,请求直接发给进程运行的宿主机的操作系统,而所有运行在宿主机上的容器都共享同一套CPU和内存,并且这种模式下,由于没有虚拟化的额外开销,执行效率会更高。
我们继续通过下图来比较分别运行在宿主机上,运行在虚拟机上,以及运行在容器上的三个应用实例的区别。
我们从左到右一次来分析,最左边的部署方案是三个应用实例运行在同一台机器上,未做任何隔离,因此三个应用的实例会使用相同的操作系统内核。在中间的部署方式中,应用程序运行在不同的虚拟机中,因此应用之间可以做到相对彻底的隔离,比如说运行在虚拟机1和虚拟机2中的应用,使用的操作系统内核可以不一样。
最右边是容器部署的场景,虽然说三个应用实例使用相同的操作系统内核,但是由于在进程级别做了修改,因此三个进程相互之间是隔离的,从这三个进程的角度看,他们甚至都不知道对方的存在。这种进程级别的隔离能力由操作系统内核提供,每个应用程序只能看到部分硬件资源,并且认为自己是这台机器上唯一运行的进程。
由于在容器部署模式下,所有的应用的容器进程都运行在宿主机,这些进程共享操作系统内核,因此会有安全隐患。比如说如果宿主机的操作系统内核版本有缺陷,那么运行在一个容器中的应用就可以利用这个缺陷来读取另外一个容器内存中的数据。而反过来看如果这些应用分别运行在不同的虚拟机中,虽然说这些应用也是运行在同一套硬件上,但是暴露给虚拟机中运行应用的攻击面就非常有限。当然如果我们对安全的要求非常高,那么只能将不同的应用运行在独享的硬件中了。
更进一步讲,容器部署模式下,每个应用的实例共享内存空间,而虚拟机模式下,内存被切分成多个部分,每个虚拟机独享分配给自己的那部分。因此,如果我们不对容器使用的内存资源做任何限制,那么一个贪婪的容器可能会造成运行在同一台机器上的其他应用出现内存不足,进而被操作系统swap到磁盘上。这种情况只能出现在比如说Docker这种容器平台上,Kubernetes集群上,节点在加入到集群的时候,会有初始化工作,其中的一部就是关闭swap out功能,很次在Kubernetes集群上,在内部不足的情况下,要不然被当做牺牲者杀掉退出,要不然就继续运行,不存在swap到磁盘上的情况。
虚拟机功能是通过CPU的虚拟化功能和专门的虚拟化软件来支持,而容器是直接由操作系统的命名空间,Cgroups和文件系统的切换运行目录来支持,笔者会在后续的文章专门介绍支持容器技术的三个支柱(three pillars),稍安勿躁!
【Docker的运行机制】
笔者前边的文章反复的强调,Docker从来都不是Kubernetes平台上的容器标准,Kubernetes着眼于提供一套公开的标准,所有符合这套标准的容器实现,都可以无缝接入到Kubernetes集群中。从这个角度来说,我们其实并不是非得要学习Docker的知识。
但是啊,容器这种技术其实已经出现很久了,直到Docker的出现,才让容器这种轻量级的隔离技术进入到聚光灯下,而Docker是第一个让容器技术可以在不同的操作系统环境下容易的运行起来的平台,特别是被称作是杀手锏打包技术,可以说直接把同时代的竞争者给秒杀了。
Docker提供的这种打包技术,解决了困扰大规模集群部署领域存在已久的一个问题,如何解决应用程序依赖的问题。比如我们开发的是一个Java的应用程序,那么我们需要依赖于JVM运行,JVM有不同的版本,而不同的版本底层又需要不同版本的操作系统支持,另外,编写java代码需要依赖很多第三方的库,这些库也构成了依赖。
传统的打包方式,比如说war包和jar,我们其实是将代码和代码的所有依赖都打到一个包里,这样java程序在运行的时候,只有JVM(和隐含的操作系统)这一个依赖了。而Docker的打包方式就非常激进了,它直接将应用运行的所有环境,包括jar包依赖,jvm和操作系统都打到一个包里,这样,无论是在哪里运行,只要有Docker的基础设施, 应用程序完全自包含, 几乎不需要外部的任何依赖了。
这种打包方式就非常厉害了,而Docker更进了一步,将打包后的镜像进行了分层处理,并且提供了镜像的仓库,这样就解决了需要每次都打包操作系统的痛点,并且镜像可以共享,极大的促进了Docker的风靡。
我们介绍Kubernetes的话,肯定需要介绍容器,考虑到目前使用Docker的人还是主流,并且通过仔细的分析Docker的机制和原理,除了理解Kubernetes具体解决了大规模容器运维的痛点之外,也能帮助我们理解CRI这个规范提出的目的,因此我们还是需要花点时间介绍一下Docker,为后续的内容打下基础。
Docker是一套打包,分发和部署运行应用程序的平台,就如同笔者前边的介绍,我们通过docker提供的工具,可以将自己的应用和所有的依赖(包括操作系统哦)打包成镜像,并且可以对镜像进行版本控制。打包好的镜像可以上传到公共的或者私有的镜像仓库中,这样其他的环境中的机器就可以从仓库中拉取镜像,在本地运行应用程序。如下图所示:
下面我们来详细的介绍一下组成Docker的三个重要的概念:
1,镜像,容器就想是我们通过Docker提供的工具,将应用和应用的依赖进行打包后的软件制品。镜像和我们在电脑上把多个文件压缩成zip包类似,包含了应用程序,应用程序的配置信息,比如启动入口,运行起来监听的端口,应用程序的依赖,以及整个操作系统的文件系统目录(注意没有操作系统内核哦)等。
2,仓库,镜像仓库是我们可将自己打包好的软件镜像上传到一个中心仓库,分享给供组织内部的其他开发人员或者全世界各地的开发人员使用。我们可以通过docker push将本地打包好的镜像推到“对应”的仓库,而在目标部署机器上,我们可以通过docker pull来拉取对应版本的镜像。仓库的地址可以配置,基于企业镜像的安全和可见性需求,企业可以自建Harbor仓库来保存和分享内部可见的仓库,也可以将镜像推到Dockerhub,供更大群体的人使用。另外仓库提供了权限控制,你可以将自己的镜像注册为public或者private,来控制对那些人可见。
3,容器,容器是镜像在目标机器上运行起来的实例。容器和镜像的概念类似于类的实例和类的定义,有面向对象编程经验的同学应该很好理解。运行中的容器镜像在宿主机上其实就是一个被特殊处理过的进程,但是这些进程之间可以做到相互隔离,并且和宿主机进行了隔离。特别是容器实例的文件系统由于采用了联合挂载的技术,我们可以讲多个镜像的层统一挂载到一个挂载点,这样当容器运行起来的时候,看到的就是一个完整的包含操作系统文件夹结构的文件系统,并且和宿主机进行了隔离。容器实例可以通过启动参数来对使用的资源做约束,比如限制容器实例可以使用的CPU和内存的资源最大值,避免恶意的容器将整个宿主机能提供的资源都给吃光,其他的应用会由于资源不足而退出。这里需要提一个细节是,Kubernetes上的容器会进行优先级管理,当我们在编写YAML文件的时候,尽量在resource部分提供资源的request和limit部分,这样的话,Kubernetes会给启动的容器实例分配Guareeted的级别,处于这个优先级的容器实例,在资源紧张的时候,是能保证不会被作为牺牲者来腾出资源的。
【Docker镜像是如何构建,分发和运行?】
为了让大家能更好的理解Docker的三个支柱,笔者准备了下边的几张图,来详细的介绍打包,仓库和镜像之间的关系,我们先从如何构建镜像开始:
开发人员可以在本地运行:"docker build -t qigaopan/nanhang-rei4j-sample:v1.0 ." 这样的命令,将应用程序和依赖一同打包进镜像,在本地打包好镜像后,开发人员可以将打包好的镜像推送到私有或者公共的镜像仓库,详细的图示如下:
开发人员在本地可以运行:“docker push qigaopan/nanhang-rei4j-sample:v1.0” 将本地打包好的镜像推送到仓库中,这样任何有这个仓库访问权限以及镜像访问权限的客户端和用户,都可以轻松的通过docker pull来拉取对应版本的镜像,在本地或者目标机器上运行应用程序,如下图所示:
通过这种方式,我们就可以在任意安装了Docker的机器上,来运行我们的应用程序了。由于Docker提供的这种开创性的打包方式, 解决了PASS平台由来已久的一个痛点,如何打包才能顺利部署到目标环境的问题。Docker在pull镜像到本地的时候,也做了极致的优化,因为Docker本身是分层的,因此如果你要拉取的镜像某些层在本地已经存在,那么就不需要下载了,可以加快启动速度和节省网络带宽,一举多得啊。
另外,在Kubernetes中,当Kubletes驱动镜像运行时启动应用的时候,会先看本地的缓存是否已经有对应的镜像,并且这个本地缓存的尺寸也是调度器会参考的因素之一,当然我们可以在YAML文件中制定镜像拉取的策略,默认是Always。
好了,今天的这篇文章就这么多了,下篇文章会详细介绍Docker打包的镜像分层机制, Kubernetes支持的其他容器选项,敬请期待!