目前Docker底层依赖的核心技术主要包括Linux操作系统的命名空间(Namespace)、控制组(Control Group)、联合文件系统(Union File System)和Linux网络虚拟化支持。
一.命名空间(NameSpace)
命名空间(namespace)是Linux内核的一个强大特性,为容器虚拟化的实现带来极大便利。
在操作系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源,所有的资源都是应用进程直接共享的。要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等的相互隔离。前者相对容易实现一些,后者则需要宿主主机系统的深入支持。
1.进程命名空间
2.网络命名空间
3.IPC命名空间
4.挂载命名空间
5.UTS命名空间
6.用户命名空间
通过使用隔离的用户命名空间可以提高安全性,避免容器内进程获取到额外的权限。
二.控制组(CGroups)
控制组(CGroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等;控制组可以提供对容器的内存、CPU、磁盘IO等资源进行限制和计费管理。
控制组提供:
1.资源限制(Resource limiting):可以将组设置为不超过设定的内存限制。比如:内存子系统可以为进程组设定一个内存使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发Out of Memory警告。
2.优先级(Prioritization):通过优先级让一些组优先得到更多的CPU等资源。
3.资源审计(Accounting):用来统计系统实际上把多少资源用到适合的目的上,可以使用cpuacct子系统记录某个进程组使用的CPU时间。
4.隔离(isolation):为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。
5.控制(Control):挂起、恢复和重启动等操作。
安装Docker后,用户可以在/sys/fs/cgroup/memory/docker/目录下看到对Docker组应用的各种限制项
#/sys/fs/cgroup 目录含义:
blkio — 这个子系统为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等)。
cpu — 这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问。
cpuacct — 这个子系统自动生成 cgroup 中任务所使用的 CPU 报告。
cpuset — 这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。
devices — 这个子系统可允许或者拒绝 cgroup 中的任务访问设备。
freezer — 这个子系统挂起或者恢复 cgroup 中的任务。
memory — 这个子系统设定 cgroup 中任务使用的内存限制,并自动生成内存资源使用报告。
net_cls — 这个子系统使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序(tc)识别从具体 cgroup 中生成的数据包。
net_prio — 这个子系统用来设计网络流量的优先级
hugetlb — 这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统。
三.联合文件系统(Union File System)
联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。
联合文件系统是实现Docker镜像的技术基础。Docker镜像可以通过分层来进行继承。例如,用户基于基础镜像(用来生成其他镜像的基础,往往没有父镜像)来制作各种不同的应用镜像。这些镜像共享同一个基础镜像层,提高了存储效率。此外,当用户改变了一个Docker镜像(比如升级程序到新的版本),则会创建一个新的层(layer)。因此,用户不用替换整个原镜像或者重新建立,只需要添加新层即可。用户分发镜像的时候,也只需要分发被改动的新层内容(增量部分)。这让Docker的镜像管理变得十分轻量级和快速。
1.Docker存储
Docker目前通过插件化方式支持多种文件系统后端。Debian/Ubuntu上成熟的AUFS(Another Union File System,或v2版本往后的Advanced Multilayered Unification File System),就是一种联合文件系统实现。AUFS支持为每一个成员目录(类似Git的分支)设定只读(readonly)、读写(readwrite)或写出(whiteout-able)权限,同时AUFS里有一个类似分层的概念,对只读权限的分支可以在逻辑上进行增量地修改(不影响只读部分的)。
Docker镜像自身就是由多个文件层组成,每一层有唯一的编号(层ID)。
可以通过docker history查看一个镜像由哪些层组成。例如查看ubuntu:14.04镜像由4层组成,每层执行了不同的命令:
$ docker history ubuntu:14.04
IMAGE CREATED CREATED BY SIZE COMMENT
2a274e3405ec 13 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
df697c8b1bf4 13 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB
371166fb96e0 13 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 194.5 kB
69191ca023af 13 months ago /bin/sh -c #(nop) ADD file:c8f078961a543cdefa 188.1 MB
对于Docker镜像来说,这些层的内容都是不可修改的、只读的。而当Docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂载一个新的可读写的层给容器。容器中的内容更新将会发生在可读写层。当所操作对象位于较深的某层时,需要先复制到最上层的可读写层。当数据对象较大时,往往意味着IO性能较差。因此,一般推荐将容器修改的数据通过volume方式挂载,而不是直接修改镜像内数据。此外,对于频繁启停Docker容器的场景下,文件系统的IO性能也将十分关键。具体看,Docker所有的存储都在Docker目录下,以Ubuntu系统为例,默认路径是/var/lib/docker。
在这个目录下面,存储由Docker镜像和容器运行相关的文件和目录,可能包括aufs、containers、graph、image、init、linkgraph.db、network、repositories-aufs、swarm、tmp、trust、volumes等。
最关键的就是aufs目录,这是aufs文件系统所在,保存Docker镜像相关数据和信息。该目录包括layers、diff和mnt三个子目录。1.9版本和之前的版本中,命名跟镜像层的ID是匹配的,而自1.10开始,层数据相关的文件和目录名与层ID不再匹配。
layers子目录包含层属性文件,用来保存各个镜像层的元数据:某镜像的某层下面包括哪些层。
例如:某镜像由5层组成,则文件内容应该如下:
cat aufs/layers/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init
d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6
29460ac934423a55802fcad24856827050697b4a9f33550bd93c82762fb6db8f
b670fb0c7ecd3d2c401fbfd1fa4d7a872fbada0a4b8c2516d0be18911c6b25d6
83e4dde6b9cfddf46b75a07ec8d65ad87a748b98cf27de7d5b3298c1f3455ae4
diff子目录包含层内容子目录,用来保存所有镜像层的内容数据。
例如:# ls aufs/diff/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init/
dev etc
mnt子目录下面的子目录是各个容器最终的挂载点,所有相关的AUFS层在这里挂载到一起,形成最终效果。一个运行中容器的根文件系统就挂载在这下面的子目录上。同样,1.10版本之前的Docker中,子目录名和容器ID是一致的。其中,还包括容器的元数据、配置文件和运行日志等。
2.多种文件系统比较
Docker目前支持的联合文件系统种类包括AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。
各种文件系统目前的支持情况如下:
AUFS:最早支持的文件系统,对Debian/Ubuntu支持好,虽然没有合并到Linux内核中,但成熟度很高;
OverlayFS:类似于AUFS,性能更好一些,已经合并到内核,未来会取代AUFS,但成熟度有待提高;
Device Mapper:Redhat公司和Docker团队一起开发用于支持RHEL的文件系统,内核支持,性能略慢,成熟度高;
btrfs:参考zfs等特性设计的文件系统,由Linux社区开发,试图未来取代Device Mapper,成熟度有待提高;
vfs:基于普通文件系统(ext、nfs等)的中间层抽象,性能差,比较占用空间,成熟度也一般。
zfs:最初设计为Solaris 10上的写时文件系统,拥有不少好的特性,但对Linux支持还不够成熟。
总结一下,AUFS和Device Mapper的应用最为广泛,支持也相对成熟,推荐生产环境考虑。长期来看,OverlayFS将可能具有更好的特性。
四.Linux网络虚拟化支持。
Docker的本地网络实现其实就是利用了Linux上的网络命名空间和虚拟网络设备(特别是veth pair)。
1.基本原理
直观上看,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;此外,如果不同子网之间要进行通信,需要额外的路由机制。
Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高。这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度要快得多。
Docker容器网络就很好地利用了Linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做veth pair)。
一般情况下,Docker创建一个容器的时候,会具体执行如下操作:
创建一对虚拟接口,分别放到本地主机和新容器的命名空间中;
本地主机一端的虚拟接口连接到默认的docker0网桥或指定网桥上,并具有一个以veth开头的唯一名字,如veth1234;
容器一端的虚拟接口将放到新创建的容器中,并修改名字作为eth0。这个接口只在容器的命名空间可见;
从网桥可用地址段中获取一个空闲地址分配给容器的eth0(例如172.17.0.2/16),并配置默认路由网关为docker0网卡的内部接口docker0的IP地址(例如172.17.42.1/16)。
完成这些之后,容器就可以使用它所能看到的eth0虚拟网卡来连接其他容器和访问外部网络。用户也可以通过docker network命令来手动管理网络。
在使用docker run命令启动容器的时候,可以通过–net参数来指定容器的网络配置。
有5个可选值bridge、none、container、host和用户定义的网络:
–net=bridge:默认值,在Docker网桥docker0上为容器创建新的网络栈。
–net=none:让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后,用户可以自行进行配置。
–net=container:NAME_or_ID:让Docker将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享IP地址和端口等网络资源,两者进程可以直接通过lo环回接口通信。
–net=host:告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其他root进程一样打开低范围的端口,可以访问本地网络服务,比如D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用–privileged=true参数,容器甚至会被允许直接配置主机的网络栈。
–net=user_defined_network:用户自行用network相关命令创建一个网络,通过这种方式将容器连接到指定的已创建网络上去。
用户使用–net=none后,Docker将不对容器网络进行配置。
下面,将手动完成配置网络的整个过程。
首先,启动一个/bin/bash容器,指定–net=none参数:
$ docker run -i -t --rm --net=none base /bin/bash
root@63f36fc01b5f:/#
在本地主机查找容器的进程id,并为它创建网络命名空间:
$ docker inspect -f '{{.State.Pid}}' 63f36fc01b5f
2778
$ pid=2778
$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid
检查桥接网卡的IP和子网掩码信息:
$ ip addr show docker0
21: docker0: ...
inet 172.17.42.1/16 scope global docker0
…
创建一对“veth pair”接口A和B,绑定A接口到网桥docker0,并启用它:
$ sudo ip link add A type veth peer name B
$ sudo brctl addif docker0 A
$ sudo ip link set A up
将B接口放到容器的网络命名空间,命名为eth0,启动它并配置一个可用IP(桥接网段)和默认网关:
$ sudo ip link set B netns $pid
$ sudo ip netns exec $pid ip link set dev B name eth0
$ sudo ip netns exec $pid ip link set eth0 up
$ sudo ip netns exec $pid ip addr add 172.17.42.99/16 dev eth0
$ sudo ip netns exec $pid ip route add default via 172.17.42.1
以上,就是Docker配置网络的具体过程。
当容器终止后,Docker会清空容器,容器内的网络接口会随网络命名空间一起被清除,A接口也会自动从docker0卸载并清除。
此外,在删除/var/run/netns/下的内容之前,用户可以使用ip netns exec命令在指定网络命名空间中进行配置,从而更新容器内的网络配置。