TOC

容器

    容器是一种基础工具,泛指任何可以用于容纳其他物品的工具,可以部分或完全封闭,被用于容纳、存储、运输物品;物体可以被放置在容器中,而容器则可以保护内容物。

主机级别虚拟化

    它是虚拟整个完整的物理硬件平台比如vmware,可以自由的安装操作系统,安装的操作系统甚至可以和我们底层的宿主机不同的操作系统;

主机级虚拟化类型一
VMware ESXi
    直接在硬件上安装hypervisor虚拟机管理器,相对于类型二来讲,无需在硬件硬件之上安装一个宿主机操作系统,而是直接安装然hypervisor虚拟机器管理器,后在hypervisor之上安装使用虚拟机,也就意味着没有任何主机是跑在硬件之上的,所有操作系统都跑在虚拟机内部;
主机级虚拟化类型二
VMware、VirtualBox
    有一个物理机设备,在物理机之上首先需要安装一个主机操作系统,我们称之为Host Os也称之为宿主机操作系统,在宿主机之上安装一个VMM,虚拟机管理器,在这个管理器再创建管理虚拟机;
主机级虚拟化总结
    以上两种虚拟化的实现,首先得有底层的硬件平台,先不论在硬件之上是否有一层Host Os,但是这个虚拟机管理器的软件虚拟出来的环境,应该是一个虚拟出来的一个 独立的硬件平台,因此用户要使用虚拟机就需要在这个虚拟机之上自己去部署,一个完整意义上的操作系统,这个完整的操作系统是指的我们需要在上面安装一个带有内核空间和用户空间的操作系统,运行内核不是主要目的,内核的核心在于资源分配和管理,所以在真正在用户空间跑的应用程序才是能产生生产力的,比如需要提供一个web服务,内核不会这样去提供,我们需要借助用户空间来运行相应的进程来实现,必须我们常用的Nginx、Httpd都是运行在用户空间的,所以真正能产生生产力的应该是用户空间的应用进程而不是出于通用目的而设计的资源管理平台;
    但是内核又不能没有,原因在于现代软件都是基于系统调用和库调用研发的,我们要想在这一组硬件平台之上使用软件程序,就必须借助内核,不安装内核,不安装库,就没办法运行应用程序。假设我们创建一个虚拟机的目的就是为了运行一个web应用程序服务器,为此我们不得不安装内核,安装用户空间,而后去跑web程序,这样来看代价似乎有点大,它需要实现两级调度和资源分派,第一 自己的虚拟机有内核,这个内核就实现了一次内存虚拟化、CPU调度等。。。但是真正的虚拟机本身也是运行在我们的宿主机的用户空间的,那么就意味着还要经过一层宿主机的系统调用才能完成整个调度事件,这样看来资源的浪费就太大了;
    因此这种传统的主机级虚拟化技术,的的确确能让我们在一组硬件平台之上实现跨系统环境的隔离等各种需求,但事实上它带给我们的资源开销也是不容忽视的,而很多时候,我们创建虚拟机的目的,仅仅是为了运行一个或几个负有生产责任的进程而已,为此付出的代价就太大了。
    那既然如此,减少中间层,减少中间环节,就是一个很好的提高效率的方式,比如我们把虚拟机的这层内核拿掉,只保留用户空间,但是这么一来我们就会发现一个问题,我们加虚拟机的原因就是为了需要达到资源隔离,虚拟机内部的程序遭受到木马,也不会影响到其他的虚拟机、宿主机的资源,最多损坏了这个虚拟机的环境而已就,所以隔离才是我们需要追求的目标。但是我们如果将虚拟机这一层的内核空间拿掉就会带来跟多问题,比如我们如果我们要运行两个nginx他们都得监听80端口,大家都知道一个主机上是不可以创建两个80的套接字的。因此我们虽然拿掉虚拟机的内核,但是又不能让它重新回到非隔离环境,彼此之间互相不可达,互不干扰,只不过他们共享同一组底层资源而已。

