容器基础知识之——网络与数据卷2021-12-06

详细了解了容器的隔离与限制以及基本的容器知识之后,本篇主要讲述容器的网络和数据卷的操作。
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 的挂载点,也出现在这一层。

暂无评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


虚拟化 | 云计算 | 机器学习 | 股市复盘
© 2024 涛哥,版权所有, 京ICP备20014492-2号