容器系列之docker从零开始
LXC 常用的虚拟化技术
- 主机级虚拟化
描述:虚拟整个完整的硬件级平台,如vmware像是一个裸的虚拟机,可以在上面安装操作系统,可以安装不同的操作系统
* Type-I:
描述:直接在硬件上安装一个虚拟机管理器hypervisor,在它上面直接安装虚拟机,在硬件上不用安装宿主机操作
系统,直接上hypervisor,在hypervisor上安装使用虚拟机
* Type-II: vmware workstation vistualbox
描述:有宿主机,有物理机设备,在物理机上先安装一个主机级别操作系统host os,在宿主机上安装一个VMM,虚拟机管理器,在软件之上再创建管理虚拟化
实现机制:有底层的硬件平台,不管VMM下面有没有host os,但是虚拟出来的场景,是一个独立的硬件平台,用户要使用虚拟机就要自己在上面部署完整意义上的操作系统,在上面安装内核,内核上有用户空间,用户空间上跑进程,内核的作用是资源分配和管理,真正产生生产力的是用户空间中运行的程序
分析:内核是必备的,使用用户空间上面有很多用户进程,最后还是要使用内核来协调,假设创建虚拟机的目的就只有一个,为了运行web服务器,为此不得不安装内核,安装用户空间,这样代价有点大.
一个资源运行需要两极调度和分派,第一自己虚拟机有内核,也有虚拟化,CPU调度,以及I/O的调度,真正的虚拟机调度也是被内核所调度和管理,传统主机级别的虚拟化可以实现跨平台级别的虚拟化,隔离和实验,带来的资源开销也是不容忽视的,但是实际生产上只是为了一个或几次有生产责任的进程而已.
所以减少中间层,就是有效的提高效率, 如要提高效率,可以把虚拟机中间层的内核去掉,只保留进程,但是这样产生问题,因为创建虚拟机的目的是缓解隔离,如果要提供两个nginx都要监听两个80端口,一个主机上并没有两个一样的套接字,在虚拟机可以做到很好的资源隔离.因此,真正抽去内核,而也不能让它真正的影响
同一个硬件平台,提供一个虚拟的隔离环境管理器,创建一个个的隔离环境,要运行隔离出来的进程就运行在这隔离的环境中,进程是运行在用户空间,内核是内核空间,要进程运行在一个隔离的环境中,实际隔离的是用户空间,将用户空间隔离成彼此间互相不干扰,一个用户空间内只运行一个或部分进程,无论怎么隔离都会像xen或者kvm一样还是有一个会有特权的,通过它来管理其它的用户进程,启动进程时,让进程启动并运行在用户空间中,可以共享同一个内核,但是在自身所看到的边界是自己运行时所看到的用户空间的边界,但是这种隔离没有主机虚拟化隔离这么彻底,而且用户空间是给进程提供运行环境的,而且还能够保护其内核进程不受其他进程的干扰,这就是容器技术.
历史:最早是叫jail,为了安全,慢慢搬到vserver,一定程度上也能实现jail的功能,其实vserver后面主流的功能是chroot,也就为了实现切根,真正的根是文件系统的根,假如在一个子目录下也创建一个FHS定义的发行版应该具有的根下的子目录结构后使用chroot就能够把那个子目录当作根来使用,所以在里面的里程也以为它就是根,从这个角度看,它就是一个单独的jail,不过它并不能真正实现与宿主机特权或者是其他用户的隔离,因为chroot隔离也就是看上去的空间,因为底层还是同一个内核,隔离后是特权还是普通模式,资源应该如何分配等,表面看上是chroot,后面是一堆技术的支撑。
真正要实现隔离至少在一个单独的用户空间中,任何进程运行在用户空间中,它为认为自己是唯一运行在当前内核上的用户空间的进程,而且里面所看到的所有进程也是这样的进程,要用主机名,根文件系统也要有,每个用户空间都要有自己独立的根文件系统,每个用户空间都要用自己的IPC(进程监控),如果两个进程之间可以通过IPC进行通信,这样隔离就没有意义,IPC可以使用共享内存,一个进程到数据放到一段内存空间中,如unix.sock就是通过内存来实现的,要确保进程之间的IPC是独立的,IPC在同一个用户空间是可以的,但是跨用户空间就不可以,其实在底层内核是期望各进程之间中以使用IPC来通信的,现在要把它隔离开来。
只要有独立的用户空间,独立的容器,就要把资源分开,UTS(主机名和域名),Mount(挂载数,文件系统),IPC,在每个用户空间中,每个进程应该会从属于某个进程,因为进程都是由父进程所创建的,如果一个进程没有父进程,应该是用户空间的init,一个系统运行就是两颗数,进程数和文件系统数,对于当前的用户空间,既然认为自己用户空间是当前系统中唯一的,给它一个假象,要么自己是Init,要么自己人属于某个init,否则这个用户空间的进程就无法被关闭,子进程是由父进程创建,所以它的结束,回收也是由父进程来操作,如果init结束了,它会在结束前把它创建的进程进行结束才能离开,所以在每个用户空间都应用有自己的init,事实上对以系统来说,init只有一个,所以只能给每个用户空间做一个假的init,要么就是叫init,要么在用户空间只能运行一个进程,只要这个进程终止,这个用户空间就会消失,否则只要用多个进程,它就同属于一个进程来管理,通常叫做init,因此进程数是使得每个进程,要么从属某个ID号是1的进程,就意味着有独立的进程数PID,故障隔离,1号进程一定是init的,在一个内核之上真正PID是1的只有一个,现在不得不为每一个用户空间对进程去回收,这样不得不把它们隔离开来,把init这种机制创建假象,内核能够识别出来,但是却需要进行伪装,这是PID.
还是就是运行的某个用户的进程都应该使用某个用户的身份来运行,第一个用户空间的ID号可能一样,名字不一样的情况是可能存在的,每个用户空间也应该看到root,但是在一个内核空间上只有一个root,如果每个用户空间都有一个root就变得麻烦,它可以删除别人的用户空间了,这样只能给每个用户空间伪装一个root,实现的效果是在真正的系统上只是一个普通的用户,一旦对于用户空间来说可能把它伪装来ID来0,但是只能在这个用户空间做任何的操作,能于真正的系统来说它还是一个普通的用户,把一个子目录的属主属组都修改成某个用户,然后chroot过去这就和根类似有所有的权限,但是需要把它们让进程看起来像是root,所以用户组这些也是需要做隔离的。
每个用户空间也就像一个虚拟机一样,实际也是有自己的IP地址的,应该可以看到自身的网卡,网络专用的接口,有自己专用的TCP/IP的协议栈,套接字层面0-65535都可以使用,另一个用户空间也是一样的,最重要是两个容器之间也要能实现通信,所以网络也要隔离。
因此在内核级中,UTS,Mount,IPC,PID,User,Net都只有一组,本身在内核设计时都是为了支持单个用户空间的进程,后来为了运行Jail的需要,以是就在开始在内核级进行虚拟化,每种资源在内核级可以切分为能够互相隔离的环境就叫做名称空间,在内核中UTS是可以以名称空间为单位进行隔离的,也就意味着可以把在同一个内核之上创建也多个名称空间,在名称空间上放UTS,每个名称空间之间做隔离,所以每个名称空间都有自己独立的名称,主机名和域名是内核级别的,所以一个主机也只能有一个主机名,现在要让每个用户空间都要用自己独立的,而且和其他的名称空间不一样,内核在内核级是隔离的,可以分别各自使用不会影响主的特权的地位。
事实上mount也是一个名称空间,把实际挂载的文件系统切分成多个,每个都不会相互干扰,IPC也是直接在内核级切分成多个,每个进程都可以使用IPC,但是跨边界不可以,为了支持容器的实际,linux内核级对UTS,MOUNT,IPC,PID,USER,NET这6种要被隔离的资源,通过名称空间机制源生支持,并且把这种功能通过系统调用对外进行输出,创建进程使用clone()这个系统调用,把进程放入某个名称空间中setns(),setns()相当于在内核中起了一个进程,这个进程可以放到容器中,把进程从容器中取出是unshare(),所以要使用容器就要靠linux内核级的资源用于支撑用户空间内核级的名称空间隔离机制,所以到现在整个Linux的容器技术就是靠内核级UTS,MOUNT,IPC,PID,USER,NET的6个namespace加下chroot技术进行实现
Linux Namespaces
描述:User是在内核3.8后才加入,所以要很好的使用容器技术,要在内核3.8后才可以
容器虚拟化
描述:现在不使用主机级的虚拟化技术,开始抽掉主机级虚拟化技术中每个虚拟机的内核让用户空间不在分属于一个内核,而是同属于一个内核,存在一个问题,主机级虚拟化在创建时可以定义创建几核,多大内存等,物理机可以使用32核心,但是创建是使用2核心,也就是创建时就做了天源的限制。
在容器中,名称空间是工作在内一组内核中,如果里面的进程内存泄漏了,不断的吞食内存,最终把整个系统的内存都食掉了,也会影响其他的用户空间,CPU也是一样逻辑,内部起一个死循环之类的,使用得其它的进程连CPU都得不到,CPU属于可压缩型资源,还好些,得不到CPU就一直挂着就可以了,内存就不行,一个进程占用所有内存,其它的内存一申请就没有,结果就会导致OOM,因内存消耗尽而被kill,因为内存属于非可压缩型资源,要就必须有,没有就瓜掉了。
所以内核级要实现一种功能来限制每个用户空间中的进程所有可用资源问题,如可以按照CPU来分配,一共有3个用户空间,定义使用的比例在三个用户空间中,是1:2:1,这种分配就比较有弹性,把CPU当作4份,第一个用户只能使用25%,第二个用户是50%,第三个用户是25%,这种优点在于如果第二个和第三个不使用,第一个需要很多的计算,它把整个CPU吃掉都可以,一旦第二三个要使用只能按照比例来分配,如果再加一个用户空间就变成1:2:1:1,进行分配因为CPU是可压缩资源,可以使用这种比较弹性的方式来分配,也可使用限制用户空间的进程最多可以使用几核心,如物理机有32核心,最多给它分配2核心来使用,所以可以在整体的资源上做比例型的分配,也可以在单一的用户空间上做绑定可以使用几个核心,调度时都可以调度,但是不能超过可使用的数量,内存也是一样的原理,最多是64G的内存,告诉它只能使用不能超4G,超过后,那个进程比较消耗内存就使用OOM来kill掉一个,因为就分配这么多,不能超过使用的限制,因为内存属于不可压缩性资源,超过不可收回来的的,这种功能必须在内核上针对名称空间来实现,靠的一种机制是
CGROUPS(control groups)控制组
Cgroups
描述:把系统级的资源分成多个组,然后把每个组内的资源量指派或者分配到每个用户空间的进程上去,可以实现对于一个系统之上,所要运行的进程给它分类,每类就是一个组,叫做控制组,每组可以分配不同的资源,而且组内还可以进行再次划分,一旦把资源分配给某一个组后,组内的子组可以自动的使用这个资源,除非单独分配,则是自动有资源的使用权限。
如果把一个用户空间当作一个组,向组内指定不同的资源,就可以限制它们使用资源的能力,把资源放入一个namespace后,名称空间内部的进程自动拥有所使用资源的能力,有了cgroups,namespace,可以使用容器。
blkio: 这个subsystem可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。
cpu: 这个subsystem使用调度程序控制task对CPU的使用。
cpuacct: 这个subsystem自动生成cgroup中task对CPU资源使用情况的报告。
cpuset: 这个subsystem可以为cgroup中的task分配独立的CPU(此处针对多处理器系统)和内存。
devices 这个subsystem可以开启或关闭cgroup中task对设备的访问。
freezer 这个subsystem可以挂起或恢复cgroup中的task。
memory 这个subsystem可以设定cgroup中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。
perfevent 这个subsystem使用后使得cgroup中的task可以进行统一的性能测试。{![perf: Linux CPU性能探测器,详见 https://perf.wiki.kernel.org/index.php/MainPage]}
net_cls 这个subsystem Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。
容器的隔离能力
描述:相对虚拟机来说,容器的隔离能力是比较低的,因为它是属于同一个内核强行设置一个边界,而虚拟机本身就属于不同的内核,所以隔离性远不如主机级的虚拟化比较好,为了加强安全性,使用selinux之类加强容器的边界,防止容器之间绕开漏洞进程操作,为了支撑容器技术做得更好,要使用seliunx,但是它并不太好使用,所以一般使用的技术还是chroot,cgroup,namespace,这些在内核
级别就已经实现了.
docker
描述:容器技术本身就是靠当年的jail来启动形成的vserver(chroot),向后为了把这种容器技术做得更加的容易使用,原先要自己使用代码来调用clone(),setns()等实现创建内核,但是并没有多少用户有这种能力能够实现,所以最好把使用容器的技术做成一组工具来使用比较方便,所以就有了LXC的解决方案,linuX container,是最早一批真正把完整的容器技术使用一组简易使用的工具和模板来极大的简化容器技术使用的方案,所以把自己叫做linuX container.
还有一组工具lxc-create快速创建,可以叫做一个linux空间,用户空间里要用bin,sbin等目录结构,还要安装上基本的应用程序,可以从宿主机copy,但是要创建目标的用户空间底层是内核,根用户空间是centos,要创建一个新的用户空间运行ubuntu,如果是ubuntu就无法复制宿主机,所以要基于模板来使用,这个模板实际就是一组脚本,创建一个名称空间后,这个脚本会自动的去执行后,会给它一个脚本的执行环境,执行时会自动实现安装过程,这个安装就是指向所打算创建那一类名称空间的系统发行版所属的仓库,从仓库中把各应用程序下载下来安装生成新的名称空间,这个名称空间就像虚拟机一样被使用。利用模板,访问到模板中所定义的,访问到发行版本的仓库,利用仓库和程序包下载到本地来完成安装过程。
底层是操作系统,有一个特权的根的名称空间或者是特权的用户空间,在用户空间中找一个子目录,在这个子目录上执行一个脚本,这个脚本能把一个发行版当作根都安装上去,然后chroot进去,lxc就靠这组工具进行快速的创建空间利用模板实现内部各文件的安装,同时还有工具在完成安装后实现chroot切换过去,就要以并行的使用多个用户空间,每个用户空间就像虚拟机一样一个独立的系统,里面有所需要的文件系统,账号等等。
LXC运行不便:但是LXC还是有更高的门槛,要学会LXC的各种工具的使用,有必要去订制模板,每个用户空间都是安装生成的,后来运行过程中生成了很多的文件,生成的数以后要迁移如何解决?如果需要大量的创建容器也不是一件容易的事情,虽然LXC极大的简化的容器技术的使用,但是比起过去使用虚拟机,它的复杂程序并没有降低,而且隔离性并没有虚拟机好,好处在于它可以让每个用户空间中的进程直接使用宿主机的性能,中间没有额外的开销,除此之外在分发上,大规模使用上还是没有很好的突破口。
docker出现:docker是出现,是LXC的增强版,把LXC的使用得到简化和普及,LXC在规模使用上(另一个主机上copy)困难,docker能够解决这方面的问题,docker早期是LXC的二次发行版。
docker功能上的实现,利用LXC作为容器的管理引擎,但是创建容器时不再使用模板来现场安装生成,而是事实通过一种叫做镜像技术来,kvm中有使用,可以把一个操作系统打包成一个镜像,可以把一个操作系统copy走,创建虚拟机时基于image来启动就可以了,可以尝试把一个操作系统中用户空间所使用到的所有组件事先准备编排好,整体打包成一个文件,这个文件叫做镜像文件,镜像文件是集中式的放在一个仓库中的,在互联网上都可以访问,如把一个最小化的centos或ubuntu放上去,或者安装nginx在最小化的centos上也放在在这个仓库中,以后有用户要创建容器时,docker是运行容器,创建容器,销毁容器,还有LXC的一组工具在此之上,docker还实现另外一种功能,但使用lxc-create创建一个容器时,它不会激活模板进行安装,而是连接到服务器上下载一个匹配容器的镜像,把镜像拖到本地,基于镜像来启动容器,所以docker极在简化了容器的使用难道,要启动一个服务器docker run自动到仓库下载下来。
为了使用得容器更加易于管理,在一个用户空间中尝试运行一个进程或者一组进程,因为目的就是为了运行一个有生产功能的软件程序,干脆采用更加精巧的机制在一个容器内只运行一个进程,如nginx安装在nginx的容器中,tomcat安装在tomcat容器中,两者使用容器的逻辑来通信,所以docker的目的是一个容器运行一个进程,而LXC是把一个它当到用户空间来使用,当作虚拟机来使用,运行M个进程,使用在容器管理上带不便,docker使用这种限制性的机制一个容器中只运行一个进程的方式。
以前运行进程是,一个主机上有一个内核,运行了很多的进程,都是属于同一个用户空间的,共享同一组主机名,用户,IPC,PID,MOUNT等相互不隔离,从而也会导致问题,可能黑客劫持一个进程,可能使用它来做跳板威胁到其他的进程。
docker所实现的功能是,同一组硬件,同一个内核,但是每个进程是运行在自己单独的空间之中,而且容器内的资源都是为这个进程所准备的如不使用usr目录可以不用,最小化给它定制的,但是之前到它都放在一个地方可以使用共享的一个文件就行了,但是使用容器的话,可能共享使用的文件要有多分份,分别放在各个容器中,也能隔离,但是占用了更多的空间,还有一个不好的,如果系统上的进程出现问题可以使用工具如Ps来观察,在容器中这些工具也要准备多份来做调试,目的是在一个名称空间中运行一个进程,如果再运行多个调试工具就可能违反它的法则,不运行也无法调试,所以运行这样的进程是必然的,但是它不是主要的目的,只是使用到时才启用,启动也是唯一一个容器的main process主进程,像nginx这些进程也是运行在同一个名称空间中,如果名称空间中的进程中止了,这个容器就没有存在的价值,也会中止。
容器优劣点:调试问题本来很装简单的,现在要进入到容器中进行调试,给运维带来极大不便,但是给开发人员带来了极大的便利,因为分发容易了,可以真正的实现了一次编写到处运行,现在的环境通常都是异构的,centos5,6,7并行,windows,等等,开发要适度各种平台,第二每种平台的访问路径都不一样,程序员可能为了每个平台就单独组织一个团队来研发,只需要组成成一个团一个镜像就可以,无论是linux,windows,使用docker run就完事了,与底层内核,操作系统都无关有独立的名称空间,使用是程序员突破分发上的难题,而且之前的部署也很麻烦,以后镜像设置好,直接使用docker run就可以解决问题,如发一个网站的新版本,以前是找到特定路径下,还在灰度之类的,现在只需要做一个镜像就可以了,不过前端还需要调试器,run起来可能还要接入到调试器上去,如果有容器的编排工具,这个过程直接都省略了,直接run就可以了,以前的容器,如java只是支持java一种编程语言,以后无论使用什么语言,操作系统,都可以在同一个平台上进行docker容器使用,极大的降低了软件开发的成本
另一个好处:docker是通过从仓库中下载来运行的,如果要批量创建,如10服务器,每台服务器只需要下载一次就可以,因为docker的镜像实现是通过构建底层是通过所谓分层构建联合挂载的机制实现的,做镜像是先做一个最基础的镜像,如基层centos镜像,里面什么也没有纯净的,要安装一个nginx,不必须从头构建,基于这个centos的基础上安装一个Nginx就可以了,但是Nginx镜像只包含nginx本身,不包含centos,是两层,两层叠在一起就是一个运行在centos上的nginx,所以叫做分层构建,一个功能在一层上实现,叠在一起形成一个统一的视图就是一个centos上运行的nginx,这样可以使用镜像分发没有这么庞大了,如在一个系统上需要运行三个容器nginx,tomcat,mariadb都是基于底层centos来构建的,只需要使用一个基础的centos镜像,再使用三个不同的层,只有nginx层,只有tomcat层,只有mariadb层,需要使用nginx时,让Nginx与centos进行联合挂载,每一层镜像都是只读的,甚至挂载两个nginx也是可以,同一个centos挂载两个nginx的镜像。
镜像文件是只读的,要修改,要在联合挂载的镜像栈的最顶层额外的附加一个新的层,这个层才是能读能写的层,而且是容器自身专有的,最低的容器只能在自己的容器来操作,要删除,只能标记不可见,要修改,先从底层复制上去,再修改,这样会造成性能的效果下降,而且这样产生另一个问题迁移也变得很困难,因此再在使用容器时不会在本地保存有效的数据,要保存持久数据是在文件系统外部挂载一个共享的存储,glusfs,ceph等存储设备,通过网络挂载上去,如mariadb是要产生数据的,保存在外存上,以后有问题,直接再起一个镜像,把外存挂载上去就可以了,脱离宿主机,容器可以运行在任何一个主机上,就变成一个容器是一个进程,程序是指令加数据组成,指令是放在程序文件中,数据放在文件中或外部存储中,如使用vim编辑一个文件,是靠进程来实现,不是靠程序,vim进程关闭了,而文件不会随着进程中止而消失,启动一个容器,相当于一个进程,进程中止了,数据还在外部的存储中,因此容器中止时,可以把容器直接删除,不需要持久,所有容器像进程一样有一个生命周期,而且与主机也没有密切的关联关系
实现另一种功能:假设底层准备了10个主机,如果有一个功能给它加上一层,如都是docker,在docker之上给它建构一个层次,中间层要启动一个容器,由它来决定找那个主机更加空闲,在其实一个主机上启动一个容器,这个容器需要存储数据就给它分配一个外部的存储空间,一旦任务结束了,把容器删除,所以中间层组件可以帮忙要启动的容器调度,整个底层docker host之上,当需要构建一个nmp时,它们是有依赖关系的,所以要事先定义好,docker没有这功能,所以需要一个在docker基础之上,能够到应该程序之间的依赖关系,从属关系等反映在启动关闭次序时的管理逻辑中,这种功能叫做容器的编排功能,如docker的swarm+compose,compose是单机编排的,只能编排在一个单机服务器之上的,如nmp可能运行在一个主机上,但是compose也一样可以编排,如果把n个docker的主机当一个主机来管理,swarm能够实现这个功能,还有一个docker machine,使用这三个来当作编排工具来使用
还有另一个工具ASF的mesos,一个数据中心统一资源编排工具,但是mesos不是用来专门编排容器的,是实现统一资源调试和分配的,如果要编排容器要加一个中间层marathon
第三种是kubernetes,简称k8s
docker引擎的变化: lxc --> libcontainer --> runC
运维问题:a. 发布操作可以使用编排工具来实现,因为直接手动管理很麻烦,所以要使用编排工具,增加运维环境管理的复杂度
b. 出现问题,调试难度加大,可能每个镜像要增加调试工具,使用得系统很庞大