关于Docker实现原理,简单总结如下:
使用Namespaces实现了系统环境的隔离,Namespaces允许一个进程以及它的子进程从共享的宿主机内核资源(网络栈、进程列表、挂载点等)里获得一个仅自己可见的隔离区域,让同一个Namespace下的所有进程感知彼此变化,对外界进程一无所知,仿佛运行在一个独占的操作系统中;
使用CGroups限制这个环境的资源使用情况,比如一台16核32GB的机器上只让容器使用2核4GB。使用CGroups还可以为资源设置权重,计算使用量,操控任务(进程或线程)启停等;
使用镜像管理功能,利用Docker的镜像分层、写时复制、内容寻址、联合挂载技术实现了一套完整的容器文件系统及运行环境,再结合镜像仓库,镜像可以快速下载和共享,方便在多环境部署。
• Docker 引擎管理着镜像,然后移交给 containerd 运行,containerd 再使用 runC 运行容器。
• Containerd 是一个简单的守护进程,它可以使用 runC 管理容器,使用 gRPC 暴露容器的其他功能。它管理容器的开始,停止,暂停和销毁。由于容器运行时是孤立的引擎,引擎最终能够启动和升级而无需重新启动容器。
• runC是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。runC基本上是一个小命令行工具且它可以不用通过Docker引擎,直接就可以使用容器。
因此,容器中的主应用在 host 上的父进程是 containerd-shim,是它通过工具 runC 来启动这些进程的。
这也能看出来,pid namespace 通过将 host 上 PID 映射为容器内的 PID, 使得容器内的进程看起来有个独立的 PID 空间。
2.2 UTS namespace
类似地,容器可以有自己的 hostname 和 domainname:
root@devstack:/home/sammy# hostname
devstack
root@devstack:/home/sammy# docker exec -it web31 hostname
8b7dd09fbcae
2.3 user namespace
2.3.1 Linux 内核中的 user namespace
老版本中,Linux 内核里面只有一个数据结构负责处理用户和组。内核从3.8 版本开始实现了 user namespace。通过在 clone() 系统调用中使用 CLONE_NEWUSER 标志,一个单独的 user namespace 就会被创建出来。在新的 user namespace 中,有一个虚拟的用户和用户组的集合。这些用户和用户组,从 uid/gid 0 开始,被映射到该 namespace 之外的 非 root 用户。
在现在的linux内核中,管理员可以创建成千上万的用户。这些用户可以被映射到每个 user namespace 中。通过使用 user namespace 功能,不同的容器可以有完全不同的 uid 和 gid 数字。容器 A 中的 User 500 可能被映射到容器外的 User 1500,而容器 B 中的 user 500 可能被映射到容器外的用户 2500.
为什么需要这么做呢?因为在容器中,提供 root 访问权限有其特殊用途。想象一下,容器 A 中的 root 用户 (uid 0) 被映射到宿主机上的 uid 1000,容器B 中的 root 被映射到 uid 2000.类似网络端口映射,这允许管理员在容器中创建 root 用户,而不需要在宿主机上创建。
从内核的提交日志上看,user namespace 是 linux 内核 3.8 版本中引入的,而 RedHat 企业版 7 的 linux 内核版本是 3.10,但 7.1版本并不支持 user namespace。这是为什么呢?实际上,在 Fedora 项目中,Redhat 已经在 user namespace 上已经投入了很长时间了,而且认为这是一个非常重要的功能。因此,我们并没有在 7.1 中启用 user namespace,直到我们认为它满足了生产要求为止。而新版本的 Fedora 已经启用了该功能了。在最新的 RedHat 企业版 Linux 7.4 版本中,已经正式启用了 user namespace:
(引用自 https://www.redhat.com/cms/managed-files/li-new-in-rhel74-technology-overview-f10498kc-201801-en.pdf)
2.3.2 Docker 对 user namespace 的支持
在 Docker 1.10 版本之前,Docker 是不支持 user namespace。也就是说,默认地,容器内的进程的运行用户就是 host 上的 root 用户,这样的话,当 host 上的文件或者目录作为 volume 被映射到容器以后,容器内的进程其实是有 root 的几乎所有权限去修改这些 host 上的目录的,这会有很大的安全问题。
举例:
• 启动一个容器: docker run -d -v /bin:/host/bin --name web34 training/webapp python app.py
• 此时进程的用户在容器内和外都是root,它在容器内可以对 host 上的 /bin 目录做任意修改:
root@devstack:/home/sammy# docker exec -ti web34 id
uid=0(root) gid=0(root) groups=0(root)
root@devstack:/home/sammy# id
uid=0(root) gid=0(root) groups=0(root)
而 Docker 1.10 中引入的 user namespace 就可以让容器有一个 “假”的 root 用户,它在容器内是 root,它被映射到容器外一个非 root 用户。也就是说,user namespace 实现了 host users 和 container users 之间的映射。
启用步骤:
[root@node1 1573]# uname -a
Linux node1.exampleos.com 3.10.0-514.2.2.el7.x86_64 #1 SMP Tue Dec 6 23:06:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
[root@node1 1573]# cat /boot/config-3.10.0-514.2.2.el7.x86_64 | grep CONFIG_USER_NS
CONFIG_USER_NS=y
如果是 「y」,则启用了,否则未启用。同样地,可以查看其它 namespace:
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
2.3.4 在 Centos/RedHat Linux 7 中启用 user namespace
资料来源:https://github.com/procszoo/procszoo/wiki/How-to-enable-“user”-namespace-in-RHEL7-and-CentOS7%3F
这两个版本中,默认 user namespace 是未被启用的。
运行下面的命令,然后运行 reboot,就可以启用了:
grubby --args=“user_namespace.enable=1” --update-kernel=" ( g r u b b y − − d e f a u l t − k e r n e l ) " 运 行 下 面 的 命 令 , 然 后 运 行 r e b o o t , 就 关 闭 了 : g r u b b y − − r e m o v e − a r g s = " u s e r n a m e s p a c e . e n a b l e = 1 " − − u p d a t e − k e r n e l = " (grubby --default-kernel)" 运行下面的命令,然后运行 reboot,就关闭了: grubby --remove-args="user_namespace.enable=1" --update-kernel=" (grubby−−default−kernel)"运行下面的命令,然后运行reboot,就关闭了:grubby−−remove−args="usernamespace.enable=1"−−update−kernel="(grubby --default-kernel)"
2.3.5 OpenShift 对 user namespace 的支持
在 OpenShift 3.11 版本中,应该还不支持 user namespace,下面是 dockerd 进程:
/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc
–exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current
–init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json
–signature-verification=False --storage-driver overlay2 --mtu=1400
[root@node1 1573]# ls
attr cgroup comm cwd fd io map_files mountinfo net oom_adj pagemap root sessionid stack status timers
autogroup clear_refs coredump_filter environ fdinfo limits maps mounts ns oom_score personality sched setgroups stat syscall uid_map
auxv cmdline cpuset exe gid_map loginuid mem mountstats numa_maps oom_score_adj projid_map schedstat smaps statm task wchan
[root@node1 1573]# cat uid_map
0 0 4294967295
[root@node1 1573]# cat gid_map
0 0 4294967295
正是/proc//uid_map 和 /proc//gid_map 这两个文件, 把容器中的uid和真实系统的uid给映射在一起。这两个文件的格式为:
ID-inside-ns ID-outside-ns length
其中:
• 第一个字段ID-inside-ns表示在容器显示的UID或GID,
• 第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。
• 第三个字段表示映射的范围,一般填1,表示一一对应。
举个例子, 0 1000 256这条配置就表示父user namespace中的1000~1256映射到新user namespace中的0~256。
比如,把真实的uid=1000映射成容器内的uid=0:
把namespace内部从0开始的uid映射到外部从0开始的uid,其最大范围是无符号32位整形:
上面的截图中正是后面这种情形,也就是容器中的 uid 和宿主机上的 uid 是从0开始一一对应着映射的。
备注:linux user namespace 非常复杂,应该是所有 namespace 中最复杂的一个。这里只是一个简单介绍,还进一步理解,还需要阅读更多材料,比如 https://lwn.net/Articles/532593/ 系列文章。
2.4 network namespace
默认情况下,当 docker 实例被创建出来后,使用 ip netns 命令无法看到容器实例对应的 network namespace。这是因为 ip netns 命令是从 /var/run/netns 文件夹中读取内容的。
步骤:
root@devstack:/home/sammy# ip netns
web5
root@devstack:/home/sammy# ip netns exec web5 ip addr
1: lo:
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
15: eth0:
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:3/64 scope link
valid_lft forever preferred_lft forever
其他的几个 namespace,比如 network,mnt 等,比较简单,这里就不多说了。总之,Docker 守护进程为每个容器都创建了六种namespace 的实例,使得容器中的进程都处于一种隔离的运行环境之中:
root@devstack:/proc/1726/ns# ls -l
total 0
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 ipc -> ipc:[4026532210]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 mnt -> mnt:[4026532208]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:44 net -> net:[4026532213]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 pid -> pid:[4026532211]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 user -> user:[4026532207]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 uts -> uts:[4026532209]
root@devstack:/proc/2962# docker run -d -v /bin:/host/bin --name web38 --pid host --userns host training/webapp python app.py
f40f6702b61e3028a6708cdd7b167474ddf2a98e95b6793a1326811fc4aa161d
root@devstack:/proc/2962#
root@devstack:/proc/2962# docker exec -it web38 bash
root@f40f6702b61e:/opt/webapp# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 33480 2768 ? Ss 17:40 0:01 /sbin/init
root 2 0.0 0.0 0 0 ? S 17:40 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 17:40 0:00 [ksoftirqd/0]
root 5 0.0 0.0 0 0 ? S< 17:40 0:00 [kworker/0:0H]
root 6 0.0 0.0 0 0 ? S 17:40 0:00 [kworker/u2:0]
root 7 0.0 0.0 0 0 ? S 17:40 0:00 [rcu_sched]
…
3.3 --uts
同样地,可以使容器使用 Docker host uts namespace。此时,最明显的是,容器的 hostname 和 Docker hostname 是相同的。
root@devstack:/proc/2962# docker run -d -v /bin:/host/bin --name web39 --uts host training/webapp python app.py
38e8b812e7020106bf8d3952b88085028fc87f4427af0c3b0a29b6a69c979221
root@devstack:/proc/2962# docker exec -it web39 bash
root@devstack:/opt/webapp# hostname
devstack
[root@node1 1573]# uname -r
3.10.0-514.2.2.el7.x86_64
[root@node1 1573]# cat /boot/config-3.10.0-514.2.2.el7.x86_64 | grep CGROUP
CONFIG_CGROUPS=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_HUGETLB=y
CONFIG_CGROUP_PERF=y
CONFIG_CGROUP_SCHED=y
CONFIG_BLK_CGROUP=y
CONFIG_NETFILTER_XT_MATCH_CGROUP=m
CONFIG_NET_CLS_CGROUP=y
CONFIG_NETPRIO_CGROUP=y
对应的 cgroup 的配置值如果是 ‘y’,则表示已经被启用了。
Linux 系统中,一切皆文件。Linux 也将 cgroups 实现成了文件系统,方便用户使用。在我的 Ubuntu 14.04 测试环境中:
root@devstack:/home/sammy# mount -t cgroup
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
systemd on /sys/fs/cgroup/systemd type cgroup (rw,noexec,nosuid,nodev,none,name=systemd)
root@devstack:/home/sammy# lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
root@devstack:/home/sammy# ls /sys/fs/cgroup/ -l
total 0
drwxr-xr-x 3 root root 0 Sep 18 21:46 blkio
drwxr-xr-x 3 root root 0 Sep 18 21:46 cpu
drwxr-xr-x 3 root root 0 Sep 18 21:46 cpuacct
drwxr-xr-x 3 root root 0 Sep 18 21:46 cpuset
drwxr-xr-x 3 root root 0 Sep 18 21:46 devices
drwxr-xr-x 3 root root 0 Sep 18 21:46 freezer
drwxr-xr-x 3 root root 0 Sep 18 21:46 hugetlb
drwxr-xr-x 3 root root 0 Sep 18 21:46 memory
drwxr-xr-x 3 root root 0 Sep 18 21:46 perf_event
drwxr-xr-x 3 root root 0 Sep 18 21:46 systemd
我们看到 /sys/fs/cgroup 目录中有若干个子目录,我们可以认为这些都是受 cgroups 控制的资源以及这些资源的信息。
• 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系统进行限制,这是一个大页文件系统。
默认的话,在 Ubuntu 系统中,你可能看不到 net_cls 和 net_prio 目录,它们需要你手工做 mount:
root@devstack:/sys/fs/cgroup# modprobe cls_cgroup
root@devstack:/sys/fs/cgroup# mkdir net_cls
root@devstack:/sys/fs/cgroup# mount -t cgroup -o net_cls none net_cls
root@devstack:/sys/fs/cgroup# modprobe netprio_cgroup
root@devstack:/sys/fs/cgroup# mkdir net_prio
root@devstack:/sys/fs/cgroup# mount -t cgroup -o net_prio none net_prio
root@devstack:/sys/fs/cgroup# ls net_prio/cgroup.clone_children cgroup.procs net_prio.ifpriomap notify_on_release tasks
cgroup.event_control cgroup.sane_behavior net_prio.prioidx release_agent
root@devstack:/sys/fs/cgroup# ls net_cls/
cgroup.clone_children cgroup.event_control cgroup.procs cgroup.sane_behavior net_cls.classid notify_on_release release_agent tasks
1.2 实验
1.2.1 通过 cgroups 限制进程的 CPU
写一段最简单的 C 程序:
int main(void)
{
int i = 0;
for(;? i++;
return 0;
}
编译,运行,发现它占用的 CPU 几乎到了 100%:
top - 22:43:02 up 1:14, 3 users, load average: 0.24, 0.06, 0.06 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2304 root 20 0 4188 356 276 R 99.6 0.0 0:11.77 hello
接下来我们做如下操作:
root@devstack:/home/sammy/c# mkdir /sys/fs/cgroup/cpu/hello
root@devstack:/home/sammy/c# cd /sys/fs/cgroup/cpu/hello
root@devstack:/sys/fs/cgroup/cpu/hello# ls
cgroup.clone_children cgroup.procs cpu.cfs_quota_us cpu.stat tasks
cgroup.event_control cpu.cfs_period_us cpu.shares notify_on_release
root@devstack:/sys/fs/cgroup/cpu/hello# cat cpu.cfs_quota_us
-1
root@devstack:/sys/fs/cgroup/cpu/hello# echo 20000 > cpu.cfs_quota_us
root@devstack:/sys/fs/cgroup/cpu/hello# cat cpu.cfs_quota_us
20000
root@devstack:/sys/fs/cgroup/cpu/hello# echo 2428 > tasks
然后再来看看这个进程的 CPU 占用情况:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2428 root 20 0 4188 356 276 R 19.9 0.0 0:46.03 hello
它占用的 CPU 几乎就是 20%,也就是我们预设的阈值。这说明我们通过上面的步骤,成功地将这个进程运行所占用的 CPU 资源限制在某个阈值之内了。
如果此时再启动另一个 hello 进程并将其 id 加入 tasks 文件,则两个进程会共享设定的 CPU 限制:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2428 root 20 0 4188 356 276 R 10.0 0.0 285:39.54 hello
12526 root 20 0 4188 356 276 R 10.0 0.0 0:25.09 hello
1.2.2 通过 cgroups 限制进程的 Memory
同样地,我们针对它占用的内存做如下操作:
root@devstack:/sys/fs/cgroup/memory# mkdir hello
root@devstack:/sys/fs/cgroup/memory# cd hello/
root@devstack:/sys/fs/cgroup/memory/hello# cat memory.limit_in_bytes
18446744073709551615
root@devstack:/sys/fs/cgroup/memory/hello# echo 64k > memory.limit_in_bytes
root@devstack:/sys/fs/cgroup/memory/hello# echo 2428 > tasks
root@devstack:/sys/fs/cgroup/memory/hello#
上面的步骤会把进程 2428 说占用的内存阈值设置为 64K。超过的话,它会被杀掉。
1.2.3 限制进程的 I/O
运行命令:
sudo dd if=/dev/sda1 of=/dev/null
通过 iotop 命令看 IO (此时磁盘在快速转动),此时其写速度为 242M/s:
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2555 be/4 root 242.60 M/s 0.00 B/s 0.00 % 61.66 % dd if=/dev/sda1 of=/dev/null
接着做下面的操作:
root@devstack:/home/sammy# mkdir /sys/fs/cgroup/blkio/io
root@devstack:/home/sammy# cd /sys/fs/cgroup/blkio/io
root@devstack:/sys/fs/cgroup/blkio/io# ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 Sep 18 21:46 /dev/sda1
root@devstack:/sys/fs/cgroup/blkio/io# echo ‘8:0 1048576’ > /sys/fs/cgroup/blkio/io/blkio.throttle.read_bps_device
root@devstack:/sys/fs/cgroup/blkio/io# echo 2725 > /sys/fs/cgroup/blkio/io/tasks
结果,这个进程的IO 速度就被限制在 1Mb/s 之内了:
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2555 be/4 root 990.44 K/s 0.00 B/s 0.00 % 96.29 % dd if=/dev/sda1 of=/dev/null
1.3 术语
cgroups 的术语包括:
• 任务(Tasks):就是系统的一个进程。
• 控制组(Control Group):一组按照某种标准划分的进程,比如官方文档中的Professor和Student,或是WWW和System之类的,其表示了某进程组。Cgroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组。而资源的限制是定义在这个组上,就像上面示例中我用的 hello 一样。简单点说,cgroup的呈现就是一个目录带一系列的可配置文件。
• 层级(Hierarchy):控制组可以组织成hierarchical的形式,既一颗控制组的树(目录结构)。控制组树上的子节点继承父结点的属性。简单点说,hierarchy就是在一个或多个子系统上的cgroups目录树。
• 子系统(Subsystem):一个子系统就是一个资源控制器,比如CPU子系统就是控制CPU时间分配的一个控制器。子系统必须附加到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。Cgroup的子系统可以有很多,也在不断增加中。
2. Docker 对 cgroups 的使用
2.1 默认情况
默认情况下,Docker 启动一个容器后,会在 /sys/fs/cgroup 目录下的各个资源目录下生成以容器 ID 为名字的目录(group),比如:
/sys/fs/cgroup/cpu/docker/03dd196f415276375f754d51ce29b418b170bd92d88c5e420d6901c32f93dc14
此时 cpu.cfs_quota_us 的内容为 -1,表示默认情况下并没有限制容器的 CPU 使用。在容器被 stopped 后,该目录被删除。
运行命令 docker run -d --name web41 --cpu-quota 25000 --cpu-period 100 --cpu-shares 30 training/webapp python app.py 启动一个新的容器,结果:
root@devstack:/sys/fs/cgroup/cpu/docker/06bd180cd340f8288c18e8f0e01ade66d066058dd053ef46161eb682ab69ec24# cat cpu.cfs_quota_us
25000
root@devstack:/sys/fs/cgroup/cpu/docker/06bd180cd340f8288c18e8f0e01ade66d066058dd053ef46161eb682ab69ec24# cat tasks
3704
root@devstack:/sys/fs/cgroup/cpu/docker/06bd180cd340f8288c18e8f0e01ade66d066058dd053ef46161eb682ab69ec24# cat cpu.cfs_period_us
2000
Docker 会将容器中的进程的 ID 加入到各个资源对应的 tasks 文件中。表示 Docker 也是以上面的机制来使用 cgroups 对容器的 CPU 使用进行限制。
相似地,可以通过 docker run 中 mem 相关的参数对容器的内存使用进行限制:
–cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
–kernel-memory string Kernel memory limit
-m, --memory string Memory limit
–memory-reservation string Memory soft limit
–memory-swap string Swap limit equal to memory plus swap: ‘-1’ to enable unlimited swap
–memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
比如 docker run -d --name web42 --blkio-weight 100 --memory 10M --cpu-quota 25000 --cpu-period 2000 --cpu-shares 30 training/webapp python app.py:
root@devstack:/sys/fs/cgroup/memory/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410# cat memory.limit_in_bytes
10485760
root@devstack:/sys/fs/cgroup/blkio/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410# cat blkio.weight
100
目前 docker 已经几乎支持了所有的 cgroups 资源,可以限制容器对包括 network,device,cpu 和 memory 在内的资源的使用,比如:
root@devstack:/sys/fs/cgroup# find -iname ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./net_prio/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./net_cls/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./systemd/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./hugetlb/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./perf_event/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./blkio/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./freezer/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./devices/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./memory/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./cpuacct/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./cpu/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
./cpuset/docker/ec8d850ebbabaf24df572cb5acd89a6e7a953fe5aa5d3c6a69c4532f92b57410
2.2 net_cls
net_cls 和 tc 一起使用可用于限制进程发出的网络包所使用的网络带宽。当使用 cgroups network controll net_cls 后,指定进程发出的所有网络包都会被加一个 tag,然后就可以使用其他工具比如 iptables 或者 traffic controller (TC)来根据网络包上的 tag 进行流量控制。关于 TC 的文档,网上很多,这里不再赘述,只是用一个简单的例子来加以说明。
关于 classid,它的格式是 0xAAAABBBB,其中,AAAA 是十六进制的主ID(major number),BBBB 是十六进制的次ID(minor number)。因此,0X10001 表示 10:1,而 0x00010001 表示 1:!。
(1)首先在host 的网卡 eth0 上做如下设置:
tc qdisc del dev eth0 root #删除已有的规则
tc qdisc add dev eth0 root handle 10: htb default 12
tc class add dev eth0 parent 10: classid 10:1 htb rate 1500kbit ceil 1500kbit burst 10k #限速
tc filter add dev eth0 protocol ip parent 10:0 prio 1 u32 match ip protocol 1 0xff flowid 10:1 #只处理 ping 参数的网络包
其结果是:
• 在网卡 eth0 上创建了一个 HTB root 队列,hangle 10: 表示队列句柄也就是major number 为 10
• 创建一个分类 10:1,限制它的出发网络带宽为 80 kbit (千比特每秒)
• 创建一个分类器,将 eth0 上 IP IMCP 协议 的 major ID 为 10 的 prio 为 1 的网络流量都分类到 10:1 类别
(2)启动容器
容器启动后,其 init 进程在host 上的 PID 就被加入到 tasks 文件中了:
root@devstack:/sys/fs/cgroup/net_cls/docker/ff8d9715b7e11a5a69446ff1e3fde3770078e32a7d8f7c1cb35d51c75768fe33# ps -ef | grep 10047
231072 10047 10013 1 07:08 ? 00:00:00 python app.py
设置 net_cls classid:
echo 0x100001 > net_cls.classid
再在容器启动一个 ping 进程,其 ID 也被加入到 tasks 文件中了。
(3)查看tc 情况: tc -s -d class show dev eth0
Every 2.0s: tc -s class ls dev eth0 Wed Sep 21 04:07:56 2016
class htb 10:1 root prio 0 rate 1500Kbit ceil 1500Kbit burst 10Kb cburst 1599b
Sent 17836 bytes 182 pkt (dropped 0, overlimits 0 requeues 0)
rate 0bit 0pps backlog 0b 0p requeues 0
lended: 182 borrowed: 0 giants: 0
tokens: 845161 ctokens: 125161
我们可以看到 tc 已经在处理 ping 进程产生的数据包了。再来看一下 net_cls 和 ts 合作的限速效果:
10488 bytes from 192.168.1.1: icmp_seq=35 ttl=63 time=12.7 ms
10488 bytes from 192.168.1.1: icmp_seq=36 ttl=63 time=15.2 ms
10488 bytes from 192.168.1.1: icmp_seq=37 ttl=63 time=4805 ms
10488 bytes from 192.168.1.1: icmp_seq=38 ttl=63 time=9543 ms
其中:
• 后两条说使用的 tc class 规则是 tc class add dev eth0 parent 10: classid 10:1 htb rate 1500kbit ceil 15kbit burst 10k
• 前两条所使用的 tc class 规则是 tc class add dev eth0 parent 10: classid 10:1 htb rate 1500kbit ceil 10Mbit burst 10k
3. Docker run 命令中 cgroups 相关命令
block IO:
–blkio-weight value Block IO (relative weight), between 10 and 1000
–blkio-weight-device value Block IO weight (relative device weight) (default [])
–cgroup-parent string Optional parent cgroup for the container
CPU:
–cpu-percent int CPU percent (Windows only)
–cpu-period int Limit CPU CFS (Completely Fair Scheduler) period
–cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota
-c, --cpu-shares int CPU shares (relative weight)
–cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
–cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
Device:
–device value Add a host device to the container (default [])
–device-read-bps value Limit read rate (bytes per second) from a device (default [])
–device-read-iops value Limit read rate (IO per second) from a device (default [])
–device-write-bps value Limit write rate (bytes per second) to a device (default [])
–device-write-iops value Limit write rate (IO per second) to a device (default [])
Memory:
–kernel-memory string Kernel memory limit
-m, --memory string Memory limit
–memory-reservation string Memory soft limit
–memory-swap string Swap limit equal to memory plus swap: ‘-1’ to enable unlimited swap
–memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
一些说明:
cgroup 只能限制 CPU 的使用,而不能保证CPU的使用。也就是说, 使用 cpuset-cpus,可以让容器在指定的CPU或者核上运行,但是不能确保它独占这些CPU;cpu-shares 是个相对值,只有在CPU不够用的时候才其作用。也就是说,当CPU够用的时候,每个容器会分到足够的CPU;不够用的时候,会按照指定的比重在多个容器之间分配CPU。
对内存来说,cgroups 可以限制容器最多使用的内存。使用 -m 参数可以设置最多可以使用的内存。
Docker 网络概况
用一张图来说明 Docker 网络的基本概况:
四种单节点网络模式
2.1 bridge 模式
Docker 容器默认使用 bridge 模式的网络。其特点如下:
• 使用一个 linux bridge,默认为 docker0
• 使用 veth 对,一头在容器的网络 namespace 中,一头在 docker0 上
• 该模式下Docker Container不具有一个公有IP,因为宿主机的IP地址与veth pair的 IP地址不在同一个网段内
• Docker采用 NAT 方式,将容器内部的服务监听的端口与宿主机的某一个端口port 进行“绑定”,使得宿主机以外的世界可以主动将网络报文发送至容器内部
• 外界访问容器内的服务时,需要访问宿主机的 IP 以及宿主机的端口 port
• NAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率。
• 容器拥有独立、隔离的网络栈;让容器和宿主机以外的世界通过NAT建立通信
• 关于容器通过 NAT 连接外网的原理,请参考我的另一篇文章 Netruon 理解(11):使用 NAT 将 Linux network namespace 连接外网。
iptables 的 SNTA 规则,使得从容器离开去外界的网络包的源 IP 地址被转换为 Docker 主机的IP地址:
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all – 172.17.0.0/16 0.0.0.0/0
MASQUERADE all – 172.18.0.0/16 0.0.0.0/0
效果是这样的:
(图片来源)
示意图:
2.2 Host 模式
定义:
Host 模式并没有为容器创建一个隔离的网络环境。而之所以称之为host模式,是因为该模式下的 Docker 容器会和 host 宿主机共享同一个网络 namespace,故 Docker Container可以和宿主机一样,使用宿主机的eth0,实现和外界的通信。换言之,Docker Container的 IP 地址即为宿主机 eth0 的 IP 地址。其特点包括:
•
o 这种模式下的容器没有隔离的 network namespace
o 容器的 IP 地址同 Docker host 的 IP 地址
o 需要注意容器中服务的端口号不能与 Docker host 上已经使用的端口号相冲突
o host 模式能够和其它模式共存
实验:
(1)启动一个 host 网络模式的容器
docker run -d --name hostc1 --network host -p 5001:5001 training/webapp python app.py
(2)检查其 network namespace,其中可以看到主机上的所有网络设备
root@docker2:/home/sammy# ln -s /proc/28353/ns/net /var/run/netns/hostc1
root@docker2:/home/sammy# ip netns
hostc1
root@docker2:/home/sammy# ip netns exec hostc1
No command specified
root@docker2:/home/sammy# ip netns exec hostc1 ip a
1: lo:
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0:
link/ether 08:00:27:d4:66:75 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.20/24 brd 192.168.1.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fed4:6675/64 scope link
valid_lft forever preferred_lft forever
…
示意图:
2.3 container 模式
定义:
Container 网络模式是 Docker 中一种较为特别的网络的模式。处于这个模式下的 Docker 容器会共享其他容器的网络环境,因此,至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。
实验:
(1)启动一个容器:
docker run -d --name hostcs1 -p 5001:5001 training/webapp python app.py
(2)启动另一个容器,并使用第一个容器的 network namespace
docker run -d --name hostcs2 --network container:hostcs1 training/webapp python app.py
注意:因为此时两个容器要共享一个 network namespace,因此需要注意端口冲突情况,否则第二个容器将无法被启动。
示意图:
2.4 none 模式
定义:
网络模式为 none,即不为 Docker 容器构造任何网络环境。一旦Docker 容器采用了none 网络模式,那么容器内部就只能使用loopback网络设备,不会再有其他的网络资源。Docker Container的none网络模式意味着不给该容器创建任何网络环境,容器只能使用127.0.0.1的本机网络。
实验:
(1)创建并启动一个容器: docker run -d --name hostn1 --network none training/webapp python app.py
(2)检查其网络设备,除了 loopback 设备外没有其它设备
root@docker2:/home/sammy# ip netns exec hostn1 ip a
1: lo:
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3. 多节点 Docker 网络
Docker 多节点网络模式可以分为两类,一类是 Docker 在 1.19 版本中引入的基于 VxLAN 的对跨节点网络的原生支持;另一种是通过插件(plugin)方式引入的第三方实现方案,比如 Flannel,Calico 等等。
3.1 Docker 原生overlay 网络
Docker 1.19 版本中增加了对 overlay 网络的原生支持。Docker 支持 Consul, Etcd, 和 ZooKeeper 三种分布式key-value 存储。其中,etcd 是一个高可用的分布式 k/v存储系统,使用etcd的场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。
3.1.1 安装配置
准备三个节点:
• devstack 192.168.1.18
• docker1 192.168.1.21
• docker2 192.168.1.19
在 devstack 上使用Docker 启动 etcd 容器:
export HostIP=“192.168.1.18”
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379
–name etcd quay.io/coreos/etcd
/usr/local/bin/etcd
-name etcd0
-advertise-client-urls http:// H o s t I P : 2379 , h t t p : / / {HostIP}:2379,http:// HostIP:2379,http://{HostIP}:4001
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001
-initial-advertise-peer-urls http://KaTeX parse error: Expected 'EOF', got '\ ' at position 15: {HostIP}:2380 \̲ ̲ -listen-peer-u…{HostIP}:2380
-initial-cluster-state new
使用 Docker 启动 etcd 请参考 https://coreos.com/etcd/docs/latest/docker_guide.html。不过,应该是因为制造镜像所使用的Dockerfile 原因,官网上的命令因为少了上面红色字体部分而会造成启动失败:
b847195507addf4fb5a01751eb9c4101416a13db4a8a835e1c2fa1db1e6f364e
docker: Error response from daemon: oci runtime error: exec: “-name”: executable file not found in $PATH.
添加红色部分后,容器可以被正确创建:
root@devstack:/# docker exec -it 179cd52b494d /usr/local/bin/etcdctl cluster-health
member 5d72823aca0e00be is healthy: got healthy result from http://:2379
cluster is healthy
root@devstack:/home/sammy# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
179cd52b494d quay.io/coreos/etcd "/usr/local/bin/etcd " 8 seconds ago Up 8 seconds 0.0.0.0:2379-2380->2379-2380/tcp, 0.0.0.0:4001->4001/tcp etcd
root@devstack:/home/sammy# netstat -nltp | grep 2380
tcp6 0 0 :::2380 ::? LISTEN 4072/docker-proxy
root@devstack:/home/sammy# netstat -nltp | grep 4001
tcp6 0 0 :::4001 ::? LISTEN 4047/docker-proxy
在docker1 和 docker2 节点上修改 /etc/default/docker,添加:
DOCKER_OPTS="–cluster-store=etcd://192.168.1.18:2379 --cluster-advertise=192.168.1.20:2379"
然后分别重启 docker deamon。注意,要使用IP地址;要是使用 hostname 的话,docker 服务将启动失败:
root@docker2:/home/sammy# docker ps
An error occurred trying to connect: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json: read unix @->/var/run/docker.sock: read: connection reset by peer
3.1.2 使用 Docker overlay 网络
(1)在docker1上运行下面的命令创建一个 overlay 网络:
root@docker1:/home/sammy# docker network create -d overlay overlaynet1
1de982804f632169380609b9be7c1466b0064dce661a8f4c9e30d781e79fc45a
root@docker1:/home/sammy# docker network inspect overlaynet1
[
{
“Name”: “overlaynet1”,
“Id”: “1de982804f632169380609b9be7c1466b0064dce661a8f4c9e30d781e79fc45a”,
“Scope”: “global”,
“Driver”: “overlay”,
“EnableIPv6”: false,
“IPAM”: {
“Driver”: “default”,
“Options”: {},
“Config”: [
{
“Subnet”: “10.0.0.0/24”,
“Gateway”: “10.0.0.1/24”
}
]
},
“Internal”: false,
“Containers”: {},
“Options”: {},
“Labels”: {}
}
]
在 docker2 上你也会看到这个网络,说明通过 etcd,网络数据是分布式而不是本地的了。
(2)在网络中创建容器
在 docker2 上,运行 docker run -d --name over2 --network overlaynet1 training/webapp python app.py
在 docker1 上,运行 docker run -d --name over1 --network overlaynet1 training/webapp python app.py
进入容器 over2,发现它有两块网卡:
root@docker2:/home/sammy# ln -s /proc/23576/ns/net /var/run/netns/over2
root@docker2:/home/sammy# ip netns
over2
root@docker2:/home/sammy# ip netns exec over2 ip a
22: eth0:
link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe00:2/64 scope link
valid_lft forever preferred_lft forever
24: eth1:
link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.2/16 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe13:2/64 scope link
valid_lft forever preferred_lft forever
其中 eth1 的网络是一个内部的网段,其实它走的还是普通的 NAT 模式;而 eth0 是 overlay 网段上分配的IP地址,也就是它走的是 overlay 网络,它的 MTU 是 1450 而不是 1500.
进一步查看它的路由表,你会发现只有同一个 overlay 网络中的容器之间的通信才会通过 eth0,其它所有通信还是走 eth1.
root@docker2:/home/sammy# ip netns exec over2 route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.19.0.1 0.0.0.0 UG 0 0 0 eth1
10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
172.19.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth1
先看此时的网络拓扑图:
可见:
• Docker 在每个节点上创建了两个 linux bridge,一个用于 overlay 网络(ov-000100-1de98),一个用于非 overlay 的 NAT 网络(docker_gwbridge)
• 容器内的到overlay 网络的其它容器的网络流量走 overlay 网卡(eth0),其它网络流量走 NAT 网卡(eth1)
• 当前 Docker 创建 vxlan 隧道的ID范围为 256~1000,因而最多可以创建745个网络,因此,本例中的这个 vxlan 隧道使用的 ID 是 256
• Docker vxlan 驱动使用 4789 UDP 端口
• overlay网络模型底层需要类似 consul 或 etcd 的 KV 存储系统进行消息同步
• Docker overlay 不使用多播
• Overlay 网络中的容器处于一个虚拟的大二层网络中
• 关于 linux bridge + vxlan 组网,请参考 Neutron 理解(14):Neutron ML2 + Linux bridge + VxLAN 组网
• 关于 linux network namspace + NAT 组网,请参考 Netruon 理解(11):使用 NAT 将 Linux network namespace 连接外网
• github 上代码在这里 https://github.com/docker/libnetwork/blob/master/drivers/overlay/
ov-000100-1de98 的初始情形:
root@docker1:/home/sammy# ip -d link show dev vx-000100-1de98
8: vx-000100-1de98:
link/ether 22:3c:3f:8f:94:f6 brd ff:ff:ff:ff:ff:ff promiscuity 1
vxlan id 256 port 32768 61000 proxy l2miss l3miss ageing 300
root@docker1:/home/sammy# bridge fdb show dev vx-000100-1de98
22:3c:3f:8f:94:f6 vlan 0 permanent
这里很明显的一个问题是,vxlan dev vx-000100-1de98 的 fdb 表内容不全,导致从容器1 ping 容器2 不通。待选的解决方式不外乎下面几种:
• 使用一个中央数据库,它保存所有容器的 IP 地址和所在节点的 IP 地址的映射关系
• 使用多播
• 使用比如 BGP 的特殊协议来广告(advertise)容器的 IP 和所在节点的 IP 的映射关系
Docker 从某种程度上利用了第一种和第三种方式的组合,首先Docker 利用 consul 以及 etcd 这样的分布式 key/value 存储来保存IP地址映射关系,另一方面个Docker 节点也通过某种协议来直接广告映射关系。
为了测试,中间重启了 docker1 节点,发现 over1 容器无法启动,报错如下:
docker: Error response from daemon: network sandbox join failed: could not get network sandbox (oper true): failed get network namespace “”: no such file or directory.
根据 https://github.com/docker/docker/issues/25215,这是 Docker 的一个bug,fix 刚刚推出。一个 workaround 是重新创建 overlay network。
回到容器之间无法ping通对方的问题,尚不知道根本原因是什么(想吐槽Docker目前的问题真不少)。要使得互相 ping 能工作,至少必须具备下面的条件:
在 docker1 上,
• 为 vxlan dev 添加一条 fdb entry:02:42:14:00:00:03 dst 192.168.1.20 self
• 在容器中添加一条 arp entry:ip netns exec over1 arp -s 20.0.0.3 02:42:14:00:00:03
在 docker 2 上,
• 为 vxlan dev 添加一条 fdb entry:02:42:14:00:00:02 dst 192.168.1.21 self permanent
• 在容器中添加一条 arp entry:ip netns exec over4 arp -s 20.0.0.2 02:42:14:00:00:02
3. 网络性能对比
3.1 在我的测试环境中的数据
使用 iperf 工具检查测试了一下性能并做对比:
类型 TCP UDP
Overlay 网络中的两个容器之间 (A) 913 Mbits/sec 1.05 Mbits/sec
Bridge/NAT 网络中的两个容器之间 (B) 1.73 Gbits/sec
主机间 © 2.06 Gbits/sec 1.05 Mbits/sec
主机到另一个主机上的 bridge 网络模式的容器 (D) 1.88 Gbits/sec
主机到本主机上的容器 (E) 20.5 Gbits/sec
主机到另一个主机上的 host 网络模式的容器 (F) 2.02 Gbits/sec 1.05 Mbits/sec
容器 Overlay 效率 (A/C) 44% 100% ?
单个 NAT 效率 (D/C) 91%
两个 NAT 效率 (B/C) 83%
Host 网络模式效率 (F/C) 98% 100%
两台主机是同一个物理机上的两个虚机,因此,结果的绝对值其实没多少意义,相对值有一定的参考性。
3.2 网上文章中的对比数据
文章 Testing Docker multi-host network performance 对比了多种网络模式下的性能,结果如下:
看起来这个表里面的数据和我的表里面的数据差不了太多。
3.3 关于Docker 网络模式选择的简单结论
• Bridge 模式的性能损耗大概为10%
• 原生 overlay 模式的性能损耗非常高,甚至达到了 56%,因此,在生产环境下使用这种模式需要非常谨慎。
• 如果一定要使用 overlay 模式的话,可以考虑使用 Cisco 发起的 Calico 模式,它的性能和 bridge 相当。
• Weave overlay 模式的性能数据非常可疑,按理说应该不可能这么差。
Docker 存储可以分为分层文件系统和卷,本文将介绍 AUFS 分层文件系统。
$ tree
.
├── fruits
│ ├── apple
│ └── tomato
└── vegetables
├── carrots
└── tomato
输入以下几个命令:
$ mkdir mnt
$ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt
$ tree ./mnt
./mnt
├── apple
├── carrots
└── tomato
我们可以看到在./mnt目录下有三个文件,苹果apple、胡萝卜carrots和蕃茄tomato。水果和蔬菜的目录被union到了./mnt目录下了。
我们来修改一下其中的文件内容:
$ echo mnt > ./mnt/apple
$ cat ./mnt/apple
mnt
$ cat ./fruits/apple
mnt
上面的示例,我们可以看到./mnt/apple的内容改了,./fruits/apple的内容也改了。
$ echo mnt_carrots > ./mnt/carrots
$ cat ./vegetables/carrots
$ cat ./fruits/carrots
mnt_carrots
关于 AUFS 的几个特点:
• AUFS 是一种联合文件系统,它把若干目录按照顺序和权限 mount 为一个目录并呈现出来
• 默认情况下,只有第一层(第一个目录)是可写的,其余层是只读的。
• 增加文件:默认情况下,新增的文件都会被放在最上面的可写层中。
• 删除文件:因为底下各层都是只读的,当需要删除这些层中的文件时,AUFS 使用 whiteout 机制,它的实现是通过在上层的可写的目录下建立对应的whiteout隐藏文件来实现的。
• 修改文件:AUFS 利用其 CoW (copy-on-write)特性来修改只读层中的文件。AUFS 工作在文件层面,因此,只要有对只读层中的文件做修改,不管修改数据的量的多少,在第一次修改时,文件都会被拷贝到可写层然后再被修改。
• 节省空间:AUFS 的 CoW 特性能够允许在多个容器之间共享分层,从而减少物理空间占用。
• 查找文件:AUFS 的查找性能在层数非常多时会出现下降,层数越多,查找性能越低,因此,在制作 Docker 镜像时要注意层数不要太多。
• 性能:AUFS 的 CoW 特性在写入大型文件时第一次会出现延迟。
本部分内容主要应用自 Docker基础技术:AUFS。
2. Docker 文件系统
2.1 Docker 镜像的 rootfs
前面基础知识部分谈到过,同一个内核版本的所有 Linux 系统的 bootfs 是相同的,而 rootfs 则是不同的。在 Docker 中,基础镜像中的 roofs 会一直保持只读模式,Docker 会利用 union mount 来在这个 rootfs 上增加更多的只读文件系统,最后它们看起来就像一个文件系统即容器的 rootfs。
(图片来源)
可见在一个Linux 系统之中,
• 所有 Docker 容器都共享主机系统的 bootfs 即 Linux 内核
• 每个容器有自己的 rootfs,它来自不同的 Linux 发行版的基础镜像,包括 Ubuntu,Debian 和 SUSE 等
• 所有基于一种基础镜像的容器都共享这种 rootfs
以 training/webapp 镜像为例,
root@docker1:/var/lib/docker/aufs/diff/b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa# docker history training/webapp
IMAGE CREATED CREATED BY SIZE COMMENT
6fae60ef3446 16 months ago /bin/sh -c #(nop) CMD [“python” “app.py”] 0 B
16 months ago /bin/sh -c #(nop) EXPOSE 5000/tcp 0 B
16 months ago /bin/sh -c #(nop) WORKDIR /opt/webapp 0 B
16 months ago /bin/sh -c #(nop) ADD dir:9b2a69f6f30d18b02b5 703 B
16 months ago /bin/sh -c pip install -qr /tmp/requirements. 4.363 MB
16 months ago /bin/sh -c #(nop) ADD file:c59059439864153904 41 B
16 months ago /bin/sh -c DEBIAN_FRONTEND=noninteractive apt 135.3 MB
16 months ago /bin/sh -c apt-get update 20.8 MB
16 months ago /bin/sh -c #(nop) MAINTAINER Docker Education 0 B
17 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
17 months ago /bin/sh -c sed -i 's/^#\s*(deb.*universe)$/ 1.895 kB
17 months ago /bin/sh -c echo ‘#!/bin/sh’ > /usr/sbin/polic 194.5 kB
17 months ago /bin/sh -c #(nop) ADD file:f4d7b4b3402b5c53f2 188.1 MB
它是基于 Ubuntu Docker 基础镜像。在基础镜像层中,我们能看到完整的 Ubuntu rootfs:
root@docker1:/var/lib/docker/aufs/diff/b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa# ls -l
total 76
drwxr-xr-x 2 root root 4096 Apr 27 2015 bin
drwxr-xr-x 2 root root 4096 Apr 11 2014 boot
drwxr-xr-x 3 root root 4096 Apr 27 2015 dev
drwxr-xr-x 61 root root 4096 Apr 27 2015 etc
drwxr-xr-x 2 root root 4096 Apr 11 2014 home
drwxr-xr-x 12 root root 4096 Apr 27 2015 lib
drwxr-xr-x 2 root root 4096 Apr 27 2015 lib64
drwxr-xr-x 2 root root 4096 Apr 27 2015 media
drwxr-xr-x 2 root root 4096 Apr 11 2014 mnt
drwxr-xr-x 2 root root 4096 Apr 27 2015 opt
drwxr-xr-x 2 root root 4096 Apr 11 2014 proc
drwx------ 2 root root 4096 Apr 27 2015 root
drwxr-xr-x 7 root root 4096 Apr 27 2015 run
drwxr-xr-x 2 root root 4096 Apr 27 2015 sbin
drwxr-xr-x 2 root root 4096 Apr 27 2015 srv
drwxr-xr-x 2 root root 4096 Mar 13 2014 sys
drwxrwxrwt 2 root root 4096 Apr 27 2015 tmp
drwxr-xr-x 10 root root 4096 Apr 27 2015 usr
drwxr-xr-x 11 root root 4096 Apr 27 2015 var
我们来看两种典型的文件:
(1)bin 目录中的文件会被直接使用
root@docker1:/var/lib/docker/aufs/diff# find -iname mountpoint
./b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa/bin/mountpoint
(2)在基础镜像层中 proc 目录为空,也就是说容器中看到的 proc 目录中的文件是后来生成的。
2.2 Docker 使用的 AUFS 文件系统
关于 Docker的分层镜像,除了 aufs,docker还支持btrfs, devicemapper和vfs,你可以使用 -s 或 –storage-driver= 选项来指定相关的镜像存储。在Ubuntu 14.04下,Docker 默认 Ubuntu的 AUFS。因为 AUFS 还没有进入Linux 内核主干的原因,RedHat 上使用的是 devicemapper。
我们可以在 docker info 命令的输出中查看所使用的存储驱动:
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 19
Dirperm1 Supported: false
以一个正在运行着的 Docker 容器为例,其镜像有13层:
“RootFS”: {
“Type”: “layers”,
“Layers”: [
“sha256:1154ba695078d29ea6c4e1adb55c463959cd77509adf09710e2315827d66271a”,
“sha256:528c8710fd95f61d40b8bb8a549fa8dfa737d9b9c7c7b2ae55f745c972dddacd”,
“sha256:37ee47034d9b78f10f0c5ce3a25e6b6e58997fcadaf5f896c603a10c5f35fb31”,
“sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef”,
“sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef”,
“sha256:b75c0703b86b8ccbdc1f1b28b4982774768861ac250f83bdb940b1e90291f302”,
“sha256:5c121779bb29172c628a21087ea8ced766959da2f223c8b6bd4ffe943ace43d8”,
“sha256:3ee91c5cb95b01496b4afdc721ba7fd3c22e0e5e2f3e9e70d3f8579b5082d4f3”,
“sha256:6bbb1d0f845289217e20b66697fa7d651394d89983b0f5a89b88f037194476fe”,
“sha256:b44b0832d4c6bf33122ce3aa896b133df88275e6d20663a9bf2d941f764ac1fd”,
“sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef”,
“sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef”,
“sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef”
]
}
从 AUFS 的角度,可以看到有一个可写的容器层和14个只读的镜像层:
root@docker1:/sys/fs/aufs/si_ab487e40195df24f# cat *
/var/lib/docker/aufs/diff/2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab=rw #可写的容器层
/var/lib/docker/aufs/diff/2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab-init=ro+wh #本层及以下是只读的镜像层
/var/lib/docker/aufs/diff/5472f8388f9a61f6bd84498201a5ad71a2ec88cda16c42a3a1da7c30da45f102=ro+wh
/var/lib/docker/aufs/diff/61dcf0881e790bf52ec555727b58641791adeefadcc7abc2a77fd228bde1371a=ro+wh
/var/lib/docker/aufs/diff/f68672aaf17dd158aabc635b2d8d459d79db1cd5ff38bf3834fe8f9c7a05235e=ro+wh
/var/lib/docker/aufs/diff/45818d286499870412357d66eb6af951699f89db785c7c6a242d2e1ac99734f9=ro+wh
/var/lib/docker/aufs/diff/b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa=ro+wh
/var/lib/docker/aufs/diff/85cb840562788e1b458e68265e62fd2da9d0d7e737256500e8a276bcb237183c=ro+wh
/var/lib/docker/aufs/diff/c18ba8efcb455e97f6aabe3985b147f6a37b8f5ad090373e88ddd326b4f90896=ro+wh
/var/lib/docker/aufs/diff/25de7dcc3a06f0caa3c701d4ed6c62f03e0757f6d477cc822db6e884bb366441=ro+wh
/var/lib/docker/aufs/diff/ad9e831217594cdfecd5e824690b0e52f2e16d6e2bb39b7143e66d467150cfe8=ro+wh
/var/lib/docker/aufs/diff/56d37c8eecd8be9ba13e07e1486e7a6ac2f0aa01f8e865ee6136137369d8d8a0=ro+wh
/var/lib/docker/aufs/diff/31bc6290457af4e560a3103020c85fbb5dfcfb201b0662a33165260529f87c07=ro+wh
/var/lib/docker/aufs/diff/e104672666119006648d0b82988c49527e52c64629750c5c9adde88acc790682=ro+wh
/var/lib/docker/aufs/diff/7a085e415855435121fb7837c26a5e951f622bc69364d9228d409a4929b627e1=ro+wh
根据上面 AUFS 的定义,容器的文件系统是从 14 个只读镜像层和1个可写容器层通过 AUFS mount 出来的。示意图如下:
这种分层文件系统可以通过官网的图来清晰的展示出来:
做一些实验:
(1)在容器中创建一个文件,该文件会被创建在可写的容器层中
root@docker1:/var/lib/docker/aufs/diff# find -iname createdbysammy
./2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab/opt/webapp/createdbysammy
root@docker1:/var/lib/docker/aufs/diff# ls -lt
total 60
drwxr-xr-x 9 root root 4096 Oct 4 22:37 2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab
drwxr-xr-x 6 root root 4096 Oct 1 11:56 2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab-init
(2)修改一个镜像层中的文件
修改前,文件 /etc/apt/sources.list 出现在两个层中:
root@docker1:/var/lib/docker/aufs/diff# find -iname sources.list
./f68672aaf17dd158aabc635b2d8d459d79db1cd5ff38bf3834fe8f9c7a05235e/etc/apt/sources.list
./b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa/etc/apt/sources.list
在容器中对它进行修改后,它被拷贝到了容器层然后被修改了:
root@docker1:/var/lib/docker/aufs/diff# find -iname sources.list
./f68672aaf17dd158aabc635b2d8d459d79db1cd5ff38bf3834fe8f9c7a05235e/etc/apt/sources.list
./2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab/etc/apt/sources.list
./b2188d5c09cfe24acd6da5ce67720f81138f0c605a25efc592f1f55b3fd3dffa/etc/apt/sources.list
而另外两个层中的文件保持了不变。这说明了 AUFS 的 CoW 特性。
(3)删除容器层中的文件
容器中的文件 ./usr/local/lib/python2.7/dist-packages/itsdangerous.py 位于 56d37c8eecd8be9ba13e07e1486e7a6ac2f0aa01f8e865ee6136137369d8d8a0 层中,这是一个只读层。
在容器内删除它:
root@fa385836d5b9:/# find -iname itsdangerous.py
./usr/local/lib/python2.7/dist-packages/itsdangerous.py
root@fa385836d5b9:/# rm ./usr/local/lib/python2.7/dist-packages/itsdangerous.py
root@fa385836d5b9:/# find -iname itsdangerous.py
然后,容器层中出现了一个 .wh 文件,而镜像层中的文件保持不变:
root@docker1:/var/lib/docker/aufs/diff# find -iname *itsdangerous.py
./56d37c8eecd8be9ba13e07e1486e7a6ac2f0aa01f8e865ee6136137369d8d8a0/usr/local/lib/python2.7/dist-packages/itsdangerous.py
./2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab/usr/local/lib/python2.7/dist-packages/.wh.itsdangerous.py
在手工将 .wh 文件删除后,文件就会再次回到容器中。
rm ./2ee58d81e4ac6811bbc78beb4b46bf213c79c9e2dc7e441741afc8c4349c6bab/usr/local/lib/python2.7/dist-packages/.wh.itsdangerous.py
root@fa385836d5b9:/# find -iname itsdangerous.py
./usr/local/lib/python2.7/dist-packages/itsdangerous.py