容器级虚拟化

    我们有一组硬件平台,在这一组硬件平台之上提供虚拟隔离环境管理器,创建一个又一个的隔离环境,然后我们要运行在隔离环境的进程就运行在这个隔离环境内部,因为进程是跑在用户空间的,因此我们内核提供的是内核空间进程应该运行在用户空间,而我们现在需要进程运行在隔离环境中,其实我们隔离的就是用户空间,按道理来讲我们的用户空间只有一组,因为他们在一个内核之上,但是我们现在期望实现的是将用户空间隔离成多组,彼此之间互不干扰,一个空间只运行一个或部分进程。
    随后我们启动进程时,让进程启动并运行在用户空间当中,在众多用户空间就可以共享底层同一个内核,但是在自己运行时间所能看到的边界却是用户空间的边界,所以彼此之间就进行隔离了,这个用户空间用来放进程的,给进程提供运行环境,并且还能够保护其内部的进程,不受其他进程的干扰,给我们提供一个安全运行的隔离环境,所以它叫做容器。

    容器技术最早是出现是freebsd上面,当年叫做jail,目的就是运行一些进程不受其他的干扰,实现安全运行,它提供的一个环境就好像一个沙箱一样,进程有异常行为,也不会影响自己所属这个容器的外围的其他的进程,这种隔离带给我们是一种安全运行的目的,如果一个服务进程被远程的客户端所劫持,人为破坏,那这个破坏的程度也就仅仅只能到达容器的边界,无法对外部的进程有威胁,所以最初就出现jail的目的就是为了应用的安全运行。
Chroot(vserver)
    后来有人将jail这种技术,复制到了Linux平台,起名为vsever,这个vsever在一定程度上也能实现jail的效果,其实vsever背后所用到当时主流的所能实现的功能就是chroot,真正根应该是我们文件系统的跟,假如我们在一个子目录下也创建一个FHS定义的发行版应该具有的根下的子目录结构使用chroot就能够把这个子目录当根一样使用,随后在里面运行进程,进程就会把它当为根。其实从这个角度来讲,它就是一个单独的jail,这就是切根。
    不过呢这并不能真正实现它与我们宿主机真正特权用户空间和其他用户空间的彻底隔离,因为chroot它所隔离的仅仅是看上去的这种空间,他们底层是同一个内核,隔离出来这个进程到底是运行特权模式还是非特权模式,如果需要访问一些特权资源应该怎么去指派,所以表面上看来只是chroot其实背后是一堆技术的支撑。
