容器是Linux内核提供得技术,Docker只是一个容器工具。Docker ≠ 容器
谈Docker必谈容器 先了解一下容器是什么
根据WIKI的定义,容器概念如下:
计算机领域内的容器技术想要能明白的话,要先了解一下传统虚拟化面临的问题。先看下一节。
计算机容器技术本身也是一种虚拟化技术(轻量级虚拟化)。在容器技术诞生之前我们已经有了传统虚拟化:KVM、VMware、Xen等传统虚拟化实现。那我为什么还要使用容器呢?
如上图所示传统虚拟化分为Type-I(裸机) 和Type-2(寄宿)两种类型,试想一下,如下场景:
我想运行一个Nginx,在两种虚拟化类型下分别需要做那些事情:
1.Type-I: 需要为Nginx程序创建一个操作系统,然后才能运行Nginx。Nginx运行在虚拟操作系统的用户空间。
硬件资源虚拟化链路为 hypervisor->操作系统内核空间->操作系统用户空间->Nginx。
2.Type-II: 需要为Nginx程序创建一个操作系统,然后才能运行Nginx。Nginx运行在虚拟操作系统的用户空间。
硬件资源虚拟化链路为 寄宿的操作系统->hypervisor->操作系统内核空间->操作系统用户空间->Nginx
综上所述,我就想运行一个Nginx,在传统虚拟化中需要为Nginx准备一个操作系统,才能运行Nginx。必须花费大量的时间来安装与设置虚拟机,接着才能开始评估或测试所需运作的软件,这些设置包含了操作系统的安装、安全性或兼容性软件的更新、网络、系统调校…等。
注:横线部分不是容器技术本身能解决的,由Docker提供镜像(Image) 机制解决。
回归一下这个问题的本质。应用程序运行依赖操作系统提供的库,依赖的软件和特定于系统的数据结构或文件系统。于是我们要为应用程序提供虚拟操作系统,虚拟的操作系统同时提供了一个虚拟的内核。虚拟的内核来管理虚拟的硬件设备。
**总结一下:传统虚拟化在使用成本上较高,提供虚拟的硬件设备,虚拟的操作系统。当然还有为了提供这些所造成的CPU,内存压力。既然如此何不直接把虚拟的操作系统去掉?于是容器技术就诞生了。
去掉虚拟的操作系统层后,我们所追求的环境如下:
创建多个隔离环境。应用程序应该跑在用户空间,内核提供的是内核空间,而现在需要进程运行在隔离环境,在一个内核中用户空间只有一个,那就表示所需要隔离的是用户空间。所以期望的是将用户空间隔离成多组,彼此之间互相不干扰,一个用户空间至运行一个或部分进程。(注意通常会有一个名称空间有特权,一般是第一个用户空间,类系Xen和KVM) 随后进程启动和运行在用户空间中,在众多用户空间能共享底层同一个内核,被同一个内核管理。但是进程自己运行时所能看到的边界是所属用户空间的边界。这样彼此间也就隔离了。(这样隔离并没有主机虚拟化隔离的那么彻底) 此时这个用户空间给进程提供运行环境,并且其能够保存内部进程不是其他进程干扰。这就是计算机的容器技术。
一个用户空间的主要目标就是隔离环境,任何进程运行在用户空间当中,进程就认为自己是唯一运行在到当前内核之上的用户空间中的进程,而且它所能看到的所有进程也就是当前主机的所有进程了。 一个名称空间他应该有这些组件:UTS(主机名和域名)、Mount(根文件系统)、IPC(同名称空间进程间通信专用通道)、 PID(进程数)、User(用户)、Net(网卡,网络接口,全套接字)。如上这些资源都由内核通过名称空间机制,并且封装成为系统调用向外暴露。还有一个很重要的机制chroot,即 change root directory (更改 root 目录)。在 linux 系统中,系统默认的目录结构都是以 /,即以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。 在现代计算机中容器就是通过内核提供的6个名称空间和chroot实现的。
内核在设计之初并没有提供如上的资源隔离机制,如下是各个名称空间隔离在内核实现的对应关系。
资源隔离对应内核版本号
namespace | 系统调用参数 | 隔离内容 | 内核版本 |
---|---|---|---|
UTS | CLONE_NEWUTS | 主机和域名 | 2.6.19 |
IPC | CLONE_NEWIPC | 信号量,消息队列和共享内存 | 2.6.19 |
PID | CLONE_NEWPID | 进程编号 | 2.6.24 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等 | 2.6.29 |
Mount | CLONE_NEWNS | 挂载点(文件系统) | 2.4.19 |
User | CLONE_NEWUSER | 用户和用户组 | 3.8 |
cgroups,其名称源自控制组群(英语:control groups)的简写,是Linux内核的一个功能,用来限制、控制与分离一个进程组的资源(如CPU、内存、磁盘输入输出等)。
这个项目最早是由Google的工程师(主要是Paul Menage和Rohit Seth)在2006年发起,最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为cgroups,并且被合并到2.6.24版的内核中去。自那以后,又添加了很多功能。
有过个容器在跑,万一有一个容器内部的进程出现意外情况,像脱缰的野马一样疯狂的抢占系统资源,如CPU,内存等。CPU因为是可压缩性资源所以其实还好,内存一旦被使用完系统将会发生OOM(暴头程序)等异常,且其他容器内的进程也会因没有内存而无法运行。因为容器共用一个内核所以一旦资源被疯狂掠夺,其他运行的进程将受影响。这怎么能行呢? 于是Cgroup就用来解决容器资源限制的问题。在内核中用Cgroup针对每一种名称空间进行资源限制。
Cgroups无非是把系统级的资源分成多个资源组(子系统),然后把每个组内的资源量指派或分配到特定用户空间的进程来实现。
Cgroups可以将资源分成多个资源组(子系统) 如下所示:
- blkio: 块设备IO
- cpu: CPU
- cpuacct: CPU资源使用报告
- cpuset: 多处理器平台上的CPU集合
- devices:设备访问
- freezer: 挂起或恢复任务
- memory: 内存用量及报告
- perf_event: 对cgroup中的人种进行统一的性能测试
- net_cls: cgroup中的任务创建的数据报文的类别标识符
内核使用cgroup结构体来表示一个control group对某一个或多个cgroups资源组的资源限制。cgroup结构体可以组织成一颗树的形式,每一棵cgroup组成的树称之为一个cgroups层级结构。 cgroups层级结构可以attach 一个或多个cgroups资源组,层级结构可以使用被attach的cgroups资源组进行资源限制。
上图所示 一个cgroups层级结构,一个层级结构中是一颗树形结构,树的每一个节点是一个cgroup结构体(如cpuRoot,cpu2,CM3)。 这个cgroups层级结构attach了cpu资源组和cpuacct资源组,于是当前资源组就可以对cpu资源进行限制,并且对进程的cpu使用情况进行统计。 在每一个cgroups层级结构中,每一个节点(cgroup结构体)可以对资源设置不同的限制权重,子节点默认会遵循父节点的权重。如上图cpu2组中的进程可以使用60%的cpu时间片而CM3组的进程可以使用40%的cpu时间片。
在创建了cgroups层级结构中的节点(cgroup结构体)之后,可以把进程加入到某一个节点的控制任务列表中,一个节点的控制列表中的所有进程都会受到当前节点的资源限制。同事某一个进程也可以被加入到不同的cgroups层级的节点中,因为不同的cgroups层级结构可以负责不同的系统资源。进程和节点(cgroup结构体)是多对多的关系,但是节点(cgroup结构体)不可以是同一个cgroups层级结构中的。
LXC,其名称来自Linux软件容器(Linux Containers)的缩写,一种操作系统层虚拟化(Operating system–level virtualization)技术,为Linux内核容器功能的一个用户空间接口。它将应用软件系统打包成一个软件容器(Container),内含应用软件本身的代码,以及所需要的操作系统核心和库。透过统一的名字空间和共享API来分配不同软件容器的可用硬件资源,创造出应用程序的独立沙箱运行环境,使得Linux用户可以容易的创建和管理系统或应用容器。
在Linux内核中,提供了cgroups功能,来达成资源的区隔化。它同时也提供了名称空间区隔化的功能,使应用程序看到的操作系统环境被区隔成独立区间,包括行程树,网络,用户id,以及挂载的文件系统。但是cgroups并不一定需要引导任何虚拟机。
LXC利用cgroups与名称空间的功能,提供应用软件一个独立的操作系统环境。LXC不需要Hypervisor这个软件层,软件容器(Container)本身极为轻量化,提升了创建虚拟机的速度。软件Docker被用来管理LXC的环境。
通过上面的介绍,我们知道容器(6个名称空间)和cgroups组合即可实现完整并且保证隔离的容器环境。但是控制名称空间和cgroups都是系统调用,又有多少人能掌握呢? 于是LXC出现了,LXC就是让用户可以轻松地创建和管理系统或应用程序容器。
LXC容器通常被认为是在chroot和传统虚拟化之间的。LXC的目标是创建一个尽可能接近标准的linux安装环境,但不需要单独的内核。换而言之LXC就像是传统虚拟化一样提供一个VM只是和寄宿系统公用同一个内核(与docker不一样)
LXC工作模式是这样的,使用lxc-create创建一个容器(名称空间),然后通过模板(早期shell脚本,目前yaml脚本),执行安装过程。这个模板,会自动实现安装过程,这个安装就是指向了你想创建的容器(名称空间)的系统发行版的仓库,利用仓库中的程序包下载至本地来完成安装过程。于是这个容器(名称空间)就像虚拟机一样使用。
lxc-centos模板示例
lxc-centos模板:https://github.com/AtlanCI/LXC-Centos_template/blob/main/lxc-centos
Docker 是一个开放源代码软件,是一个开放平台,用于开发应用、交付(shipping)应用、运行应用。 Docker允许用户将基础设施(Infrastructure)中的应用单独分割出来,形成更小的颗粒(容器),从而提高交付软件的速度。
Docker容器与虚拟机类似,但二者在原理上不同。容器是将操作系统层虚拟化,虚拟机则是虚拟化硬件,因此容器更具有便携性、高效地利用服务器。 容器更多的用于表示 软件的一个标准化单元。由于容器的标准化,因此它可以无视基础设施(Infrastructure)的差异,部署到任何一个地方。另外,Docker也为容器提供更强的业界的隔离兼容。
通过上面的描述我们知道了LXC被用来管理容器,但是使用LXC来管理容器将会面临下列问题:
- 要想使用LXC管理容器,要学习很对LXC工具。LXC命令列表
- 必要时要定制模板
- 每一个名称空间都是安装生成的,在该名称空间中运行的进程会生成一些文件(数据库之类的),当该宿主故障时,如何迁移到其他宿主机上
- 批量创建较困难
总结起来就是通过LXC管理容器比起传统虚拟化的虚拟机的使用复杂度没有多大降低,更何况隔离性也没有虚拟机那么好。当然了好处在于每个容器中的进行都可以直接使用宿主机的性能,中间没有额外开销(节约资源)。LXC在分发和大规模使用上没有很好的方法。于是后来就出现了Docker,早期的Docker可以看作为是LXC的增强版(主要解决分发和大规模使用)。
Docker本身不是容器,Docker只是容器的易用工具。容器是Linux内核的技术,Docker只是简化容器这种技术的使用。
目前LXC被进一步更新为LXD(当然使用方式还是和虚拟机差不多,分发和大规模使用会好很多),如想了解请参阅:https://linuxcontainers.org/lxd/introduction/
网上关于Docker的文章一抓一大把,这里只简短介绍如何解决分发和大规模的技术。
如上所述LXC面临大规模使用和在其他的主机上复刻容器(分发)很难,于是Docker就在这方面着手解决。所以早期的Docker就是LXC的二次封装。功能上是通过LXC作为容器管理引擎,但是创建容器时,不在是用模板现场安装生成,而是事先通过一种叫做镜像的技术。把一个操作系统的用户空间用到了所有组件编排好,编排好以后整体打包成一个文件。这个文件就叫做镜像文件(Image)。使用Docker创建容器时,Docker不会激活LXC的模板创建安装。而是连接到镜像仓库,下载一个创建容器所需要的镜像。 Docker极大的简化了容器的使用,比如想要运行一个nginx 直接docker run nginx 就行了。
每个容器本身可以运行一个多一组进程,Docker为了使容器的使用更加易于管理,采用在一个Docker容器中之运行一个进程。这样会带来一下好处和坏处。
好处:
1.每个容器只运行一个进程,多个进程走容器间通信(更加隔离性,更容器分发)
坏处:
1.所需要的存储空间增加了
2.调试容器中的进程比较困难
开发人员开发的好的程序只需要打包到一个Docker镜像中,便可以到处运行在拥有在Docker的机器上。一些特殊进程类似数据库这种,需要采用共享存储才可以更好的解决分发问题。通过将应用打包为镜像这种机制,可以很好的解决分发的问题。 在大规模使用场景上也是很简单的,在每个机器上只需要有一份镜像文件便可以启动N个示例。(具体参考Docker镜像,分层部署-联合挂载这一特性)
有了Docker的镜像机制后就可以很好的解决分发和大规模使用了。 当然想好用好容器还是要有容器编排工具的。
Cgroups:https://tech.meituan.com/2015/03/31/cgroups.html
LXC introduction:https://linuxcontainers.org/lxc/introduction/
LXD introduction:https://linuxcontainers.org/lxd/introduction/