说实话关于 Docker 是什么并太好说,下面我通过四点向你说明 Docker 到底是个什么东西。
传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
通过 Docker 官网,我们知道了这么多 Docker 的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker 通常用于隔离不同的应用 ,例如前端,后端以及数据库。
就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。
Docker 技术是基于 LXC(Linux container- Linux 容器)虚拟容器技术的。
LXC,其名称来自 Linux 软件容器(Linux Containers)的缩写,一种操作系统层虚拟化(Operating system–level virtualization)技术,为 Linux 内核容器功能的一个用户空间接口。它将应用软件系统打包成一个软件容器(Container),内含应用软件本身的代码,以及所需要的操作系统核心和库。通过统一的名字空间和共用 API 来分配不同软件容器的可用硬件资源,创造出应用程序的独立沙箱运行环境,使得 Linux 用户可以容易的创建和管理系统或应用容器。
LXC 技术主要是借助 Linux 内核中提供的 CGroup 功能和 namespace 来实现的,通过 LXC 可以为软件提供一个独立的操作系统运行环境。
cgroup 和 namespace 介绍:
namespace 是 Linux 内核用来隔离内核资源的方式。 通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源 (如 cpu memory i/o 等等) 的机制。
cgroup 和 namespace 两者对比:
两者都是将进程进行分组,但是两者的作用还是有本质区别。namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。
每个进程在 Linux 系统中都拥有自己的命名空间(namespace).Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。
目前,Linux内核里面实现了7种不同类型的namespace。
名称 宏定义 隔离内容
Cgroup CLONE_NEWCGROUP Cgroup root directory (since Linux 4.6)
IPC CLONE_NEWIPC System V IPC, POSIX message queues (since Linux 2.6.19)
Network CLONE_NEWNET Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount CLONE_NEWNS Mount points (since Linux 2.4.19)
PID CLONE_NEWPID Process IDs (since Linux 2.6.24)
User CLONE_NEWUSER User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS CLONE_NEWUTS Hostname and NIS domain name (since Linux 2.6.19)
注意: 由于Cgroup namespace在4.6的内核中才实现,并且和cgroup v2关系密切,现在普及程度还不高,比如docker现在就还没有用它,所以在namespace这个系列中不会介绍Cgroup namespace。
当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例:
UTS Namespace 用来隔离系统的主机名、hostname 和 NIS 域名。
IPC 就是在不同进程间传递和交换信息。IPC Namespace 使得容器内的所有进程,进行的数据传输、共享数据、通知、资源共享等范围控制在所属容器内部,对宿主机和其他容器没有干扰。
Mount Namespace 用来隔离文件系统的挂载点,不同的 Mount namespace 拥有各自独立的挂载点信息。在 Docker 这样的容器引擎中,Mount namespace 的作用就是保证容器中看到的文件系统的视图。
PID namespaces用来隔离进程的 ID 空间,使得不同容器里的进程 ID 可以重复,相互不影响。
PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。
network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。
每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。
当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起
标记为"local devices"的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。
User namespace用来隔离user权限相关的Linux资源,包括user IDs and group IDs,keys , 和capabilities.
cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。
cgroup分v1和v2两个版本,v1实现较早,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2的出现就是为了解决v1中这方面的问题,在最新的4.5内核中,cgroup v2声称已经可以用于生产环境了,但它所支持的功能还很有限,随着v2一起引入内核的还有cgroup namespace。v1和v2可以混合使用,但是这样会更复杂,所以一般没人会这样用。
用于:
cgroup 有以下几个关键概念:
mount | grep "type cgroup"
可以查看。层级是作为控制组的根目录,来绑定controller,来达到对资源的控制。注意:
- 每个层级需要绑定 controller 来进行资源控制
- 系统中可以存在多个层级,整个 Cgroups 的结构应该是多个树状结构
- 子控制组分配的资源不能超过父控制组分配的资源
相互之间的关系:
controller
controller
是可以同时附加到多个hierarchy
,但是一个已经附加到某个层级的controller不能附加到其他含有其他controller的层级上,也就是说绑定多个层级后,这些层级都是只有唯一controller。注意(v1版本):
在Linux 2.6.24当中首次发行了 cgroups 的实现,各种 cgroup controller 之间的不协调导致了 cgroup hierarchy 管理变得非常复杂。
由于Version 1当中的问题,Linux 3.10开始设计一个全新的的cgroups实现来解决问题,最终在Linux 4.5版本中官方发行,即Version 2。
Cgroups v2 希望完全取代 Cgroups v1, 但是为了兼容,v1 并没有被移除,而且很多场景下都会作为系统的默认设置,当前的系统中可以同时使用 Cgroups v2 和 v1,但是一个 controller 只能选择一个版本使用。
在当前,系统中默认使用的是 cgroup v1。
重新启动内核时,添加一个参数systemd.unified_cgroup_hierarchy=1
, 如:
grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1
与 v1 不同,cgroup v2 只有单个层级树(single hierarchy)。 用如下命令挂载 v2 hierarchy:
# mount -t <fstype> <device> <dir>
$ mount -t cgroup2 none $MOUNT_POINT
cgroupv2 文件系统 的 magic number 是 0x63677270 (“cgrp”)。
兼容性:
这说明我们能以完全后向兼容的方式,混用 v2 和 v1 hierarchy。
Controller
即Cgroups的资源控制器,用于独立控制一种资源。
控制器 | 用途 |
---|---|
blkio | 限制cgroups中task的块设备io |
cpu | 限制控制组下所有任务对 CPU 的使用 |
cpuacct | 自动生成控制组中任务对 CPU 资源使用情况的报告 |
cpuset | 为控制组中任务分配独立 CPU(针对多处理器系统) 和内存 |
devices | 控制任务对设备的访问 |
freezer | 挂起或恢复 控制组 中的任务 |
memory | 限制控制组的内存使用量,自动生成任务对内存的使用情况报告 |
pids | 限制控制组中进程可以派生出的进程数量 |
net_cls | 通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(Traffic Controller, TC) 识别从具体 cgroup 中生成的数据包 |
net_prio | 限制任务中网络流量的优先级 |
perf_event | 可以对控制组中的任务进行统一的性能测试 |
huge_tlb | 限制对 Huge page 的使用 |
rdma | 限制 RDMA/IB 资源 |
Version 2 的新变化:
Cgroups v2
提供a unified hierarchy against which all controllers are mounted
no Internal process
,除了root cgroup之外,process 只能存在于leaf node(即 不能包含child cgroups
的cgroups)之中。cgroup.controllers
和cgroup.subtree_control
来指定Active cgroups
tasks
文件。cpuset
controller使用的cgroup.clone_chiledren
被移除。cgroup.events
提供对于empty cgroup的增强版提醒机制cgroup V2当中支持的Controller:
联合文件系统(Union File System):2004年由纽约州立大学石溪分校开发,它可以把多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS允许只读和可读写目录并存,就是说可同时删除和增加内容。UnionFS应用的地方很多,比如在多个磁盘分区上合并不同文件系统的主目录,或把几张CD光盘合并成一个统一的光盘目录(归档)。另外,具有写时复制(copy-on-write)功能UnionFS可以把只读和可读写文件系统合并在一起,虚拟上允许只读文件系统的修改可以保存到可写文件系统当中。
写时复制:copy-on-write,简写为 CoW,也叫隐式共享,是一种提高资源使用效率的资源管理技术。它的思想是:如果一个资源是重复的,在没有对资源做出修改前,并不需要立即复制出一个新的资源实例,这个资源被不同的所有者共享使用。当任何一个所有者要对该资源做出修改时,复制出一个新的资源实例给该所有者进行修改,修改后的资源成为其所有者的私有资源。通过这种资源共享的方式,可以显著地减少复制相同资源带来的消耗,但是这样做也会在进行资源的修改时增加一部分开销。
任何程序运行时都会有依赖,无论是开发语言层的依赖库,还是各种系统lib、操作系统等,不同的系统上这些库可能是不一样的,或者有缺失的。为了让容器运行时一致,docker将依赖的操作系统、各种lib依赖整合打包在一起(即镜像),然后容器启动时,作为它的根目录(根文件系统rootfs),使得容器进程的各种依赖调用都在这个根目录里,这样就做到了环境的一致性。
Docker镜像的设计中,引入了层(layer)的概念,也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs(一个目录),这样应用A和应用B所在的容器共同引用相同的Debian操作系统层、Golang环境层(作为只读层),而各自有各自应用程序层,和可写层。启动容器的时候通过UnionFS把相关的层挂载到一个目录,作为容器的根文件系统。
需要注意的是,rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。