Docker是一个开源的软件项目,让用户程序部署在一个相对隔离的环境运行,借此在Linux操作系统上提供一层额外的抽象,以及操作系统层虚拟化的自动管理机制。需要额外指出的是,Docker并不等于容器(containers),Docker只是容器的一种,其他的种类的容器还有Kata container,Rocket container等等。
Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。
Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。
如今的系统在架构上较十年前已经变得非常复杂了。以前几乎所有的应用都采用三层架构(Presentation/Application/Data),系统部署到有限的几台物理服务器上(Web Server/Application Server/Database Server)。
而今天,开发人员通常使用多种服务(比如 MQ,Cache,DB)构建和组装应用,而且应用很可能会部署到不同的环境,比如虚拟服务器,私有云和公有云。
一方面应用包含多种服务,这些服务有自己所依赖的库和软件包;另一方面存在多种部署环境,服务在运行时可能需要动态迁移到不同的环境中。这就产生了一个问题:如何让每种服务能够在所有的部署环境中顺利运行?
聪明的技术人员从传统的运输行业找到了答案。
几十年前,运输业面临着类似的问题。
每一次运输,货主与承运方都会担心因货物类型的不同而导致损失,比如几个铁桶错误地压在了一堆香蕉上。另一方面,运输过程中需要使用不同的交通工具也让整个过程痛苦不堪:货物先装上车运到码头,卸货,然后装上船,到岸后又卸下船,再装上火车,到达目的地,最后卸货。一半以上的时间花费在装、卸货上,而且搬上搬下还容易损坏货物。
幸运的是,集装箱的发明解决这个难题。
打一个比方,集装箱(容器)对于远洋运输(应用运行)来说十分重要。集装箱(容器)能保护货物(应用),让其不会相互碰撞(应用冲突)而损坏,也能保障当一些危险货物发生规模不大的爆炸(应用崩溃)时不会波及其它货物(应用)但是把货物(应用)装载在集装箱(容器)中并不是一件简单的事情。而出色的码头工人(Docker)的出现解决了这一问题。它(Docker)使得货物装载到集装箱(容器)这一过程变得轻而易举。对于远洋运输(应用运行)而言,用多艘小货轮(虚拟机)代替原来的大货轮(实体机)也能保证货物(应用)彼此之间的安全,但是和集装箱(容器)比,成本过高,但适合运输某些重要货物(应用)。
任何货物,无论钢琴还是保时捷,都被放到各自的集装箱中。集装箱在整个运输过程中都是密封的,只有到达最终目的地才被打开。标准集装箱可以被高效地装卸、重叠和长途运输。现代化的起重机可以自动在卡车、轮船和火车之间移动集装箱。集装箱被誉为运输业与世界贸易最重要的发明。
Docker 将集装箱思想运用到软件打包上,为代码提供了一个基于容器的标准化运输系统。Docker 可以将任何应用及其依赖打包成一个轻量级、可移植、自包含的容器。容器可以运行在几乎所有的操作系统上。
其实,“集装箱” 和 “容器” 对应的英文单词都是 “Container”。
“容器” 是国内约定俗成的叫法,可能是因为容器比集装箱更抽象,更适合软件领域的原故吧。
由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,Docker 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。
传统的虚拟机技术启动应用服务往往需要数分钟,而 Docker 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。
开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题。
对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。
由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。
Docker镜像(Image)类似于虚拟机的镜像,可以将他理解为一个面向Docker引擎的只读模板,包含了文件系统。例如:一个镜像可以完全包含了Ubuntu操作系统环境,可以把它称作一个Ubuntu镜像。镜像也可以安装了Apache应用程序(或其他软件),可以把它称为一个Apache镜像。
镜像是创建Docker容器的基础,通过版本管理和增量的文件系统,Docker提供了一套十分简单的机制来创建和更新现有的镜像。用户可以从网上下载一个已经做好的应用镜像,并通过命令直接使用。总之,应用运行是需要环境的,而镜像就是来提供这种环境。
Docker容器(Container)类似于一个轻量级的沙箱(因为Docker是基于Linux内核的虚拟技术,所以消耗资源十分少),Docker利用容器来运行和隔离应用。容器是从镜像创建的应用运行实例,可以将其启动、开始、停止、删除,而这些容器都是相互隔离、互不可见的。可以把每个容器看作一个简易版的Linux系统环境(包括了root用户权限、进程空间、用户空间和网络空间),以及与运行在其中的应用程序打包而成的应用盒子。镜像自身是只读的。容器从镜像启动的时候,Docker会在镜像的最上层创建一个可写层,镜像本身将保持不变。就像用ISO装系统之后,ISO并没有什么变化一样。
Docker仓库(Repository)类似于代码仓库,是Docker集中存放镜像文件的场所。有时候会看到有资料将Docker仓库和注册服务器(Registry)混为一谈,并不严格区分。实际上,注册服务器是存放仓库的地方,其上往往存放着多个仓库。每个仓库集中存放某一类镜像,往往包括多个镜像文件,通过不同的标签(tag)来进行区分。例如存放Ubuntu操作系统镜像的仓库,称为Ubuntu仓库,其中可能包括14.04,12.04等不同版本的镜像。根据存储的镜像公开分享与否,Docker仓库分为公开仓库(Public)和私有仓库(Private)两种形式。
目前,最大的公开仓库是Docker Hub,存放了数量庞大的镜像供用户下载。国内的公开仓库包括Docker Pool等,可以提供稳定的国内访问。如果用户不希望公开分享自己的镜像文件,Docker也支持用户在本地网络内创建一个只能自己访问的私有仓库。当用户创建了自己的镜像之后就可以使用push明亮将它上传到指定的公有或则私有仓库。这样用户下次在另一台机器上使用该镜像时,只需将其从仓库pull下来就可以了。Docker利用仓库管理镜像的设计理念甚至命令和git非常相似,也就意味着非常好上手。
从上面这幅图就可以看出,虚拟机是正儿八经的存在一层硬件虚拟层,模拟出了运行一个操作系统需要的各种硬件,例如CPU,MEM,IO等设备。然后在虚拟的硬件上安装了一个新的操作系统Guest OS。所以在Windows宿主机上面可以跑Linux虚拟机。因为多了一层虚拟,所以虚拟机的性能必然会有所损耗。Docker容器是由Docker Deamon(Docker Deamon是运行在宿主机上面的一个后台进程,负责拉起和设置容器)拉起的一个个进程,通过Docker Deamon设置完Namespace和Cgroup之后,本质上就是一个运行在宿主机上面的进程。因为没有一层虚拟的Guest OS,所以Docker轻量级很多。但是有利就有弊,由于Docker 容器直接运行在宿主机上面,安全性就相对较差些,另外也没有办法在Windows上面运行Linux的容器,如果容器里面的应用对特定系统内核有要求也不能运行在不满足要求的宿主机上面。
对比传统虚拟机总结
Docker通常用于如下场景:
Docker利用Linux中的核心分离机制,例如Cgroups,以及Linux的核心Namespace(名字空间)来创建独立的容器。一句话概括起来Docker就是利用Namespace做资源隔离,用Cgroup做资源限制,利用Union FS做容器文件系统的轻量级虚拟化技术。Docker容器的本质还是一个直接运行在宿主机上面的特殊进程,看到的文件系统是隔离后的,但是操作系统内核是共享宿主机OS,所以说Docker是轻量级的虚拟化技术。
Linux Namespace 是Linux 提供的一种内核级别环境隔离的方法,使其中的进程好像拥有独立的操作系统环境。Linux Namespace 有 Mount Namespace,UTS Namespace, IPC Namespace, PID Namespace, Network Namespace, User Namespace, Cgroup Namespace。详情看下表:
分类 |
系统调用参数 |
隔离内容 |
内核版本 |
Mount Namespace |
CLONE_NEWNS |
文件系统挂载点 |
Linux 2.4.19(2002年) |
UTS Namespace |
CLONE_NEWUTS |
Hostname和domain name |
Linux 2.6.19 |
IPC Namespace |
CLONE_NEWIPC |
进程间通信方式,例如消息队列 |
Linux 2.6.19 |
PID Namespace |
CLONE_NEWPID |
进程ID编号 |
Linux 2.6.24 |
Network Namespace, |
CLONE_NEWNET |
网络设备,协议栈,路由表,防火墙规则,端口等 |
Linux 2.6.24 start Linux 2.6.29 end |
User Namespace |
CLONE_NEWUSER |
用户及组ID |
Linux 2.6.23 start Linux 3.8 end |
Cgroup Namespace |
CLONE_NEWCGROUP |
Cgroup根目录 |
Linux 4.6 |
上述系统调用参数CLONE_NEWNS等主要应用于以下三个系统调用:
函数声明 :
#include int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
例如:
int pid = clone(call_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
会让新创建的该进程执行call_function,例如/bin/bash,且该进程的PID进程编号是隔离状态,也就是新的PID编号,该进程ps将会看到它的PID是1。
如果多次执行上述clone就会创建多个PID Namespace,而每个Namespace里面的应用进程都认为自己是当前容器里的1号进程,它们既看不到宿主机里的真实进程空间,也看不到其他PID Namespace里面的具体情况。
上面已经讲过Docker 容器运行起来是一个直接运行在宿主机上面的进程,那么如果限定每个容器最多消耗多少CPU资源呢?如果一个容器疯狂的消耗资源岂不是会影响同一宿主机上面其他的容器?所以Docker就需要一个限制容器能够使用资源上限的机制,那就是Linux Cgroup技术。Linux Cgroup 全称是Linux Control Group。它最主要的作用是限制一个进程组能够使用的资源上限,包括CPU,MEM,DISK,NET等等。
下面我将演示一个利用Cgroup限制进程CPU的例子:
[root@nginx-1 /sys/fs/cgroup/cpu]# ll
total 0
-rw-r--r-- 1 root root 0 Sep 26 2018 cgroup.clone_children
--w--w--w- 1 root root 0 Sep 26 2018 cgroup.event_control
-rw-r--r-- 1 root root 0 Sep 26 2018 cgroup.procs
-r--r--r-- 1 root root 0 Sep 26 2018 cgroup.sane_behavior
-r--r--r-- 1 root root 0 Sep 26 2018 cpuacct.stat
-rw-r--r-- 1 root root 0 Sep 26 2018 cpuacct.usage
-r--r--r-- 1 root root 0 Sep 26 2018 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.cfs_relax_thresh_sec
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.rt_period_us
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 Sep 26 2018 cpu.shares
-r--r--r-- 1 root root 0 Sep 26 2018 cpu.stat
drwxr-xr-x 9 root root 0 Jun 6 17:03 docker
-rw-r--r-- 1 root root 0 Sep 26 2018 notify_on_release
-rw-r--r-- 1 root root 0 Sep 26 2018 release_agent
-rw-r--r-- 1 root root 0 Sep 26 2018 tasks
[root@nginx-1 /sys/fs/cgroup/cpu]# mkdir mytest #创建mytest目录,系统会自动添加以下文件
[root@nginx-1 /sys/fs/cgroup/cpu/mytest]# ll
total 0
-rw-r--r-- 1 root root 0 Jun 13 16:55 cgroup.clone_children
--w--w--w- 1 root root 0 Jun 13 16:55 cgroup.event_control
-rw-r--r-- 1 root root 0 Jun 13 16:55 cgroup.procs
-r--r--r-- 1 root root 0 Jun 13 16:55 cpuacct.stat
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpuacct.usage
-r--r--r-- 1 root root 0 Jun 13 16:55 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.cfs_relax_thresh_sec
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.rt_period_us
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 Jun 13 16:55 cpu.shares
-r--r--r-- 1 root root 0 Jun 13 16:55 cpu.stat
-rw-r--r-- 1 root root 0 Jun 13 16:55 notify_on_release
-rw-r--r-- 1 root root 0 Jun 13 16:55 tasks
[root@nginx-1 /sys/fs/cgroup/cpu/mytest]# while : ; do : ; done & # 运行一个死循环命令
[1] 2459615
op观察会发现该进程CPU跑到了100%,符合预期
主要的限制参数来源自文件cpu.cfs_quota_us,默认是-1,不做限制,如果改成20000说明限定20%的CPU上限。因为总量存在于cpu.cfs_period_us,是100000,意思cpu时间总量是100000us,20000/100000=20%。然后将bash命令的PID写到tasks文件中,改完之后的CPU占用是20%,符合预期:
同理可限制MEM,DISK和NET,需要特殊指出的是MEM是硬限制,当容器的内存使用量超过了Cgroup限定值会被系统OOM。
每个容器运行起来后都有一个独立的文件系统,例如Ubuntu镜像的容器能够看到Ubuntu的文件系统,Centos能够看到Centos的文件系统, 不是说容器是运行在宿主机上面的进程吗,为什么能够看到和宿主机不一样的文件系统呢?那就要归功于Union FS,全称是Union File System,联合文件系统。将多个不同位置的目录联合挂载到同一个目录,将相同的部分合并。Docker利用这种联合挂载能力,将容器镜像里面的多层内容呈现为统一的rootfs(根文件系统),即root用户能够看到的根目录底下所有的目录文件。rootfs打包了整个操作系统的文件和目录,是应用运行时所需要的最完整的“依赖库”,也就是我们说的“镜像”。
镜像分为基础镜像只读层,和Init层,和读写层。
Init 层存放的是/etc/hostname,/etc/resolv.conf 等, docker commit的时候不提交。
读写层一开始的时候为空,用户如果修改了文件系统,比如说增删改了文件,docker commit的时候就会提交这一层信息。
Docker 容器总结起来就是利用Linux Namespace做资源隔离,Cgroup做资源上限限制,rootfs做文件系统 运行在宿主机上面的一个特殊进程。
Docker基础原理 - 我是码客 - 博客园
开始学习Docker啦--容器理论知识(一) - 小水滴18 - 博客园
聊聊Docker理论知识(二) - 小水滴18 - 博客园
Docker与Kubernetes系列(一): Docker的基本概念_沈鸿斌的博客-CSDN博客_docker kubernet
Docker与Kubernetes系列(二): Docker的基本用法_沈鸿斌的博客-CSDN博客
docker容器技术入门知识及思维导图_adorable_的博客-CSDN博客_docker学习思维导图