Namespaces
    一个所谓的用户空间,它的主要目的是实现隔离环境,而后任何进程运行在这个用户空间当中,他就以为是运行在唯一运行当前用户空间之上的进程。但是一个用户空间看到应该有这些组件,第一主机名和域名、第二根文件系统、第三IPC进程间通信专用通道,每个用户空间都应该有这些东西,可以试想一下,如果两个用户空间可以通过IPC进行通信,那么隔离意义就不存在了,当然如果在同一个用户空间可以今天通信那是正常的,而夸用户空间,那就不是真正的意义上的隔离了。
    所以当我们隔离的了之后,对这个新的用户空间来讲,既然认为自己所属的这个用户空间来讲是当前唯一的,那就需要给他们一个假象,要么自己的init要么自己属于init,因为进程都是由父进程创建的,子进程要销毁,那么父进程还需要对其进行收尸,否则这个用户空间的进程将无法被管理。所以在每一个用户空间当中,既然要独立管理那都应该有自己的init,事实上对我们的系统来说init只有一个,于是我们就需要对每一个隔离的用户空间创建一个假的init,要么就是init要么从属于init,或者说要么在这个用户空间只能运行一个进程,要么就得有一个init的进程去管理你要运行的进程。
    所以这就意味着,它得有自己的独立的进程树,PID是互相隔离的,1号进程一定是指的init,在一个内核之上真正pid是1的只能有一个,但现在我们又不得不为每一个用户空间去伪装一个pid为1的进程,那就不得不把他们隔离开。
    我们运行一个进程,它都应该以某一个用户来运行,那么第一个用户空间和第二个用户空间的用户可能他们的id号是一样的,但名字不一样,就比如说每一个用户空间应不应该有root,但是在一个内核上也只有一个root,如果都是root那就麻烦了,root可以随意删除别人的用户空间,那隔离就没有意义了。所以就必须在每一个用户空间伪装一个root出来。真正回到宿主机的系统上它依然是一个普通用户,所以从这个角度来讲,用户、组也需要隔离。
    因为每一个用户空间都以为自己是这个系统上唯一的用户空间,他们都应该有自己的ip地址,都可以去监听一个80端口, 如果不能实现这种效果,那隔离的意义就不存在了,所以从这个角度来讲,每一个用户空间就想一个虚拟机一样,它应该能看到自己专用的网络接口,有自己专有的TCP/IP协议栈,有自己专用的网络。而且更重要的是,两个容器之间可能还需要互相通信,因为我们每一个用户空间都以为自己是这个网络中独立运行的计算机,那么很显然两个具有隔离意义的容器应该就是两个计算机,他们可以通过网络通信,而在内核级,TCP/IP协议栈可只有一个的,但现在需要给每个用户空间创建一个专用的,因此,从这个角度来讲,也需要在内核级进行隔离,独立的用户空间也应该有自己的一套TCP/IP协议栈。

    所以不论是主机名、文件系统还是网络等,都需要进行虚拟化,在后来有这种运行jail或者vserver的需要,开始支持在内核级,直接切分成几个隔离的环境,这每一种资源只要可以切分成多个互相隔离的环境,称之为namespaces名称空间,在内核当中UTS是可以直接以名称空间,直接进行隔离的,也就意味着,在内核上我们可以创建多个名称空间来,而后在这些名称空间之上每个名称空间的UTS等资源进行隔离,每个名称空间都可以有自己独有的主机名、文件系统等。让他们可以分别各自使用,而不影响宿主机的名称空间。
    为了支撑所谓的容器机制的实现,Linux内核到今天为止,在内核级一共对六种需要被隔离的资源,在内核级已经通过namespaces名称空间原生支持,并且把这种功能直接通过系统调用,向外进行输出。 所以从这个角度来讲,要想使用容器得靠Linux内核级的内核资源的namespaces名称空间隔离机制来实现,所以到今天为止整个Linux领域所谓容器化就是靠namespaces加上Chroot来实现的;

    所以现在不在使用原先的主机级虚拟化技术,开始抽掉主机级虚拟化技术的每一个虚拟机的内核,让所有的用户空间不再分属于独立的内核,而从属于同一内核,所以这种虚拟化技术叫做容器级虚拟化技术,但这种虚拟化技术有一个问题在于,资源分配,回看主机级虚拟化我们可以在创建的时候就可以定义好了每个虚拟化可以使用多少CPU资源内存资源等等。
    如果不做资源控制那么如果某一个容器被植入恶意程序,导致OOM,开始不断的吞噬系统资源,那么就影响了我们的宿主机系统和户空间的容器,不像CPU一样要使用的时候如果没有可以等待,内存要就必须得有,没有就KILL了。
    所以内核还必须使用一种功能,来限制每一个用户空间中的进程所有可用的资源总量,在整体资源上做比例型分配,也可以在单一用户空间上做核心绑定。如果一个用户空间限制了2G内存,那么这个用户空间的进程需要3G那么这个用户空间的最耗内存资源的进程OOM,不影响其他的用户空间,因为一共就这么多,多一点都不能使用。
CGroups(Control Groups)
    对于CGroups来讲,它无非就是把系统级的资源分成多个组,然后把每个组内的资源量指派给特定的用户空间或进程上来实现资源分配,实现用户空间的资源分配。
Cgroups包括如下几个资源:
    blkio:块设备IO;
    cpu:CPU资源;
    cpuacct:CPU资源使用报告;
    cpuset:多处理器平台上的CPU集合;
    devices:设备访问;
    freezer:挂起或恢复任务;
    memory:内存用量及报告;
    perf_event:对cgroup中的任务进行统一性能测试;
    net_cls:cgroup中的任务创建的数据报文的类别识别符;

    每组CGroups可以以不同的方式给它做资源分配,可以实现对一个系统之上所运行的进程给它分类,每一类给它一个资源组,叫做控制组。一旦我们把一个资源分配给某一个组之后,这个组内的子组可以自动使用这个资源,除非我们单独给它进行分配,否则它将自动拥有分配到这个组的资源使用权限。
    假设我们把一个用户空间当作一个组,那么我们就可以对这个空间的资源使用能力。
事实上容器的隔离程度相比于主机的隔离程度还是差很多的,原因在于所有容器都属于同一个内核,只不过在内核级强行利用namespaces设置了容器的边界,不像主机级虚拟化,所有的虚拟机都拥有自己的独立的内核。因此为了加强这种安全性,为了避免一个用户空间的进程绕过漏洞,进入别的用户空间,所以后来就通过selinux等安全工具,视图加固用户空间或者说容器的边界,所以为了支撑容器技术做得更加完善,可能还需要启用selinux等机制。

容器

    容器技术,是靠当年是jail启发,然后就有了vserver,再往后为了能把这种容器技术做得更加应用,然后集成到内核,需要写代码去调系统调用clone()、setns()、unshare()来创建容器,但是没多少用户有这些能力,所以就把这些使用容器技术的功能做成了一组工具,能极大的简化用户的使用麻烦程度,于是就有了一个解决方案LXC
