docker的隔离系统

关于Docker实现原理,简单总结如下:
使用Namespaces实现了系统环境的隔离,Namespaces允许一个进程以及它的子进程从共享的宿主机内核资源(网络栈、进程列表、挂载点等)里获得一个仅自己可见的隔离区域,让同一个Namespace下的所有进程感知彼此变化,对外界进程一无所知,仿佛运行在一个独占的操作系统中;

使用CGroups限制这个环境的资源使用情况,比如一台16核32GB的机器上只让容器使用2核4GB。使用CGroups还可以为资源设置权重,计算使用量,操控任务(进程或线程)启停等;

使用镜像管理功能,利用Docker的镜像分层、写时复制、内容寻址、联合挂载技术实现了一套完整的容器文件系统及运行环境,再结合镜像仓库,镜像可以快速下载和共享,方便在多环境部署。

  1. 基础知识:Linux namespace 的概念
    Linux 内核从版本 2.4.19 开始陆续引入了 namespace 的概念。其目的是将某个特定的全局系统资源(global system resource)通过抽象方法使得namespace 中的进程看起来拥有它们自己的隔离的全局系统资源实例(The purpose of each namespace is to wrap a particular global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. )。Linux 内核中实现了六种 namespace,按照引入的先后顺序,列表如下:
    namespace 引入的相关内核版本 被隔离的全局系统资源 在容器语境下的隔离效果
    Mount namespaces Linux 2.4.19
    文件系统挂接点 每个容器能看到不同的文件系统层次结构
    šUTS namespaces Linux 2.6.19
    nodename 和 domainname 每个容器可以有自己的 hostname 和 domainame
    IPC namespaces Linux 2.6.19
    特定的进程间通信资源,包括System V IPC 和 POSIX message queues
    每个容器有其自己的 System V IPC 和 POSIX 消息队列文件系统,因此,只有在同一个 IPC namespace 的进程之间才能互相通信
    PID namespaces Linux 2.6.24
    进程 ID 数字空间 (process ID number space) 每个 PID namespace 中的进程可以有其独立的 PID; 每个容器可以有其 PID 为 1 的root 进程;也使得容器可以在不同的 host 之间迁移,因为 namespace 中的进程 ID 和 host 无关了。这也使得容器中的每个进程有两个PID:容器中的 PID 和 host 上的 PID。
    Network namespaces 始于Linux 2.6.24 完成于 Linux 2.6.29
    网络相关的系统资源 每个容器用有其独立的网络设备,IP 地址,IP 路由表,/proc/net 目录,端口号等等。这也使得一个 host 上多个容器内的同一个应用都绑定到各自容器的 80 端口上。
    User namespaces 始于 Linux 2.6.23 完成于 Linux 3.8)
    用户和组 ID 空间 在 user namespace 中的进程的用户和组 ID 可以和在 host 上不同; 每个 container 可以有不同的 user 和 group id;一个 host 上的非特权用户可以成为 user namespace 中的特权用户;
    Linux namespace 的概念说简单也简单说复杂也复杂。简单来说,我们只要知道,处于某个 namespace 中的进程,能看到独立的它自己的隔离的某些特定系统资源;复杂来说,可以去看看 Linux 内核中实现 namespace 的原理,网络上也有大量的文档供参考,这里不再赘述。
  2. Docker 容器使用 linux namespace 做运行环境隔离
    当 Docker 创建一个容器时,它会创建新的以上六种 namespace 的实例,然后把容器中的所有进程放到这些 namespace 之中,使得Docker 容器中的进程只能看到隔离的系统资源。
    2.1 PID namespace
    我们能看到同一个进程,在容器内外的 PID 是不同的:
    • 在容器内 PID 是 1,PPID 是 0。
    • 在容器外 PID 是 2198, PPID 是 2179 即 docker-containerd-shim 进程.
    root@devstack:/home/sammy# ps -ef | grep python
    root 2198 2179 0 00:06 ? 00:00:00 python app.py
    root@devstack:/home/sammy# ps -ef | grep 2179
    root 2179 765 0 00:06 ? 00:00:00 docker-containerd-shim 8b7dd09fbcae00373207f01e2acde45740871c9e3b98286b5458b4ea09f41b3e /var/run/docker/libcontainerd/8b7dd09fbcae00373207f01e2acde45740871c9e3b98286b5458b4ea09f41b3e docker-runc
    root 2198 2179 0 00:06 ? 00:00:00 python app.py
    root 2249 1692 0 00:06 pts/0 00:00:00 grep --color=auto 2179
    root@devstack:/home/sammy# docker exec -it web31 ps -ef
    UID PID PPID C STIME TTY TIME CMD
    root 1 0 0 16:06 ? 00:00:00 python app.py
    关于 containerd,containerd-shim 和 container 的关系,文章 中的下图可以说明:

• 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 之间的映射。
启用步骤:

  1. 修改 /etc/default/docker 文件,添加行 DOCKER_OPTS="–userns-remap=default"
  2. 重启 docker 服务,此时 dockerd 进程为 /usr/bin/dockerd --userns-remap=default --raw-logs
  3. 然后创建一个容器:docker run -d -v /bin:/host/bin --name web35 training/webapp python app.py
  4. 查看进程在容器内外的用户:
    root@devstack:/home/sammy# ps -ef | grep python
    231072 1726 1686 0 01:44 ? 00:00:00 python app.py
    root@devstack:/home/sammy# docker exec web35 ps -ef
    UID PID PPID C STIME TTY TIME CMD
    root 1 0 0 17:44 ? 00:00:00 python app.py
    • 查看文件/etc/subuid 和 /etc/subgid,可以看到 dockermap 用户在host 上的 uid 和 gid 都是 231072:
    root@devstack:/home/sammy# cat /etc/subuid
    sammy:100000:65536
    stack:165536:65536
    dockremap:231072:65536
    root@devstack:/home/sammy# cat /etc/subgid
    sammy:100000:65536
    stack:165536:65536
    dockremap:231072:65536
    • 再看文件/proc/1726/uid_map,它表示了容器内外用户的映射关系,即将host 上的 231072 用户映射为容器内的 0 (即root)用户。
    root@devstack:/home/sammy# cat /proc/1726/uid_map
    0 231072 65536
    • 现在,我们试图在容器内修改 host 上的 /bin 文件夹,就会提示权限不足了:
    root@80993d821f7b:/host/bin# touch test2
    touch: cannot touch ‘test2’: Permission denied
    这说明通过使用 user namespace,使得容器内的进程运行在非 root 用户,我们就成功地限制了容器内进程的权限。
    2.3.3 检查 linux 操作系统是否启用了 user namespace
    运行下面的命令即可检查是否启用了:

[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=" (grubbydefaultkernel)"rebootgrubbyremoveargs="usernamespace.enable=1"updatekernel="(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 文件夹中读取内容的。
步骤:

  1. 找到容器的主进程 ID
    root@devstack:/home/sammy# docker inspect --format ‘{{.State.Pid}}’ web5
    2704
  2. 创建 /var/run/netns 目录以及符号连接
    root@devstack:/home/sammy# mkdir /var/run/netns
    root@devstack:/home/sammy# ln -s /proc/2704/ns/net /var/run/netns/web5
  3. 此时可以使用 ip netns 命令了

root@devstack:/home/sammy# ip netns
web5
root@devstack:/home/sammy# ip netns exec web5 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
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]

  1. Docker run 命令中 namespace 中相关参数
    Docker run 命令有几个参数和 namespace 相关:
    • --ipc string IPC namespace to use
    • --pid string PID namespace to use
    • --userns string User namespace to use
    • --uts string UTS namespace to use
    3.1 --userns
    –userns:指定容器使用的 user namespace
    • ‘host’: 使用 Docker host user namespace
    • ‘’: 使用由 `–userns-remap‘ 指定的 Docker deamon user namespace
    你可以在启用了 user namespace 的情况下,强制某个容器运行在 host user namespace 之中:
    root@devstack:/proc/2835# docker run -d -v /bin:/host/bin --name web37 --userns host training/webapp python app.py
    9c61e9a233abef7badefa364b683123742420c58d7a06520f14b26a547a9476c
    root@devstack:/proc/2835# ps -ef | grep python
    root 2962 2930 1 02:17 ? 00:00:00 python app.py
    否则默认的话,就会运行在特定的 user namespace 之中了。
    3.2 --pid
    同样的,可以指定容器使用 Docker host pid namespace,这样,在容器中的进程,可以看到 host 上的所有进程。注意此时不能启用 user namespace。

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

  1. 基础知识:Linux control groups
    1.1 概念
    Linux Cgroup 可让您为系统中所运行任务(进程)的用户定义组群分配资源 — 比如 CPU 时间、系统内存、网络带宽或者这些资源的组合。您可以监控您配置的 cgroup,拒绝 cgroup 访问某些资源,甚至在运行的系统中动态配置您的 cgroup。所以,可以将 controll groups 理解为 controller (system resource) (for) (process)groups,也就是是说它以一组进程为目标进行系统资源分配和控制。
    它主要提供了如下功能:
    • Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
    • Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。
    • Accounting: 一些审计或一些统计,主要目的是为了计费。
    • Control: 挂起进程,恢复执行进程。
    使用 cgroup,系统管理员可更具体地控制对系统资源的分配、优先顺序、拒绝、管理和监控。可更好地根据任务和用户分配硬件资源,提高总体效率。
    在实践中,系统管理员一般会利用CGroup做下面这些事(有点像为某个虚拟机分配资源似的):
    • 隔离一个进程集合(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。
    • 为这组进程分配其足够使用的内存
    • 为这组进程分配相应的网络带宽和磁盘存储限制
    • 限制访问某些设备(通过设置设备的白名单)
    查看 linux 内核中是否启用了 cgroup:

[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_DEBUG is not set

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_DEBUG_BLK_CGROUP is not set

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)

一些说明:

  1. cgroup 只能限制 CPU 的使用,而不能保证CPU的使用。也就是说, 使用 cpuset-cpus,可以让容器在指定的CPU或者核上运行,但是不能确保它独占这些CPU;cpu-shares 是个相对值,只有在CPU不够用的时候才其作用。也就是说,当CPU够用的时候,每个容器会分到足够的CPU;不够用的时候,会按照指定的比重在多个容器之间分配CPU。

  2. 对内存来说,cgroups 可以限制容器最多使用的内存。使用 -m 参数可以设置最多可以使用的内存。

  3. Docker 网络概况
    用一张图来说明 Docker 网络的基本概况:

  4. 四种单节点网络模式
    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: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
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: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
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: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
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: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-1de98 state UNKNOWN mode DEFAULT group default
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 模式的性能数据非常可疑,按理说应该不可能这么差。

  1. 现有的跨主机容器网络解决方案
    1.1 Flannel容器网络
    Flannel 是由 CoreOS 主导的解决方案。Flannel 为每一个主机的 Docker daemon 分配一个IP段,通过 etcd 维护一个跨主机的路由表,容器之间 IP 是可以互相连通的,当两个跨主机的容器要通信的时候,会在主机上修改数据包的 header,修改目的地址和源地址,经过路由表发送到目标主机后解包。封包的方式,可以支持udp、vxlan、host-gw等,但是如果一个容器要暴露服务,还是需要映射IP到主机侧的。
    1.2 Calico 网络方案
    Calico 是个年轻的项目,基于BGP协议,完全通过三层路由实现。Calico 可以应用在虚机,物理机,容器环境中。在Calico运行的主机上可以看到大量由 linux 路由组成的路由表,这是calico通过自有组件动态生成和管理的。这种实现并没有使用隧道,没有NAT,导致没有性能的损耗,性能很好,从技术上来看是一种很优越的方案。这样做的好处在于,容器的IP可以直接对外部访问,可以直接分配到业务IP,而且如果网络设备支持BGP的话,可以用它实现大规模的容器网络。但BGP带给它的好处的同时也带给他的劣势,BGP协议在企业内部还很少被接受,企业网管不太愿意在跨网络的路由器上开启BGP协议。
    1.3 总结
    跨主机的容器网络解决方案不外乎三大类:
    • 隧道方案:比如Flannel的 VxLan。特点是对底层的网络没有过高的要求,一般来说只要是三层可达就可以,只要是在一个三层可达网络里,就能构建出一个基于隧道的容器网络。问题也很明显,一个大家共识是随着节点规模的增长复杂度会提升,而且出了网络问题跟踪起来比较麻烦,大规模集群情况下这是需要考虑的一个点。
    • 路由方案:路由技术从三层实现跨主机容器互通,没有NAT,效率比较高,和目前的网络能够融合在一起,每一个容器都可以像虚拟机一样分配一个业务的IP。但路由网络也有问题,路由网络对现有网络设备影响比较大,路由器的路由表应该有空间限制一般是两三万条。而容器的大部分应用场景是运行微服务,数量集很大。如果几万新的容器IP冲击到路由表里,导致下层的物理设备没办法承受;而且每一个容器都分配一个业务IP,业务IP消耗会很快。
    • VLAN:所有容器和物理机在同一个 VLAN 中。
    1.4 一个网友的性能测试报告
    结论:
    方案 结论 优势 劣势
    weave(udp) 真是惨,生产环境就别考虑了。看了下他们的架构,觉得即便是 fast-data-path 也没多大意义。 无 就是个渣渣,概念好毛用都没
    calico calico 的 2 个方案都有不错的表现,其中 ipip 的方案在 big msg size 上表现更好,但蹊跷是在 128 字节的时候表现异常,多次测试如此。bgp 方案比较稳定,CPU 消耗并没有 ipip 的大,当然带宽表现也稍微差点。不过整体上来说,无论是 bgp 还是 ipip tunnel,calico 这套 overlay sdn 的解决方案成熟度和可用度都相当不错,为云上第一选择。 性能衰减少,可控性高,隔离性棒 操作起来还是比较复杂,比如对 iptables 的依赖什么的
    flannel flannel 的 2 个方案表现也凑合,其中 vxlan 方案是因为没法开 udp offload 导致性能偏低,其他的测试报告来看,一旦让网卡自行解 udp 包拿到 mac 地址什么的,性能基本上可以达到无损,同时 cpu 占用率相当漂亮。udp 方案受限于 user space 的解包,仅仅比 weave(udp) 要好一点点。好的这一点就是在实现方面更加高效。 部署简单,性能还行,可以兼容老版本 docker 的启动分配行为,避免 launcher 没法实现固定 IP 的容器漂移,没法多子网隔离,对上层设计依赖度高,没有 IPAM,对 docker 启动方法有绑定
    docker 原生 overlay 方案 其实也是基于 vxlan 实现的。受限于 cloud 上不一定会开的网卡 udp offload,vxlan 方案的性能上限就是裸机的 55% 左右了。大体表现上与 flannel vxlan 方案几乎一致。 docker 原生,性能凑合 对内核要求高(>3.16),对 docker daemon 有依赖需求 ( consul / etcd ),本身驱动实现还是略差点,可以看到对 cpu 利用率和带宽比同样基于 vxlan 的 flannel 要差一些,虽然有 api 但对 network 以及多子网隔离局部交叉这种需求还是比较麻烦,IPAM 就是个渣
    综上,云上能用 BGP 就果断上 calico bgp 方案,不能用 BGP 也可以考虑 calico ipip tunnel 方案,如果是 coreos 系又能开 udp offload,flannel 是不错的选择。Docker overlay network 还有很长一段路要走,weave 就不用考虑了。
  2. 若干企业生产环境中的容器网络方案
    2.1 PPTV Docker网络解决方案 - 基于 linux bridge
    (0)PPTV 容器云架构
    (1)网络需求
    • 网络组人力不足以维护一个Overlay网络,Overlay网络出问题排查复杂,会出现失控的状态。
    • 隧道技术影响性能,不能满足生产环境对网络性能的要求。
    • 开启bgp对现有网络改动太大,无法接受。
    • 运维组同学希望能通过网络桥接的方案解决容器网络。
    (2)选中的方案
    最终 PPTV 选中基于docker的bridge模式,将默认的docker bridge网桥替换为 linuxbridge,把 linuxbridge 网段的 ip 加入到容器里,实现容器与传统环境应用的互通。
  3. 首先会在该主机上添加一个linux bridge,把主机网卡,可以是物理机的,也可以是虚拟机的,把这个网卡加入bridge里面,bridge配上网卡原本的管理IP。
  4. 创建一个新的docker bridge网络,指定bridge子网,并将该网络的网桥绑定到上一步创建的网桥上。
    docker network create --gateway10.199.45.200 --subnet 10.199.45.0/24 -o com.docker.network.bridge.name=br-oak–aux-address “DefaultGatewayIPv4=10.199.45.1” oak-net
  5. 容器启动时候,指定容器网络为第二步中创建的bridge网络,同时为容器指定一个该网络子网内的IP。容器启动后网络IP默认即可与外界互通。
    (3)问题及解决方案
    通过网桥的方式解决容器网络有两个问题:
  6. 容器跨主机漂移要求宿主机在同一个VLAN 中:linux bridge 只能添加跟 slavehost 同一个vlan的IP,也就是说容器IP必须要和宿主机在同一vlan下,这在一定程度上就限制了容器跨宿主机漂移的范围。不过这个问题在PPTV 的生产环境中天然不存在,因为我们的生产环境中,每个数据中心的主机都在一个很大的子网内,基本能满足容器在整个数据中心的任意节点下漂移。
  7. 跨主机的 IP 地址分配需要避免地址冲突:要让容器 IP 在不同的宿主机上漂移,宿主机的 docker 网络需要使用同一个CIDR,也就是各宿主机的容器使用同一个网段。而不同宿主机的使用同一个容器网段就会涉及到IPAM的问题,因为宿主机的docker daemon只知道他本机上的容器使用了哪些IP,而这些IP在其他宿主机上有没有被使用,是不知道的。在默认的docker bridge中,因为这些ip不会直接与外部通信,所以容器使用相同IP也不会有问题,但是当容器网络通过linux bridge打通以后,所有容器都是2层互通的,也就是会出现IP冲突的问题。为了解决上边提到的问题,实现全局的IP管控,我们开发了IP池管理平台,实现对容器IP的分配管理。
  8. 需要更新服务自动注册、自动发现:因为原先的方案是基于NAT的模式做的,而现在实现了独立IP的功能。我们需要将现有的平台与PPTV内部的DNS做自动化对接,每当有容器创建和生成时,都会自动对容器的IP做DNS解析。
  9. 负载均衡:PPTV的负载均衡基本都是通过LVS + nginx实现的,但对于后台的容器应用来说,每次扩容和缩容、或者创建新的应用,负载均衡的后端配置也是需要自动更新的。
    2.2 宜信的容器网络解决方案 - 基于 Calico
    (1)网络需求
    • 让每个容器拥有自己的网络栈,特别是独立的 IP 地址
    • 能够进行跨服务器的容器间通讯,同时不依赖特定的网络设备
    • 有访问控制机制,不同应用之间互相隔离,有调用关系的能够通讯
    (2)调研过程和最终的选择
    调研了几个主流的网络模型:
    • Docker 原生的 Bridge 模型:NAT 机制导致无法使用容器 IP 进行跨服务器通讯(后来发现自定义网桥可以解决通讯问题,但是觉得方案比较复杂)
    • Docker 原生的 Host 模型:大家都使用和服务器相同的 IP,端口冲突问题很麻烦
    • Weave OVS 等基于隧道的模型:由于是基于隧道的技术,在用户态进行封包解包,性能折损比较大,同时出现问题时网络抓包调试会很蛋疼
    • Project Calico 是纯三层的 SDN 实现,它基于 BPG 协议和 Linux 自己的路由转发机制,不依赖特殊硬件,没有使用 NAT 或 Tunnel 等技术。能够方便的部署在物理服务器,虚拟机(如 OpenStack)或者容器环境下。同时它自带的基于 Iptables 的 ACL 管理组件非常灵活,能够满足比较复杂的安全隔离需求。
    各种方案的限制:
    • Docker Bridge:每个容器里面都有一个虚拟网卡,和主机上的虚拟网卡做配合,所有容器内的虚拟网卡都可以和主机通信。通过端口映射,我调用对方的容器服务的时候,不能使用该容器直接的地址,必须使用它主机的地址。因为有了这样一个转发,网络通讯的性能有所损耗。当然,还有一个问题更严重,访问你的容器先要搞清楚你的主机是什么,这样网络通讯会跳来跳去。不但麻烦,还会带来端口冲突。每创建一个容器会绑定一个端口,怎么管理好这些端口是一个很大的挑战。
    • Fannel:这个方法有几个问题,第一个是要做封包的操作,这会带来网络性能损失。第二个问题是每个主机上的容器是固定的,容器的不同主机之前的迁移必然带来IP的变化。
    • Weave:它的思路是共享IP而非绑定。在传输层先找到目标地址,然后把包发到对端,节点之间互相通过协议共享信息。Flannel和Weave的特点是类似的,都用的是UDP或者是VxLAN的技术。事实上使用UDP性能是很差的,VxLAN和UDP差不多,它有一个网络切隔,而且在里面可以跑二层协议。还有一种是IPIP封包,直接把一个IP包封装在一个IP包里面。以上不难看出,原生网络性能比较差,使用UDP的时候性能损失在50%以上,使用VxLAN也会有20%~30%的损耗。所以我要在内核上封包,使用硬件来解决这些问题。而且容器和容器之间出现通讯故障,调试的时候非常困难
    (3)Calico 使用总结
    • Calico 的应用场景主要是IDC内部,推荐部署在二层网络上,这样所有路由器之间是互通的。这种场景是大二层的解决方案,所有服务器都在里面。但大二层主要的问题是弹性伸缩的问题。频繁开关机的时候,容器启停虽然不影响交换机,但容易产生广播风暴。网络规模太大的时候,一旦出现了这样的问题,整个网络都会有故障,这个问题还没有解决的特别好。
    • Calico作为一个纯二层的应用问题不大。我们可以把集群分成了很多网段,对外是三层网络的结构。集群内部分成多个自制域,比如一个机柜是一个自制域,之间通过交换路由的方式实现在三层容器到容器的互通。瓶颈在于容器的路由信息容量。所以说本质上它不是一个应对互联网规模的解决方案,但对宜信场景是够用的。
    • 我们实施的时候遇到一些坑,比如容器在启动时没有网络、操作ETCD的时候BGP配置频繁加载,等等,最后我们多一一解决了。我们也发现了driver方案功能方面的弱点,现在还继续使用劫持API的方法。如果要在云上使用,如果支持BGP,直接可以用;如果不支持BGP可以用IPIP。现在英特尔的网卡支持两个协议,一个是GRE、一个是VxLAN。如果真的把它放在云上,我们可以考虑一下把VxLAN引进来,性能上的差别不是很大。
    2.3 京东和魅族都是采用 OVS + VLAN 方案
    根据 2016 北京容器大会 《魅族云容器化建设》 文档的一些总结:
    (1)使用 OVS port 替代 OVS veth 可以带来较大的性能提升
    (2)使用 SR-IOV
    (3)使用 DPDK
  10. 小结
    上面的几个生产环境中的网络解决方案,它们的目的都是为了把容器当作虚机用,因此都有共同的需求,比如:
    • 每个容器有独立的 IP,这样运维就可以象通过 ssh 连接到虚机一样连接进容器
    • 需要跨服务器之间的容器通信
    • 安全性,包括访问控制和隔离性
    • 性能保证和优化
    • 对现有物理网络改动和影响较小
    • 使用和调试都比较方便
    要满足以上要求,可能 OVS/Linux-bridge + VLAN 方案应用的比较多,同时看起来 Calico 方案看起来前景不错。

Docker 存储可以分为分层文件系统和卷,本文将介绍 AUFS 分层文件系统。

  1. 基础知识
    1.1 Linux 的 rootfs 和 bootfs
    一个典型的 Linux 系统要能运行的话,它至少需要两个文件系统:
    • boot file system (bootfs):包含 boot loader 和 kernel。用户不会修改这个文件系统。实际上,在启动(boot)过程完成后,整个内核都会被加载进内存,此时 bootfs 会被卸载掉从而释放出所占用的内存。同时也可以看出,对于同样内核版本的不同的 Linux 发行版的 bootfs 都是一致的。
    • root file system (rootfs):包含典型的目录结构,包括 /dev, /proc, /bin, /etc, /lib, /usr, and /tmp 等再加上要运行用户应用所需要的所有配置文件,二进制文件和库文件。这个文件系统在不同的Linux 发行版中是不同的。而且用户可以对这个文件进行修改。
    Linux 系统在启动时,roofs 首先会被挂载为只读模式,然后在启动完成后被修改为读写模式,随后它们就可以被修改了。
    1.2 AUFS
    AUFS 是一种 Union File System(联合文件系统),又叫 Another UnionFS,后来叫Alternative UnionFS,再后来叫成高大上的 Advance UnionFS。所谓 UnionFS,就是把不同物理位置的目录合并mount到同一个目录中。UnionFS的一个最主要的应用是,把一张CD/DVD和一个硬盘目录给联合 mount在一起,然后,你就可以对这个只读的CD/DVD上的文件进行修改(当然,修改的文件存于硬盘上的目录里)。
    举个例子,在 Ubuntu 14.04 系统上现有如下目录结构:

$ tree
.
├── fruits
│ ├── apple
│ └── tomato
└── vegetables
├── carrots
└── tomato

输入以下几个命令:

创建一个mount目录

$ mkdir mnt

把水果目录和蔬菜目录union mount到 ./mnt目录中

$ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt

查看./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

  1. Docker volume 的几种形态
    有状态容器都有数据持久化需求。前一篇文章中提到过,Docker 采用 AFUS 分层文件系统时,文件系统的改动都是发生在最上面的容器层。在容器的生命周期内,它是持续的,包括容器在被停止后。但是,当容器被删除后,该数据层也随之被删除了。因此,Docker 采用 volume (卷)的形式来向容器提供持久化存储。Docker volume 有如下几种形态。
    1.1 无 - 不使用 Docker volume
    默认情况下,容器不使用任何 volume,此时,容器的数据被保存在容器之内,它只在容器的生命周期内存在,会随着容器的被删除而被删除。当然,也可以使用 docker commit 命令将它持久化为一个新的镜像。
    1.2 Data volume (数据卷)
    一个 data volume 是容器中绕过 Union 文件系统的一个特定的目录。它被设计用来保存数据,而不管容器的生命周期。因此,当你删除一个容器时,Docker 肯定不会自动地删除一个volume。有如下几种方式来使用 data volume:
    (1)使用 “-v 容器内目录” 形式
    docker run -d -P --name web -v /webapp training/webapp python app.py
    使用 docker inspect 命令可以看出,Docker 将本地一个 _data 目录 mount 为容器内的 webapp 目录了:
    “Mounts”: [
    {
    “Name”: “f143b7f379fb6d012a08656fc950bf6df4bf5a5b90c72f310644aa997620122b”,
    “Source”: “/var/lib/docker/volumes/f143b7f379fb6d012a08656fc950bf6df4bf5a5b90c72f310644aa997620122b/_data”,
    “Destination”: “/webapp”,
    “Driver”: “local”,
    “Mode”: “”,
    “RW”: true,
    “Propagation”: “”
    }
    ],
    其实,在 web 容器被删除后,/var/lib/docker/volumes/f143b7f379fb6d012a08656fc950bf6df4bf5a5b90c72f310644aa997620122b/_data 目录及其中的内容都还会保留下来,但是,新启动的容器无法再使用这个目录,也就是说,已有的数据不能自动地被重复使用了。
    (2)使用 -v 来挂载一个主机上的目录到容器的目录
    docker run -d -P --name web2 -v /src/webapp:/webapp training/webapp python app.py
    主机上的目录可以是一个本地目录,也可以在一个 NFS share 内,或者在一个已经格式化好了的块设备上。
    其实这种形式和第一种没有本质的区别,容器内对 /webapp 的操作都会反映到主机上的 /src/webapp 目录内。只是,重新启动容器时,可以再次使用同样的方式来将 /src/webapp 目录挂载到新的容器内,这样就可以实现数据持久化的目标。
    (3)使用 -v 来挂载主机上的一个文件到容器内的一个文件
    docker run --rm -it -v ~/.bash_history:/root/.bash_history ubuntu /bin/bash
    1.3 使用 data container
    如果要在容器之间共享数据,最好是使用 data container。这种 container 中不会跑应用,而只是挂载一个卷。比如:
    创建一个 data container:
    docker create -v /dbdata --name dbstore training/webapp /bin/true
    启动一个 app container:
    docker run -d -P --name web3 --volumes-from dbstore training/webapp python app.py
    其实,对 web3 这个容器来说,volume 的本质没变,它只是将 dbstore 容器的 /dbdata 目录映射的主机上的目录映射到自身的 /dbdata 目录。
    “Mounts”: [
    {
    “Name”: “5341c03f3b94f13f4c86d88ccb0f3b63487adf30dea7ae6b2d06e947235e7330”,
    “Source”: “/var/lib/docker/volumes/5341c03f3b94f13f4c86d88ccb0f3b63487adf30dea7ae6b2d06e947235e7330/_data”,
    “Destination”: “/dbdata”,
    “Driver”: “local”,
    “Mode”: “”,
    “RW”: true,
    “Propagation”: “”
    }
    ],
    但是,其好处是,可以不管其目录的临时性而不断地重复使用它。
    1.4 使用 docker volume 命令
    Docker 新版本中引入了 docker volume 命令来管理 Docker volume。
    (1)使用默认的 ‘local’ driver 创建一个 volume
    root@docker1:/home/sammy# docker volume create --name vol1
    vol1
    root@docker1:/home/sammy# docker volume inspect vol1
    [
    {
    “Name”: “vol1”,
    “Driver”: “local”,
    “Mountpoint”: “/var/lib/docker/volumes/vol1/_data”,
    “Labels”: {},
    “Scope”: “local”
    }
    ]
    (2)使用这个 volume
    docker run -d -P --name web4 -v vol1:/volume training/webapp python app.p
    结果还是一样的,即将 vol1 对应的主机上的目录挂载给容器内的 /volume 目录。
    “Mounts”: [
    {
    “Name”: “vol1”,
    “Source”: “/var/lib/docker/volumes/vol1/_data”,
    “Destination”: “/volume”,
    “Driver”: “local”,
    “Mode”: “z”,
    “RW”: true,
    “Propagation”: “rprivate”
    }
    ],
    1.5 Volume 删除和孤单 volume 清理
    1.5.1 在删除容器时删除 volume
    可以使用 docker rm -v 命令在删除容器时删除该容器的卷。
    root@docker1:/home/sammy# docker run -d -P --name web5 -v /webapp training/webapp python app.py
    69199905a74cb360935e32f4e99f7f11319f6aa36033a920aa0bae25874f5c69
    root@docker1:/home/sammy# docker volume ls
    DRIVER VOLUME NAME
    local 5341c03f3b94f13f4c86d88ccb0f3b63487adf30dea7ae6b2d06e947235e7330
    local 838f4dd99721a9445be22a6b42d35e04cb43ad145ecf26107a9025f428587f76
    local vol1
    root@docker1:/home/sammy# docker rm -vf web5
    web5
    root@docker1:/home/sammy# docker volume ls
    DRIVER VOLUME NAME
    local 5341c03f3b94f13f4c86d88ccb0f3b63487adf30dea7ae6b2d06e947235e7330
    local vol1
    1.5.2 批量删除孤单 volumes
    从上面的介绍可以看出,使用 docker run -v 启动的容器被删除以后,在主机上会遗留下来孤单的卷。可以使用下面的简单方法来做清理:
    root@docker1:/home/sammy# docker volume ls -qf dangling=true
    244a23f3ab11f17345a68e77f96bb46a8dbaf445760dd86ab0faa07dfbd84236
    c864cfac232e8728b1805abc8c363d324124b38e6297544a8cbbf61d883c7e46
    f143b7f379fb6d012a08656fc950bf6df4bf5a5b90c72f310644aa997620122b
    root@docker1:/home/sammy# docker volume rm $(docker volume ls -qf dangling=true)
    244a23f3ab11f17345a68e77f96bb46a8dbaf445760dd86ab0faa07dfbd84236
    c864cfac232e8728b1805abc8c363d324124b38e6297544a8cbbf61d883c7e46
    f143b7f379fb6d012a08656fc950bf6df4bf5a5b90c72f310644aa997620122b
    root@docker1:/home/sammy# docker volume ls
    DRIVER VOLUME NAME
    local 5341c03f3b94f13f4c86d88ccb0f3b63487adf30dea7ae6b2d06e947235e7330
    local vol1
    github 上有很多脚本可以自动化地清理孤单卷,比如:
    https://github.com/chadoe/docker-cleanup-volumes/blob/master/docker-cleanup-volumes.sh
    https://github.com/meltwater/docker-cleanup
    1.6 小结
    对以上内容的两点小结:
    • 容器内的数据是临时性的,它会随着容器生命周期的结束而消失
    • 默认的 Docker volume (driver = ‘loclal’)不管是哪种形式,本质上都是将容器所在的主机上的一个目录 mount 到容器内的一个目录,因此,它不具备可移植性。
  2. Flocker:容器的分布式存储平台
    第一部分提到过,原生的 Docker volume 不具备可移植性。于是,出现了Docker 的分布式卷解决方案 Flocker。先来看看 Flocker volume 和 Docker 原生 volume 的对比:
    启动一个使用 Flocker 卷的容器:
    docker run --volume-driver flocker -v flocker-volume:/container/dir --name=container-xyz
    它带来的好处包括:
    • 容器的数据会被写入 Flocker 后端存储而不是主机上,因此,在主机出现故障时可以保证数据不丢失
    • 在容器迁移时,Flocker 会自动地将卷从一个 host 移植到另一个 host
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Docker 是一种容器化技术,它通过使用操作系统级虚拟化来实现应用程序的隔离Docker 隔离原理主要依赖于以下几个关键技术: 1. Linux 命名空间(Namespaces):Docker 使用多种 Linux 命名空间来隔离不同的系统资源,如进程、网络、文件系统、用户等。每个容器都有自己独立的命名空间,使得容器中运行的进程只能看到自己所在的命名空间中的资源,从而实现了进程级别的隔离。 2. 控制组(Control Groups):Docker 使用控制组来限制和隔离容器对系统资源的使用,如 CPU、内存、磁盘、网络带宽等。通过控制组,可以为每个容器分配一定的资源配额,并且防止容器占用过多的资源影响其他容器或宿主机。 3. 文件系统隔离Docker 使用联合文件系统(UnionFS)来实现容器的文件系统隔离。每个容器都有自己的根文件系统,可以在其中安装和运行应用程序,而不会影响其他容器或宿主机的文件系统。 4. 容器间通信:Docker 提供了网络隔离功能,使得容器可以在自己的网络命名空间中运行,并且可以通过网络与其他容器或宿主机进行通信。每个容器都有自己的网络栈,独立于其他容器和宿主机,从而实现了网络级别的隔离。 综上所述,Docker 利用 Linux 命名空间、控制组、文件系统隔离和网络隔离等技术,实现了容器级别的隔离,使得不同的容器可以在同一台宿主机上独立运行,互相不受影响。这种隔离机制使得 Docker 在应用部署、开发测试和环境一致性等方面具有很大的优势。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值