详细了解了容器的隔离与限制以及基本的容器知识之后,本篇主要讲述容器的网络和数据卷的操作。
https://time.geekbang.org/column/article/18119
参考这个专栏,我们写了一个dockerfile,在docker file中定义了Flash模块、安装目录,所需的requirements文件等,通过dockerfile部署一个Flash+OS打包的容器,名字为helloworld。为了验证容器之间或进程之间共享net namespace这个实现原理,本篇用专栏提供的c源程序测试。
操作特定容器的网络空间
首先,Flask容器已经运行,可以通过web服务4000端口对外提供访问。为了验证外部程序与这个容器的网络实现交互,这就要线要摸清楚容器内运行的情况,因此第一步运行docker ps找到容器运行的id,找到这个Flask web应用的容器(名为helloworld)。
root@docker:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
48a2641727b9 helloworld "python app.py" 27 minutes ago Up 27 minutes 0.0.0.0:4000->80/tcp, :::4000->80/tcp friendly_ardinghelli
e25f5496abe0 ubuntu:18.04 "/bin/bash" 5 hours ago Up 5 hours jovial_goldberg
23a10d6c5273 ubuntu:18.04 "bash" 22 hours ago Up 21 hours epic_neumann
e3713b4f6370 ubuntu:18.04 "/bin/bash" 23 hours ago Up 22 hours
根据容器ID-48a2641727b9,查到容器的进程号
root@docker:~# docker inspect --format '{{ .State.Pid }}' 48a2641727b9
30065
根据进程号,查看进程所对应的Net Namespace
root@docker:~# ls -al /proc/30065/ns/net
lrwxrwxrwx 1 root root 0 Dec 6 11:21 /proc/30065/ns/net -> 'net:[4026532905]'
此时运行gcc set_ns程序,也即是创建一个新进程(/bin/bash),这个进程要设置与进程编号为30065的容器相同的net namespace。
root@docker:~# gcc -o set_ns set_ns.c
root@docker:~# ./set_ns /proc/30065/ns/net /bin/bash
此时在这个bash的网络namespace就等于容器的了。
root@docker:~# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.5 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:05 txqueuelen 0 (Ethernet)
RX packets 46 bytes 4051 (4.0 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 30 bytes 2720 (2.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
root@docker:~# ps
PID TTY TIME CMD
30222 pts/2 00:00:00 bash
34405 pts/2 00:00:00 bash
34606 pts/2 00:00:00 ps
明显可以看到显示网卡,只有两个网卡,这就是容器的内部私有网络,与宿主机的网络没有任何关系。
首先刚才通过set_ns启动的bash的进程号
root@docker:~# ps aux | grep /bin/bash
root 26498 0.0 0.0 18516 3160 pts/0 Ss+ Dec05 0:00 /bin/bash
root 29089 0.0 1.2 1364684 48260 pts/0 Sl+ 06:34 0:00 docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu:18.04 /bin/bash
root 29141 0.0 0.0 18512 3332 pts/0 Ss+ 06:34 0:00 /bin/bash
root 29589 19.9 0.0 18512 412 pts/0 R 06:44 60:39 /bin/bash
root 34405 0.0 0.0 20188 3988 pts/2 S+ 11:41 0:00 /bin/bash
root 34605 0.0 0.0 13144 1024 pts/3 R+ 11:47 0:00 grep --color=auto /bin/bash
根据该进程编号,进入到进程对应的/proc,查看net namespace,果然与容器相同,都指向同一个namespace,所以两个进程是共有一个网络。
root@docker:~# ls -al /proc/34405/ns/net
lrwxrwxrwx 1 root root 0 Dec 6 11:44 /proc/34405/ns/net -> 'net:[4026532905]'
这个过程就是加入到容器net namespace的底层实现。
Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net,比如:
docker run -it –net container:4ddf4638572d busybox ifconfig
最终杀掉进入容器的那个bash进程,再运行ifconfig,恢复宿主机的网络状态了。
root@docker:~# kill -9 34405
root@docker:~# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:52ff:fe02:f056 prefixlen 64 scopeid 0x20<link>
ether 02:42:52:02:f0:56 txqueuelen 0 (Ethernet)
RX packets 15409 bytes 827120 (827.1 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 26758 bytes 39349227 (39.3 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.101 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::20c:29ff:fe9f:c331 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:9f:c3:31 txqueuelen 1000 (Ethernet)
RX packets 520187 bytes 475758512 (475.7 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 336181 bytes 72645953 (72.6 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 2095 bytes 193741 (193.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2095 bytes 193741 (193.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth06b1a37: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::d4ca:f0ff:feda:5f53 prefixlen 64 scopeid 0x20<link>
ether d6:ca:f0:da:5f:53 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 53 bytes 3952 (3.9 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth2caf7fd: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::cde:50ff:fe51:929d prefixlen 64 scopeid 0x20<link>
ether 0e:de:50:51:92:9d txqueuelen 0 (Ethernet)
RX packets 30 bytes 2720 (2.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 47 bytes 4121 (4.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth4c4fcc8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::c4d6:82ff:fe59:89ea prefixlen 64 scopeid 0x20<link>
ether c6:d6:82:59:89:ea txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 66 bytes 4874 (4.8 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethaa7d196: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::5847:61ff:fe38:53d9 prefixlen 64 scopeid 0x20<link>
ether 5a:47:61:38:53:d9 txqueuelen 0 (Ethernet)
RX packets 14905 bytes 995967 (995.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 26222 bytes 38562461 (38.5 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
root@docker:~#
总结,通过上述操作和底层原理的演示,可以清晰地看到namespace是创建进程的主要参数,通过操作改变网络进程这个namespace,可以实现多个进程对同一个容器网络的访问。
当然,我如果创建两个busybox,使用同一个容器网路这也是可行的。
root@docker:~# docker exec -it 00a9fcb4026a /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:04
inet addr:172.17.0.4 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:6 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:516 (516.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/ # exit
启动一个新的busybox,使用--net container: 容器ID,使用之前创建的busybox容器的网络。可以看到运行ifconfig,显示的网卡地址都是相同的。
root@docker:~# docker run -it --net container:00a9fcb4026a busybox /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:04
inet addr:172.17.0.4 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:8 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:656 (656.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
在容器内部,找到宿主机的docker0网桥,地址为172.17.0.1,容器通过内部的eth0,其实对应的外部接口为veth,最后桥接到容器的docker0网桥上,这样就可以连通外部网络。
/ # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.100 ms
64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.086 ms
64 bytes from 172.17.0.1: seq=2 ttl=64 time=0.083 ms
^C
--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.083/0.089/0.100 ms
/ # ping www.baidu.com
PING www.baidu.com (110.242.68.3): 56 data bytes
64 bytes from 110.242.68.3: seq=0 ttl=51 time=11.525 ms
64 bytes from 110.242.68.3: seq=1 ttl=51 time=11.802 ms
^C
--- www.baidu.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 11.525/11.663/11.802 ms
/ #
/ # exit
退出容器后,可以看到系统中两个busybox进程。
root@docker:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4dc68e354e38 busybox "/bin/sh" 32 seconds ago Exited (127) 2 seconds ago nice_mclaren
00a9fcb4026a busybox "/bin/sh" 6 minutes ago Up About a minute focused_lalande
然后我们创建第三个独立的busybox,与其他两个网络没有关系,登入容器发现产生了新的网络地址172.17.0.5,这个地址也是连接到docker0网桥上,因此和其他容器都是相通的,并且可访问外网。
root@docker:~# docker run -it busybox /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:05
inet addr:172.17.0.5 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:5 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:426 (426.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/ # ping 172.17.0.4
PING 172.17.0.4 (172.17.0.4): 56 data bytes
64 bytes from 172.17.0.4: seq=0 ttl=64 time=0.109 ms
64 bytes from 172.17.0.4: seq=1 ttl=64 time=0.089 ms
^C
--- 172.17.0.4 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.089/0.099/0.109 ms
/ # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.085 ms
64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.085 ms
^C
--- 172.17.0.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.085/0.085 ms
操作容器的数据卷
现在,我们再一起回到前面提交镜像的操作 docker commit 上来吧。上述镜像经过配置Flash Web服务器,可以以4000端口对外提供网络服务,最后通过docker commit,实际上就是在容器运行起来后,把最上层的“可读写层”,加上原先容器镜像的只读层,打包组成了一个新的镜像。当然,下面这些只读层在宿主机上是共享的,不会占用额外的空间。
容器技术使用了 rootfs 机制和 Mount Namespace,构建出了一个同宿主机完全隔离开的文件系统环境。这时候,我们就需要考虑这样两个问题:
1、容器里进程新建的文件,怎么才能让宿主机获取到?
2、宿主机上的文件和目录,怎么才能让容器里的进程访问到?
这正是 Docker Volume 要解决的问题:Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。
Docker 又是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去呢?难道又是 Mount Namespace 的黑科技吗?实际上当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。
而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层,保存在 /var/lib/docker/aufs/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/aufs/mnt/ 目录中,这样容器所需的 rootfs 就准备好了。所以,我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。
更重要的是,由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。
而这里要使用到的挂载技术,就是 Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。
所以,在一个正确的时机,进行一次绑定挂载,Docker 就可以成功地将一个宿主机上的目录或文件,不动声色地挂载到容器中。
这样,进程在容器里对这个 /test 目录进行的所有操作,都实际发生在宿主机的对应目录(比如,/home,或者 /var/lib/docker/volumes/[VOLUME_ID]/_data)里,而不会影响容器镜像的内容。那么,这个 /test 目录里的内容,既然挂载在容器 rootfs 的可读写层,它会不会被 docker commit 提交掉呢?
root@docker:~# docker run -d -v /test helloworld
root@docker:~# docker exec -it 4957cd87230a /bin/bash
root@4957cd87230a:/# ls
app bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys test tmp usr var
root@4957cd87230a:/# ls -al test
total 12
drwxr-xr-x 2 root root 4096 Dec 7 06:02 .
drwxr-xr-x 1 root root 4096 Dec 7 06:00 ..
-rw-r--r-- 1 root root 35 Dec 7 06:02 wtao.txt
docker 挂在的volume/test文件夹实际上对应一个宿主机的本地文件夹,如下:
root@docker:~# docker volume ls
DRIVER VOLUME NAME
local 4e0c32339558593f853c76564dc0519029d0343e7adfc18fc05a196bdc4031bc
在宿主机的文件夹下,可以修改内容,这些内容都会被mount到容器中。
root@docker:/var/lib/docker/volumes/4e0c32339558593f853c76564dc0519029d0343e7adfc18fc05a196bdc4031bc# cd /var/lib/docker/volumes/4e0c32339558593f853c76564dc0519029d0343e7adfc18fc05a196bdc4031bc/_data/
root@docker:/var/lib/docker/volumes/4e0c32339558593f853c76564dc0519029d0343e7adfc18fc05a196bdc4031bc/_data# ls -al
total 12
drwxr-xr-x 2 root root 4096 Dec 7 06:02 .
drwx-----x 3 root root 4096 Dec 7 06:00 ..
-rw-r--r-- 1 root root 35 Dec 7 06:02 wtao.txt
root@docker:/var/lib/docker/volumes/4e0c32339558593f853c76564dc0519029d0343e7adfc18fc05a196bdc4031bc/_data#
由于这个mount namespace的操作发生在容器内部,作为宿主机,它是看不到容器的这些mount操作,因此进行容器进项的commit和push过程中,不会把test文件夹的内容上传,而仅仅会保留/test这个文件的影子和空客。同时也可以理解到init层中的文件,包含了容器的/etc/hosts之类的配置,这个每个容器都是完全不同的内容,也不会上传。容器上传的都是copy on write这一层。
经过上述介绍了如何使用 Linux Namespace、Cgroups,以及 rootfs 的知识,对容器进行了一次庖丁解牛似的解读。借助这种思考问题的方法,最后的 Docker 容器,我们实际上就可以用下面这个“全景图”描述出来:
这个容器进程“python app.py”,运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。
暂无评论