LXC,LinuX Container
    它是除了vserver以外最终把完整的容器技术,用一组简易的工具和模版来极大的简化容器技术使用的一个方案,所以它把自己叫做LinuX Container,lxc-create可以使用这个命令快速创建一个容器,我们通常把它称之为一个用户空间,有最基本的/bin,/sbin等这些目录结构,也可以装上了一些基本的应用程序,可以从根用户空间去复制,但是因为我们底层根的用户空间是centos,那么想创建的用户空间的是Ubuntu的用户空间,那么就无法从根用户空间去复制了,所以就有了template,这个template就是一组脚本,这个脚本执行的时候,会自动的去执行安装过程,这个安装主要是指向用户所想创建的系统用户空间的系统发行版所属的仓库,从仓库中把各个应用程序下载下来,而后安装并且生成新的名称空间,然后chroot进去,于是这个名称空间就像虚拟机一样可以被使用了。
    所有的名称空间都基于这种方法实现,LXC就使用了这一组工具,帮用户快速的实现了创建空间,利用模版完成内部所需与的各种文件的安装,同时安装完成之后自动chroot切换过去,于是整个用户空间就跑起来了,每个用户空间和我们的虚拟机没什么两样里面的各种文件、用户账户、主机名等什么都有,可以安装程序启动服务都可以。
    但是依然有很多是门槛,需要理解LXC的各种工具,必要的时候还需要去定制模版,更重要的是每一个用户空间都是安装生成的,我们在里面后来运行当中生成了很多数据文件,那么这个宿主机在将来出现故障之后想迁移到其他的主机上去,就不是一件容易的事了,所以从这个角度来讲LXC虽然极大的简化了容器的使用,但事实上比我们过去使用虚拟机来讲,它的复杂程度从某些程度来讲没有太大的降低的,更何况它的隔离性也没有那么好,批量创建名称空间也很难,当然好处在于,它能够让每一个用户空间的进程,直接使用宿主机的性能,中间没有额外开销了,资源方面的节约做得很好,于是后来就出现了Docker。

Docker

    从上面来看,可以理解为我们的Docker就是LXC的一个升级版,它去创建容器、启动容器、销毁容器底层还是借助于LXC的工具,Docker其实也不是什么容器,而只是容器技术的应用前端工具,容器是Linux内核中的技术,它只是把这种技术的使用做得更加简化。
    LXC想大规模创建是很难实现的,想在另外一个主机上复制一个一样的容器节点,也很难,那Docker就开始在这方面着手找解决方案了,其实Docker早期的版本,其核心就是一个LXC,它是LXC的二次封装发行版,利用LXC做容器管理引擎,但是在创建容器时,它不再是用模版去现场安装生成,而是它事先通过一种叫镜像的技术,把一个操作系统用户空间所需要用到的所有组件,事先编排好,编排好之后,整体打包成一个文件,我们称之为镜像文件(image)这个镜像文件是放在一个集中统一的仓库中的,这个仓库里面有各个指定发行版系统所要运行起来的用户空间基本结构的一个镜像文件,比如最小化的Ubuntu,如果很多时候都会用到nginx,那么这个仓库里面也可以有一个基于最小化的Ubuntu构建的nginx构建的镜像,以后当有人想要一个nginx直接把这个nginx镜像啦下来就可以直接启动了。
    所以,当Docker使用create命令的时候,并不是利用template去安装,而是自己去docker官方镜像仓库里面去下载一个匹配度最高的image,拖到本地,并基于镜像启动容器,所以docker极大的简化了容器的使用难度,以后想用docker启动一个容器时候,我们只需要使用docker run就结束了,它自动连接到互联网是docker仓库里面下在下来,直接启动。
    在面前说一个用户空间当中我们可以尝试运行一组进行,或者一个进程,Docker还采用了另外的一种方式,为了使得容器更加易于管理,在一个容器内只运行一个进程(core process),容器启动进程开启,进程结束容器停止,比如需要在两个程序运行一个nginx一个tomcat,那么用docker的理念做这个事情就是两个服务分两个用户空间(容器),二者使用容器之间的通信逻辑来通信,所以一个容器只运行一个进程,这的Docker的方式。而LXC是把一个容器当一个用户空间来用,可以运行N个进程,所以就使得将来在容器管理的时候就极为不便,而Docker这种方式一容器一进程,使得隔离做得更好。
    那么就带来另一个问题,当我们想要调试程序的时候,你得进入这个用户空间,然后才能进程应用程序的调试,所以容器这个东西给运维带来了不便利,但是给开发带来了极大的便利,因为分发容易了,开发一款软件,可能需要考虑到各种平台x86平台x64平台centos平台Ubuntu平台,但是Docker能做到一次编写,到处运行,无论的windows linux 还是mac只要有docker就能跑起来。
    所以在于运维这块发布操作可以借助Docker编排工具来实现,其实Docker必须要靠编排工具来编排的一个容器,如果没有编排工具的话,手动管理其实比直接管理应用程序更麻烦,所以增加了运维管理的复杂度,但是确实是降低了程序员开发时的复杂度,如果应用程序出现故障是比较麻烦的,因为容器内部没有调试工具,但是这样换来的好处,就是他们确实是隔离的,还有更大的好处就是批量创建,对于docker来讲,批量创建就是一个镜像到处run即可,如此简单就可以实现分布式节点。
    docker的镜像构建底层是通过实习分层构建联合挂载的机制来实现的,意思就是说,当我们做镜像的时候先做一个底层的基础镜像比如centos最纯净的一个镜像,随后想使用一个nginx,不必从头建一个nginx镜像,而是直接基于这个基础的centos镜像装一个nginx然后打包,然后就是一个nginx镜像了,其实他们的分层的centos是一层,nginx是一层然后把他们叠在一起,形成一个统一视图,实现了联合挂载,就像在一个centos上有一个nginx,这样的一个好处在于以后镜像分发,就没那么庞大了。镜像每一层镜像都是只读的,如果想要可读可写那么就需要在只读层上面再挂载一层新层。
    真正是使用容器时,我们不会在容器本地保存有效数据,如果有要打算持久化的数据,我们可以在容器内部挂载一个共享存储,一般我们需要一个共享的持久存储。所以我们就可以把容器当作一个进程,容器终止进程删除,数据依然保留,然后重新创建容器数据依然可用。由此容器就有了生命周期这么一说了,从创建而开始,从停止而结束。更重要的是,它和我们的宿主机主机是没有任何关系的,可以运行在任何一个主机之上,所以随后哦我们就可以实现这种功能。
    当我们需要启动一个nmp的时候,那么是n先启动还是m先启动,他们之间需要依赖关系,这就需要在docker的基础只是能够把应用程序之间的依赖关系,从属关系等等反应在启动关闭时的次序管理逻辑中,这种工具叫做容器编排工具。
    后来docker社区越来越强大了,就脱离了LXC,自己研发了一个容器引擎叫libcontainer,所以现在版本当中已经没有了LXC,但是后来被CNCF挟持,为了docker在容器领域当中未来的话语权,CNCF有一个大的组织,另起炉灶,后来CNCF定义了各种Docker标准化,最后CNCF基于这个标准化,开发了替代的libcontainer的一款容器引擎,叫做runC,现在我们用的新版docker都是基于runC底层实现的,这也是一个容器运行时的环境标准,遵循OCF标准;
   LXC->libcontainer->runC
容器编排工具
    machina+swarm+compose:compose是单机编排,只能编排一个docker宿主机之上的容器,swarm实现多主机编排,实现多主机编排;
    mesos+marathon:mesos实现统一资源调度分配,marathon实现编排容器;
    kubernetes:google研发的,google在docker出世之前已经使用容器十多年,所以等docker出来后将其十几年的使用经验放到了docker的kubernetes编排系统上面,因此在现今来看以上的两种组合基本都更换成了k8s;

Docker组成三部分

    docker daemon、docker client、docker registry
Docker Hub
公共仓库:自建仓库,Docker registery
私有仓库:官方所提供的仓库,Docker hub/registery
    一个docker registery拥有两重功能,第一提供镜像存储的仓库,第二提供用户获取镜像的认证,同时还提供当前registery所有的镜像的搜索索引,在一个registery上仓库有很多,一个仓库有仓库(repository)的名称,一个仓库通常只用来放一个应用程序的镜像,nginx作为一个镜像仓库那么nginx作为仓库名,镜像名我们称之为tag它的组成部分上仓库门+版本号,比如nginx:1.0.1。
Docker依赖环境
    64 bits CPU
    Linux Kernel 3.10+
    Linux Kernel cgroups and namespaces

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注