目录
- 1. Docker镜像原理介绍
- 1.1 操作系统基础
- 1.2 Union FS(联合文件系统)
- 1.3 再看 Docker 镜像是什么
- 2. 镜像的实现原理
- 2.1 Docker 分层存储实现原理
- 2.2 docker 镜像加载原理
- 3. 镜像分层存储实战
- 3.1 基础知识
- 3.2 实战过程
- 4. overlay 文件系统工作实战
- 5. Docker卷原理介绍
- 5.1 Docker 卷的机制
- 5.2 操作实战
- 5.2.1 Linux mount bind
- 5.2.2 查看 Docker 联合挂载
- 5.2.3 Docker 卷深度思考(docker commit 能提交卷里面的内容吗)
- 6. Docker网络原理介绍
- 7. Docker总结
- 7.1 Docker的基本概念
- 7.2 Docker的优势
- 7.3 Docker的使用场景
- 7.4 Docker的局限性
- 7.5 Docker的发展趋势
1. Docker镜像原理介绍
(1)docker 是操作系统层的虚拟化,所以 docker 镜像的本质是在模拟操作系统。我们先看下操作系统是什么。
1.1 操作系统基础
(1)操作系统是由:进程调度子系统、进程通信子系统、内存管理子系统、设备管理子系统、文件管理子系统、网络通信子系统、作业控制子系统组成。Linux 的文件管理子系统由 bootfs 和 rootfs 组成。
- bootfs:要包含 bootloader 和 kernel, bootloader 主要是引导加载 kernel, Linux 刚启动时会加载 bootfs 文件系统,在 Docker 镜像的最底层是引导文件系统 bootfs。这一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。
- rootfs: 在 bootfs 之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。 rootfs 就是各种不同的操作系统发行版,比如Ubuntu, Centos 等等。
1.2 Union FS(联合文件系统)
(1)联合文件系统的介绍:
-
联合文件系统(Union File System), 2004 年由纽约州立大学开发,它可以把多个目录内容联合挂载到同一个目录下,而目录的物理位置是分开的。 UnionFS 可以把只读和可读写文件系统合并在一起,具有写时复制功能,允许只读文件系统的修改可以保存到可写文件系统当中。
-
UnionFS(联合文件系统)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。
-
UnionFS 是一种为 Linux, FreeBSD 和 NetBSD 操作系统设计的把其他文件系统联合到一个联合挂载点的文件系统服务。它使用 branch 把不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。这些 branches 或者是read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。
-
看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一个重要的资管管理技术叫写时复制。
-
写时复制(copy-on-write,下文简称 CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源;这个资源可以被新旧实例共享。
-
创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗, 但是也会在进行资源修改的时候增加小部分的开销。
1.3 再看 Docker 镜像是什么
(1)Docker 镜像是什么???
- image 里面是一层层文件系统 Union FS。联合文件系统,可以将几层目录挂载到一起,形成一个虚拟文件系统。虚拟文件系统的目录结构就像普通 linux 的目录结构一样, docker 通过这些文件再加上宿主机的内核提供了一个 linux 的虚拟环境。
- 每一层文件系统我们叫做一层 layer,联合文件系统可以对每一层文件系统设置三种权限,只读(readonly)、读写(readwrite)和写出(whiteout-able),但是 docker镜像中每一层文件系统都是只读的。
- 构建镜像的时候,从一个最基本的操作系统开始,每个构建的操作都相当于做一层的修改,增加了一层文件系统。一层层往上叠加,上层的修改会覆盖底层该位置的可见性,这也很容易理解,就像上层把底层遮住了一样。当你使用的时候,你只会看到一个完全的整体,你不知道里面有几层,也不清楚每一层所做的修改是什么。
(2)可以看到镜像分层结构有以下特性:
- 镜像共享宿主机的 kernel。
- base 镜像是 linux 的最小发行版。
- 同一个 docker 主机支持不同的 Linux 发行版。
- 采用分层结构,可以上层引用下层,最大化的共享资源。
- 容器层位于可写层,采用 cow 技术进行修改,该层仅仅保持变化的部分,并不修改镜像下面的部分。
- 容器层以下都是只读层。
- docker 从上到下找文件。
2. 镜像的实现原理
2.1 Docker 分层存储实现原理
(1)分层存储实现方式:
- docker 镜像技术的基础是联合文件系统(UnionFS),其文件系统是分层的
# 目前 docker 支持的联合文件系统有很多种,包括: AUFS、 overlay、 overlay2、 DeviceMapper、 VSF 等
- Linux 中各发行版实现的 UnionFS 各不相同,所以 docker 在不同 linux 发行版中使用的也不同。通过 docker info 命令可以查看当前系统所使用哪种 UnionFS,常见的几种发行版使用如下:
CentOS, Storage Driver: overlay2、 overlay
debain, Storage Driver: aufs
RedHat, Storage Driver: devicemapper
- overlay2 是 overlay 的升级版,官方推荐,更加稳定,而新版的 docker 默认也是这个驱动, linux 的内核 4.0 以上或者或使用 3.10.0-514 或更高版本内核的 RHEL 或CentOS。
(2)Union FS 的原理:
- docker 镜像由多个只读层叠加面成,启动容器时, docker 会加载只读镜像层并在镜像栈顶部加一个读写层;
- 如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件版本仍然存在,只是已经被读写层中该文件的副本所隐藏,此即“写时复制(COW)”机制。
- 如果一个文件在最底层是可见的,如果在 layer1 上标记为删除,最高的层是用户看到的 Layer2 的层,在 layer0 上的文件,在 layer2 上可以删除,但是只是标记删除,用户是不可见的,总之在到达最顶层之前,把它标记来删除,对于最上层的用户是不可见的,当标记一删除,只有用户在最上层建一个同名一样的文件,才是可见的。
(3)overlay2 实现:
- 架构:OverlayFS 将单个 Linux 主机上的两个目录合并成一个目录。这些目录被称为层,统一过程被称为联合挂载 OverlayFS 底层目录称为 lowerdir, 高层目录称为upperdir,合并统一视图称为 merged:
图中可以看到三个层结构,即 lowerdir、 upperdir、 merged 层
-
分层:
- lowerdir 层:其中 lowerdir 是只读的镜像层(image layer),其中就包含 bootfs/rootfs 层,bootfs(boot file system)主要包含 bootloader 和 kernel, bootloader 主要是引导加载kernel,当 boot 成功 kernel 被加载到内存中, bootfs 就被 umount 了, rootfs(root filesystem)包含的就是典型 Linux 系统中的/dev、 /proc、 /bin、 /etc 等标准目录。lowerdir 是可以分很多层的,除了 bootfs/rootfs 层以外,还可以通过 Dockerfile 建立很多 image 层
- upperdir 层:upper 是容器的读写层,采用了 CoW(写时复制)机制,只有对文件进行修改才会将文件拷贝到 upper 层,之后所有的修改操作都会对 upper 层的副本进行修改。 upperdir 层是lowerdir 的上一层,只有这一层可读可写的,其实就是 Container 层,在启动一个容器的时候会在最后的 image 层的上一层自动创建,所有对容器数据的更改都会发生在这一层。
- workdir 层:它的作用是充当一个中间层的作用,每当对 upper 层里面的副本进行修改时,会先当到workdir,然后再从 workdir 移动 upper 层。
- merged 层:是一个统一图层,从 mergedir 可以看到 lower、upper、workdir 中所有数据的整合,整个容器展现出来的就是 mergedir 层。merged 层就是联合挂载层,也就是给用户暴露的统一视觉,将 image 层和 container 层结合,就如最上边的图中描述一致,同一文件,在此层会展示离它最近的层级里的文件内容,或者可以理解为,只要 container 层中有此文件,便展示 container 层中的文件内容,若 container 层中没有,则展示image 层中的。
-
如何完成读写:
- 读:如果文件在 upperdir(容器)层,直接读取文件;如果文件不在 upperdir(容器)层,则从镜像层(lowerdir)读取;
- 写:
- 首次写入:如果 upperdir 中不存在, overlay 和 overlay2 执行 copy_up 操作,把文件从 lowdir 拷贝到 upperdir 中,由于 overlayfs 是文件级别的(即使只有很少的一点修改,也会产生 copy_up 的动作),后续对同一文件的再次写入操作将对已经复制到容器层的文件副本进行修改,这也就是常常说的写时复制(copy-on-write)。
- 删除文件或目录:当文件被删除时,在容器层(upperdir)创建 whiteout 文件,镜像层(lowerdir)的文件是不会被删除的,因为它们是只读的,但 whiteout 文件会阻止它们显示,当目录被删除时,在容器层(upperdir)一个不透明的目录,这个和上边的 whiteout的原理一样,组织用户继续访问, image 层不会发生改变
(4)注意事项:
- copy_up 操作只发生在文件首次写入,以后都是只修改副本。
- 容器层的文件删除只是一个“障眼法”,是靠 whiteout 文件将其遮挡,image 层并没有删除,这也就是为什么使用 docker commit 提交保存的镜像会越来越大,无论在容器层怎么删除数据, image 层都不会改变。
docker__85">2.2 docker 镜像加载原理
(1)boots(boot file system)主要包含 bootloader 和 Kernel, bootloader 主要是引导加kernel,Linux 刚启动时会加 bootfs 文件系统,在 Docker 镜像的最底层是 bootfs。这一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加載器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。
(2)rootfs(root file system),在 bootfs 之上。包含的就是典型 Linux 系统中的
/dev、/proc、/bin、/etc 等标准目录和文件。 rootfs 就是各种不同的操作系统发行版,比如Ubuntu,Centos 等等。
(3)典型的 Linux 在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在 docker 中,起初也是将 rootfs 以 readonly 方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs 之上,并且允许再次将下层的 file system 设定为 readonly 并且向上叠加,这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行目录,每一个被称作一个 Layer。
(4)下面的这张图片非常好的展示了组装的过程,每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括命名空间、控制组、rootfs 等等,这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也能够减少磁盘的占用。
3. 镜像分层存储实战
3.1 基础知识
(1)tree 命令详解:
- Linux tree 命令用于以树状图列出目录的内容。
- 执行 tree 指令,它会列出指定目录下的所有文件,包括子目录里的文件。
(2)安装:
- ubuntu 安装:
apt install tree -y
- centos 安装:
yum install tree -y
(3)语法:
tree [-aACdDfFgilnNpqstux][-I <范本样式>][-P <范本样式>][目录...]
(4)常用参数说明:
- -a 显示所有文件和目录。
- -d 显示目录名称而非内容。
- -D 列出文件或目录的更改时间。
- -f 在每个文件或目录之前,显示完整的相对路径名称。
- -i 不以阶梯状列出文件或目录名称。
- -L level 限制目录显示层级。
- -l 如遇到性质为符号连接的目录,直接列出该连接所指向的原始目录。
- -P<范本样式> 只显示符合范本样式的文件或目录名称。
3.2 实战过程
(1)拉取镜像:
docker pull nginx:1.21.1
(2)docker image history 可以查看镜像的分层信息, nginx 的 1.21.1 的官方基于debian 的镜像制作文件如下:
[Dockerfile-nginx1.21.1.txt]
(3)通过 docker image history 查看如下,我们可以看到 dockerfile 和做出来的镜像是对应的,而且不是每一层都占用空间的。
[xiaomaker@xiaomaker-virtual-machine:~]$ docker history nginx:1.21.1
(4)我们通过 inspcet 命令查看,该镜像的存储位置,可以看到 GraphDriver 也就是我们的存储驱动,是 overlay2 的存储驱动, nginx 的 overlay2 的四个目录也都显示出来了,我们知道 docker 的默认目录是/var/lib/docker,之所以在/data/var/lib/docker 下面是因为我们规划了磁盘,调整了默认的存储目录。
docker image inspect nginx:1.21.1
(5)我们可以看到 lowerdir, upperdir,都位于/data/var/lib/docker/overlay2 下面,因为我们调整过默认存储位置所以对比默认的/var/lib/docker 多了/data,我们在这个目录下查找我们的 nginx,看下文件是如何被存储的。搜索后可以看到我们找到了多个nginx 文件,因为我们本地有多个 nginx 镜像所以搜到了多个 nginx 文件,通过lowerdir 的值我们可以确定有一个是和我们 nginx:1.12.1 的匹配上的。
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ tree -P nginx -f | grep "/sbin/nginx"
(6)同样的方式我们通过 Dockerfile 发现, nginx 还存储了个 docker-entrypoint.sh,我们搜索这个文件,我们发现这个文件也被放到了 diff 目录下面,和我们的 lowerdir 中一个 layer 是对应的。查看内容我们确定并没有什么加密之类的,也就是是镜像的文件分层后放到了 diff 目录下面。
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ tree -P "docker-entrypoint.sh" -f | grep "docker-entrypoint.sh"
(7)接下来我们进入 diff 的上一级目录查看,可以看到 link 文件,里面是每一个 diff 目录的短名称,或者说软链接。
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ ll ./14096822a444ae4469397896ad70aedb1b1e61799d3ead85089ab83914735e76/
total 16
-rw------- 1 root root 0 Jan 18 18:48 committed
drwxr-xr-x 2 root root 4096 Jan 18 18:48 diff
-rw-r--r-- 1 root root 26 Jan 18 18:48 link
-rw-r--r-- 1 root root 57 Jan 18 18:48 lower
drwx------ 2 root root 4096 Jan 18 18:48 work
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ cat ./14096822a444ae4469397896ad70aedb1b1e61799d3ead85089ab83914735e76/link
QJTI7BHG2F37TD3V4VR5FOFBYV[root@VM-0-6-centos overlay2]#
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ ll ./l/QJTI7BHG2F37TD3V4VR5FOFBYV
lrwxrwxrwx 1 root root 72 Jan 18
18:48 ./l/QJTI7BHG2F37TD3V4VR5FOFBYV -
> ../14096822a444ae4469397896ad70aedb1b1e61799d3ead85089ab83914735
e76/diff
(8)通过遍历 l 目录我们会发现,整个 docker 的镜像的 diff 目录都被做了对应的软链接,或者说起了个短名称。
(9)每一个 diff 是一个层级的内容,层级的关系是存放到了 lower 文件中,里面存放着父级的层级。
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ ll ./14096822a444ae4469397896ad70aedb1b1e61799d3ead85089ab83914735e76/
total 16
-rw------- 1 root root 0 Jan 18 18:48 committed
drwxr-xr-x 2 root root 4096 Jan 18 18:48 diff
-rw-r--r-- 1 root root 26 Jan 18 18:48 link
-rw-r--r-- 1 root root 57 Jan 18 18:48 lower
drwx------ 2 root root 4096 Jan 18 18:48 work
[xiaomaker@xiaomaker-virtual-machine:overlay2]$ cat ./14096822a444ae4469397896ad70aedb1b1e61799d3ead85089ab83914735e76/lower
l/OTMLVWGYWVFOBDWPG7FDXYI2N5:l/WXT5VHSKBEQXADHTKXC6VMRVNQ
(10)最后我们查看下 mergeddir,发现 merged dir 是不存在的,当我们启动为容器的时候才是有有效的。
(11)实战总结:
- 我们通过镜像实际存储位置可以看到镜像在存储的时候,通过分层来实现,并通过link 和 lower 完成层与层之间链接关系配置, diff 存放了我们的内容,并且没有什么加密。
4. overlay 文件系统工作实战
(1)我们首先创建一个目录用来挂载我们的文件系统:
mkdir -p /data/myworkdir/fs
(2)我们创建文件系统的工作目录:
cd /data/myworkdir/fs
mkdir upper lower merged work
(3)准备一些文件:
echo "in lower" > lower/in_lower.txt
echo "in upper" > upper/in_upper.txt
echo "In both. from lower" > lower/in_both.txt
echo "In both. from upper" > upper/in_both.txt
(4)挂载 overlay 目录:
[xiaomaker@xiaomaker-virtual-machine:fs]$ mount -t overlay overlay -o lowerdir=./lower,upperdir=./upper,workdir=./work ./merged
(5)通过 df -h 可以看到我们完成了挂载:
[xiaomaker@xiaomaker-virtual-machine:fs]$ df -h
(6)我们此时看下目录结构,然后发现 merged 目录自动生成了 3 个文件,可以看到boteh,lower,upper 都在。 merged 目录其实就是用户看到的目录,用户的实际文件操作在这里进行。
[xiaomaker@xiaomaker-virtual-machine:fs]$ tree -a .
├── lower
│ ├── in_both.txt
│ └── in_lower.txt
├── merged
│ ├── in_both.txt
│ ├── in_lower.txt
│ └── in_upper.txt
├── upper
│ ├── in_both.txt
│ └── in_upper.txt
└── work
└── work
(7)编辑:merge 目录下编辑一下 in_lower.txt, upper 目录下就会马上出现一个 in_lower.txt,而且内容就是编辑后的。而 lower 目录下的 in_lower.txt 内容不变:
[xiaomaker@xiaomaker-virtual-machine:fs]$ cd merged/
[xiaomaker@xiaomaker-virtual-machine:merged]$ vi in_lower.txt
#修改内容如下:
in lower! after edit!
[xiaomaker@xiaomaker-virtual-machine:merged]$ cat in_lower.txt
in lower! after edit!
#再次查看目录树,可以看到 in_lower.txt 在 upper 里面生成了,发生了
copy_up
root@139-159-150-152:/data/myworkdir/fs/merged# cd ..
[xiaomaker@xiaomaker-virtual-machine:fs]$ tree
.
├── lower
│ ├── in_both.txt
│ └── in_lower.txt
├── merged
│ ├── in_both.txt
│ ├── in_lower.txt
│ └── in_upper.txt
├── upper
│ ├── in_both.txt
│ ├── in_lower.txt
│ └── in_upper.txt
└── work
└── work
5 directories, 8 files
#查看文件内容,发现是编辑后的
root@139-159-150-152:/data/myworkdir/fs# cat upper/in_lower.txt
in lower! after edit!
(8)如果我们删除 in_lower.txt, lower 目录里的"in_lower.txt"文件不会有变化,只是在upper/ 目录中增加了一个特殊文件来告诉 OverlayFS, "in_lower.txt’这个文件不能出现在 merged/ 里了,类似 AuFS 的 whiteout:
[xiaomaker@xiaomaker-virtual-machine:fs]$ rm -f merged/in_lower.txt
[xiaomaker@xiaomaker-virtual-machine:fs]$ tree ../
├── lower
│ ├── in_both.txt
│ └── in_lower.txt
├── merged
│ ├── in_both.txt
│ └── in_upper.txt
├── upper
│ ├── in_both.txt
│ ├── in_lower.txt
│ └── in_upper.txt
└── work
└── work
[xiaomaker@xiaomaker-virtual-machine:fs]$ ll upper/
total 16
drwxr-xr-x 2 root root 4096 Mar 16 10:39 ./
drwxr-xr-x 6 root root 4096 Mar 16 10:29 ../
-rw-r--r-- 1 root root 21 Mar 16 10:29 in_both.txt
c--------- 1 root root 0, 0 Mar 16 10:39 in_lower.txt
-rw-r--r-- 1 root root 10 Mar 16 10:29 in_upper.txt
注意到 upper 下 in_lower.txt 的文件类型没有 ,而是 c 不是-或者 d。
(9)实战总结:
- 可以看到这种文件系统对于底层来说不影响,共享比较容易,但是如果编辑,删除频繁的话,性能还是比较差的。要不停的拷贝或者标记。
5. Docker卷原理介绍
5.1 Docker 卷的机制
(1)卷机制的介绍:
- Docker 又是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去呢?
- 当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(chroot 就是可以改变某进程的根目录,使这个程序不能访问目录之外的其他目录,这个跟我们在一个容器中是很相似的)之前,容器进程一直可以看到宿主机上的整个文件系统。
- 而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层,保存在 /var/lib/docker/overlay2/{layer id}/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/{layerid}/merged/ 目录中,这样容器所需的 rootfs 就准备好了。
- 所以,我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。
- 由于执行这个挂载操作时, “容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。
5.2 操作实战
5.2.1 Linux mount bind
(1)功能:通过 mount --bind 命令来将两个目录连接起来, mount --bind 命令是将前一个目录挂载到后一个目录上,所有对后一个目录的访问其实都是对前一个目录的访问。可以理解为硬链接。
(2)语法:
mount --bind src dest
(3)样例:
- 创建目录:
[xiaomaker@xiaomaker-virtual-machine:mount]$ mkdir -p /data/myworkdir/mount/data1
[xiaomaker@xiaomaker-virtual-machine:mount]$ mkdir -p /data/myworkdir/mount/data2
- 目录 1 中创建文件:
echo "Hello My bit" > /data/myworkdir/mount/data1/testfile.txt
- 查看两个目录:
[xiaomaker@xiaomaker-virtual-machine:mount]$ ll /data/myworkdir/mount/data1
total 12
drwxr-xr-x 2 root root 4096 Mar 25 17:39 ./
drwxr-xr-x 6 root root 4096 Mar 25 17:38 ../
-rw-r--r-- 1 root root 13 Mar 25 17:39 testfile.txt
[xiaomaker@xiaomaker-virtual-machine:mount]$ ll /data/myworkdir/mount/data2
total 8
drwxr-xr-x 2 root root 4096 Mar 25 17:38 ./
drwxr-xr-x 6 root root 4096 Mar 25 17:38 ../
- 绑定目录:
mount --bind /data/myworkdir/mount/data1 /data/myworkdir/mount/data2
- 查看目录:
[xiaomaker@xiaomaker-virtual-machine:mount]$ ll /data/myworkdir/mount/data2
total 12
drwxr-xr-x 2 root root 4096 Mar 25 17:39 ./
drwxr-xr-x 6 root root 4096 Mar 25 17:38 ../
-rw-r--r-- 1 root root 13 Mar 25 17:39 testfile.txt
[xiaomaker@xiaomaker-virtual-machine:mount]$ ll /data/myworkdir/mount/data1
total 12
drwxr-xr-x 2 root root 4096 Mar 25 17:39 ./
drwxr-xr-x 6 root root 4096 Mar 25 17:38 ../
- 修改文件,可以看到马上反应到另外一个目录:
[xiaomaker@xiaomaker-virtual-machine:mount]$ echo "Hello bit after my edit" >> /data/myworkdir/mount/data1/testfile.txt
[xiaomaker@xiaomaker-virtual-machine:mount]$ cat /data/myworkdir/mount/data2/testfile.txt
Hello My bit
Hello bit after my edit
5.2.2 查看 Docker 联合挂载
(1)启动一个容器,挂载一个目录:
docker run --rm -d --name mynginx -p 80:80 -v test_volume:/usr/share/nginx/html nginx:1.22.1
(2)查看 docker inspect 容器查看容器存储信息信息:
docker inspect mynginx
(3)执行 mount 查看一个容器的 meged 其实是 upper 和 lower 挂载而成:
[xiaomaker@xiaomaker-virtual-machine:mount]$ mount | grep overlay | grep 27bc85a04695d475066edb2573cf6b0dc5b4fdea45665a88303275cadcc6c257 overlay on
docker_commit__420">5.2.3 Docker 卷深度思考(docker commit 能提交卷里面的内容吗)
(1)容器的镜像操作,比如 docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来,容器中可读写层的 /test 目录(/var/lib/docker/overlay2/{layerid}/[可读写层 ID]/test),始终是空的。在宿主机的眼里 /test 的 inode 就是原本没有重定向的 inode,你所有的修改都不在这个文件下。不过,由于 Docker 一开始还是要创建 /test 这个目录作为挂载点,所以执行了 dockercommit 之后,你会发现新产生的镜像里,会多出来一个空的 /test 目录。
(1)实际跑个案例看下:
- 启动容器,挂载下文件:
docker run --rm -d --name mynginx -p 80:80 -v test_volume:/test nginx:1.22.1
- 进入容器的挂载宿主机目录,修改 index.html:
[xiaomaker@xiaomaker-virtual-machine:mount]$ cd /data/var/lib/docker/volumes/test_volume/_data
[xiaomaker@xiaomaker-virtual-machine:_data]$ echo "Hello bit for test commit " > index.html
- 容器中查看:
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker exec mynginx cat /test/index.html
Hello bit for test commit
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker commit mynginx mycommit:v0.1
sha256:94ffd1b2035b9130893de027a1770eaef1e7c3cb7c79706c9766eb9d0d796044
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker images | grep mycommit:v0.1
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker images | grep mycomm
mycommit v0.1
94ffd1b2035b 28 seconds ago 142MB
- 然后查看新镜像中文件,可以看到只有一个空目录:
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker run --rm -it mycommit:v0.1 ls -l /test
total 0
[xiaomaker@xiaomaker-virtual-machine:_data]$ docker run --rm -it mycommit:v0.1 ls -l /
total 76
drwxr-xr-x 2 root root 4096 Feb 27 00:00 bin
drwxr-xr-x 2 root root 4096 Dec 9 19:15 boot
drwxr-xr-x 5 root root 360 Mar 25 10:11 dev
drwxr-xr-x 1 root root 4096 Mar 1 18:43 docker-entrypoint.d
-rwxrwxr-x 1 root root 1616 Mar 1 18:43 docker-entrypoint.sh
drwxr-xr-x 1 root root 4096 Mar 25 10:11 etc
drwxr-xr-x 2 root root 4096 Dec 9 19:15 home
drwxr-xr-x 1 root root 4096 Feb 27 00:00 lib
drwxr-xr-x 2 root root 4096 Feb 27 00:00 lib64
drwxr-xr-x 2 root root 4096 Feb 27 00:00 media
drwxr-xr-x 2 root root 4096 Feb 27 00:00 mnt
drwxr-xr-x 2 root root 4096 Feb 27 00:00 opt
dr-xr-xr-x 221 root root 0 Mar 25 10:11 proc
drwx------ 2 root root 4096 Feb 27 00:00 root
drwxr-xr-x 1 root root 4096 Mar 25 10:06 run
drwxr-xr-x 2 root root 4096 Feb 27 00:00 sbin
drwxr-xr-x 2 root root 4096 Feb 27 00:00 srv
dr-xr-xr-x 13 root root 0 Mar 25 10:11 sys
drwxr-xr-x 2 root root 4096 Mar 25 10:06 test
drwxrwxrwt 1 root root 4096 Mar 1 18:43 tmp
drwxr-xr-x 1 root root 4096 Feb 27 00:00 usr
drwxr-xr-x 1 root root 4096 Feb 27 00:00 var
6. Docker网络原理介绍
6.1 Linux 常见网络虚拟化
(1)什么是虚拟网卡:虚拟网卡(又称虚拟网络适配器),即用软件模拟网络环境,模拟网络适配器。简单来说就是软件模拟的网卡。
6.1.1 虚拟网卡:tun/tap
(1)简介:
- tap/tun 虚拟了一套网络接口,这套接口和物理的接口无任何区别,可以配置 IP,可以路由流量,不同的是,它的流量只在主机内流通。
- tun 和 tap 是两个相对独立的虚拟网络设备,其中 tap 模拟了以太网设备,操作二层数据包(以太帧), tun 则模拟了网络层设备,操作三层数据包(IP 报文)。
- 使用 tun/tap 设备的目的是实现把来自协议栈的数据包先交由某个打开了
/dev/net/tun 字符设备的用户进程处理后,再把数据包重新发回到链路中。你可以通俗地将它理解为这块虚拟化网卡驱动一端连接着网络协议栈,另一端连接着用户态程序,而普通的网卡驱动则是一端连接着网络协议栈,另一端连接着物理网卡。 - 最典型的 VPN 应用程序为例,程序发送给 tun 设备的数据包:
- 应用程序通过 tun 设备对外发送数据包后, tun 设备,便会把数据包通过字符设备发送给 VPN 程序, VPN 收到数据包,会修改后再重新封装成新报文,譬如数据包原本是发送给 A 地址的, VPN 把整个包进行加密,然后作为报文体,封装到另一个发送给 B 地址的新数据包当中。然后通过协议栈发送到物理网卡发送出去。
- 使用 tun/tap 设备传输数据需要经过两次协议栈,不可避免地会有一定的性能损耗,所以引入了新的网卡实现方式 veth。
(2)生活案例理解虚拟网卡tun和tap:
- 虚拟网卡就像游戏中的战马至于实际生活中的战马,在游戏中,人物可以骑着虚拟的马战斗。
6.1.2 虚拟网卡:veth
(1)简介:
- 在 Linux Kernel 2.6 版本, Linux 开始支持网络名空间隔离的同时,也提供了专门的虚拟以太网(Virtual Ethernet,习惯简写做 veth)让两个隔离的网络名称空间之间可以互相通信。
- 直接把 veth 比喻成是虚拟网卡其实并不十分准确,如果要和物理设备类比,它应该相当于由交叉网线连接的一对物理网卡。形象化的理解如下:
- veth 实际上不是一个设备,而是一对设备,因而也常被称作 veth pair。要使用 veth,必须在两个独立的网络名称空间中进行才有意义,因为 veth pair 是一端连着协议栈,另一端彼此相连的,在 veth 设备的 其中一端输入数据,这些数据就会从设备的另外一端原样不变地流出。
- veth 通信不需要反复多次经过网络协议栈,这让 veth 比起 tap/tun 具有更好的性能。
- veth 实现了点对点的虚拟连接,可以通过 veth 连接两个 namespace,如果我们需要将 3 个或者多个 namespace 接入同一个二层网络时,就不能只使用 veth 了。 在物理网络中,如果需要连接多个主机,我们会使用网桥,或者又称为交换机。 Linux 也提供了网桥的虚拟实现。
(2)生活案例来理解虚拟网卡veth:
- veth 就像一座桥,直接连接两个地方。
6.1.3 虚拟交换机
(1)简介:
- 使用 veth pair 将两个隔离的 netns 连接在了一起,在现实世界里等同于用一根网线把两台电脑连接在了一起,但是在现实世界里往往很少会有人这样使用。因为一台设备不仅仅只需要和另一台设备通信,它需要和很多很多的网络设备进行通信,如果还使用这样的方式,需要十分复杂的网络接线,并且现实世界中的普通网络设备也没有那么多网络接口。
- 那么,想要让某一台设备和很多网络设备都可以通信需要如何去做呢?在我们的日常生活中,除了手机和电脑,最常见的网络设备就是路由器了,我们的手机连上 WI-FI,电脑插到路由器上,等待从路由器的 DHCP 服务器上获取到 IP,他们就可以相互通信了,这便是路由器的二层交换功能在工作。 Linux Bridge 最主要的功能就是二层交换,是对现实世界二层交换机的模拟。
- Linux Bridge ,由 brctl 命令创建和管理。 Linux Bridge 创建以后,真实的物理设备(如 eth0)抑或是虚拟的设备(veth 或者 tap)都能与 Linux Bridge 配合工作。
- 有了虚拟化网络设备后,下一步就是要使用这些设备组成网络。
(2)生活案例来理解虚拟交换机:
- 交换机就像立交桥,有很多直接相连的道路组成。
6.1.4 虚拟组网:VxLan
(1)简介:
- 物理网络的拓扑结构是相对固定的。云原生时代的分布式系统的逻辑拓扑结构变动频率,譬如服务的扩缩、断路、限流,等等,都可能要求网络跟随做出相应的变化。正因如此,软件定义网络(Software Defined Network, SDN)的需求在云计算和分布式时代变得前所未有地迫切, SDN 的核心思路是在物理的网络之上再构造一层虚拟化的网络。
- SDN 里位于下层的物理网络被称为 Underlay,它着重解决网络的连通性与可管理性,位于上层的逻辑网络被称为 Overlay,它着重为应用提供与软件需求相符的传输服务和网络拓扑。
(2)vlan:
- 交换机是一个 L2 设备,插在同一个交换机的网络设备组成了一个 L2 网络, L2 网络之间通过 MAC 地址通信,同时这个 L2 网络也是一个广播域。
- 同属于一个广播域的两个设备想要通信,一设备须得向网络中的所有设备发送请求信息,只有对应 MAC 地址的设备才是真正的接收方,但实际上却是数据帧传遍整个网络,所有设备都会收到,且直接丢弃。
如此一来,将造成一系列不好的后果:网络整体带宽被占用、潜在的信息安全风险、占用 CPU 资源…… - 因此, VLAN 应运而生!Vlan(Virtual Local Area Network)即虚拟局域网,是一个将物理局域网在逻辑上划分成多个广播域技术。通过在交换机上配置 Vlan,可以实现在同一 Vlan 用户可以进行二层互访,在不同 Vlan 间的用户被二层隔离,这样既能够隔离广播域,又可以提升网络安全性。
(3)VLAN 究竟能够解决什么问题?
- 限制广播域。广播域被限制在一个局域网内,节省了带宽,提高了网络处理能力。
- 增强局域网的安全性。不同局域网内的报文在传输时是相互隔离的,即一个 VLAN内的用户不能和其它 VLAN 内的用户直接通信,如果不同 VLAN 要进行通信,则需要通过路由器或三层交换机等三层设备。
- 灵活构建虚拟工作组。用局域网可以划分不同的用户到不同的工作组,同一工作组的用户也不必局限于某一固定的物理范围,网络构建和维护更方便灵活。
(4)不过 VLAN 也并非没有缺点。
- 随着虚拟化技术的发展,一台物理服务器往往承载了多台虚拟机,公有云或其它大型虚拟化云数据中心动辄需容纳上万甚至更多租户, VLAN 技术最多支持 4000 多个VLAN,逐渐无法满足需求。
- 公有云提供商的业务要求将实体网络租借给多个不同的用户,这些用户对于网络的要求有所不同,而不同用户租借的网络有很大的可能会出现 IP 地址、 MAC 地址的重叠。传统的 VLAN 并没有涉及这个问题,因此需要一种新的技术来保证在多个租户网络中存在地址重叠的情况下依旧能有效通信的技术。
- 虚拟化技术使得单台主机可以虚拟化出多台虚拟机同时运行,而每台虚拟机都会有其唯一的 MAC 地址。这样,为了保证集群中所有虚机可以正常通信,交换机必须保存每台虚机的 MAC 地址,这样就导致了交换机中的 MAC 表异常庞大,从而影响交换机的转发性能。
(5)vxlan:
- VXLAN 是另一种网络虚拟化技术,有点类似于 VLAN,但功能更强大。
- 在传统的 VLAN 网络中,共享同一底层 L2 网段的 VLAN 不能超过 4096 个。只有 12比特用于对 Ethernet Frame 格式中的 VLAN ID 字段进行编码。
- VXLAN 协议定义了 8 个字节的 VXLAN Header,引入了类似 VLAN ID 的网络标识,称为 VNI(VXLAN Network ID),由 24 比特组成,这样总共是 1600 多万个,从而满足了大规模不同租户之间的标识、隔离需求。
- VXLAN 是基于 L3 网络构建的虚拟 L2 网络,是一种 Overlay 网络。 VXLAN 不关心底层物理网络拓扑,它将 Ethernet Frame 封装在 UDP 包中,只要它能承载 UDP 数据包,就可以在远端网段之间提供以太网 L2 连接。
- 每个 VXLAN 节点上的出站 L2 Ethernet Frame 都会被捕获,然后封装成 UDP 数据包,并通过 L3 网络发送到目标 VXLAN 节点。当 L2 Ethernet Frame 到达 VXLAN 节点时,就从 UDP 数据包中提取(解封装),并注入目标设备的网络接口。这种技术称为隧道。
- 因此, VXLAN 节点会创建一个虚拟 L2 网段,从而创建一个 L2 广播域。
(6)生活案例来理解VXLAN :
- vxlan 就像中国的路网,一样连接了很多城市。
6.1.5 MacVLan 和 IPVLan
(1)MacVLan简介:
- MACVLAN 允许对同一个网卡设置多个 IP 地址,还允许对同一张网卡上设置多个MAC 地址,这也是 MACVLAN 名字的由来。原本 MAC 地址是网卡接口的“身份证”,应该是严格的一对一关系,而 MACVLAN 打破这层关系,方法是在物理设备之上、网络栈之下生成多个虚拟的 Device,每个 Device 都有一个 MAC 地址,新增 Device 的操作本质上相当于在系统内核中注册了一个收发特定数据包的回调函数,每个回调函数都能对一个 MAC 地址的数据包进行响应,当物理设备收到数据包时,会先根据MAC 地址进行一次判断,确定交给哪个 Device 来处理。
- 可以看到 bridge 到 macvlan 缩短了通讯链路,性能比较高:
- 许多网卡在硬件层面对支持的 MAC 地址数量存在限制。超过该限制会导致性能的下降。所以 macvlan 虽然近乎完美还是没有实际推广完成。
(2)生活案例理解MacVLan:
- macvlan 相当于一个人有多个身份证。
(3)IPVLan简介:
- Ipvlan 与 macvlan 非常相似,但又存在显著不同。 Ipvlan 的子接口上并不拥有独立的MAC 地址。所有共享父接口 MAC 地址的子接口拥有各自独立的 IP。
- 共享 MAC 地址会影响 DHCP 相关的操作。如果虚拟机、容器需通过 DHCP 获取网络配置,请确保它们在 DHCP 请求中使用各自独立的 ClientID; DHCP 服务器会根据请求中的 ClientID 而非 MAC 地址来分配 IP 地址。某些设备不支持分配多个 ip 地址所以IPVlan 也没有实际大规模推广。
(4)生活案例理解IPVLan:
- ipvlan 相当于一个身份证对应了多个人。
docker__593">6.2 docker 网络分类
(1)docker 常见的网络的分类如下,其中 bridge 网络、 host 网络、container 网络、 none网络、 overlay 网络、 macvlan 网络、 ipvlan 网络。其中 overlay 网络往往配合 swarm和 k8s 来使用。
(2)bridge 网络:
- 命令如下:
docker network create -d bridge bridgenet1
(3)host 网络:
- 如果启动容器的时候使用 host 模式,那么这个容器将不会获得一个独立的
Network Namespace,而是和宿主机共用一个 Network Namespace。容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。 host 网络结构如下:
- host 网络创建命令如下:
docker run --name c2 -itd --network=host busybox
(4)container 网络:
- 这个模式指定新创建的容器和引进存在的一个容器共享一个 network namespace ,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 ip,而是和一个指定的容器共享 ip,端口等,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。容器网络原理如下:
- 容器网络创建命令参考如下:
docker run -itd --name netcontainer2 --network
container:netcontainer1 busybox
(5)none 网络:
- Docker 容器拥有自己的 Network Namespace,但是,并不为 Docker 容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、 IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。
- 命令如下:
docker run -itd --name c3 --network none busybox
(6)overlay 网络:
- Overlay 驱动创建一个支持多主机网络的覆盖网络。
- 在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装在 IP 报文之上的新的数据格式。
- Overlay 网络实际上是目前最主流的容器跨节点数据传输和路由方案,底层原理VXLAN。
- Overlay 网络将多个 Docker 守护进程连接在一起,允许不同机器上相互通讯,同时支持对消息进行加密,实现跨主机的 docker 容器之间的通信, Overlay 网络将多个Docker 守护进程连接在一起,使 swarm 服务能够相互通信。这种策略消除了在这些容器之间进行操作系统级路由的需要。下图是个典型的 overlay 网络。
- overlay 网络的创建命令如下:
docker network create -d overlay ovnet1
(7)macvlan 网络:
- 为每个容器的虚拟网络接口分配一个 MAC 地址,使其看起来是直接连接到物理网络的物理网络接口。在这种情况下,您需要在 Docker 主机上指定一个物理接口以用于 .macvlan 以及 子网和网关 macvlan。您甚至可以 macvlan 使用不同的物理网络接口隔离您的网络。注意要把对应的网卡开启混杂模式命令为 ifconfig 网卡名称(如 eth0)promisc。macvlan 典型的拓扑如下:
- macvlan 的创建命令如下:
docker network create -d macvlan --subnet=172.16.10.0/24 --
gateway=172.16.10.1 -o parent=eth0 mac1
(8)ipvlan 网络:
- ipvlan 和 overlay 都可以实现不同主机上的容器之间的通讯,但是 ipvlan 是所有容器都在一个网段,相当于在一个 vlan 里面,然后可以通过不同的子接口对应不同网段,实现不同容器之间的通讯。而 overlay 可以实现不同网段之间的通讯。 ipvlan 的 l2 网络示例如下:
- ipvlan 的创建命令参考如下:
docker network create -d ipvlan --subnet=192.168.1.0/24 --
gateway=192.168.1.1 -o ipvlan_mode=l2 -o parent=eth0 pub_net
docker_Bridge__675">6.3 深入理解 docker Bridge 网络
6.3.1 网络介绍
(1)介绍如下:
- Docker Bridge 网络采用内置的 bridge 驱动, bridge 驱动底层采用的是 Linux 内核中Linux bridge 技术。就网络而言, bridge 网络是在网络段之间转发流量的链路层设备,而网桥可以是在主机内核中运行的硬件设备或软件设备;就 Docker 而言,桥接网络使用软件网桥 docker0,它允许连接到同一网桥网络的容器进行通信,同时提供与未连接到该网桥网络容器的隔离。
- Docker Container 的 bridge 桥接模式可以参考下图:
- Bridge 桥接模式的实现步骤主要如下:
- Docker Daemon 利用 veth pair 技术,在宿主机上创建两个虚拟网络接口设备,假设为 veth0 和 veth1。而 veth pair 技术的特性可以保证无论哪一个 veth 接收到网络报文,都会将报文传输给另一方。
- Docker Daemon 将 veth0 附加到 Docker Daemon 创建的 docker0 网桥上。保证宿主机的网络报文可以发往 veth0;
- Docker Daemon 将 veth1 添加到 Docker Container 所属的 namespace 下,并被改名为 eth0。如此一来,保证宿主机的网络报文若发往 veth0,则立即会被 eth0 接收,实现宿主机到 Docker Container 网络的联通性;同时,也保证 Docker Container 单独使用 eth0,实现容器网络环境的隔离性。
docker_bridge__veth_pair__687">6.3.2 实战一、 docker bridge 的 veth pair 如何对应
(1)我们运行一个 busybox 容器:
docker run --name netcontainer1 -itd busybox
(2)我们在容器中上查看 pair 信息:
[xiaomaker@xiaomaker-virtual-machine:user]$ docker exec -it netcontainer1 sh
/ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
536: eth0@if537: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500
qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
/ # cat /sys/class/net/eth0/iflink537
(3)宿主机上我们执行 ip link,找到 id 为 537 的这样两个 pair 就对应上了。
root@139-159-150-152:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state
UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel
state UP mode DEFAULT group default qlen 1000
link/ether fa:16:3e:9d:f4:ac brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
noqueue state UP mode DEFAULT group default
link/ether 02:42:d7:87:d1:1b brd ff:ff:ff:ff:ff:ff
537: veth600bcb3@if536: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 3e:a8:bc:98:4d:90 brd ff:ff:ff:ff:ff:ff linknetnsid 0
539: veth81c87ff@if538: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 5a:35:04:aa:f3:71 brd ff:ff:ff:ff:ff:ff linknetnsid 1
6.3.3 实战二、容器与宿主机/外界主机通信原理
(1)实现原理:
- Bridge 桥接模式,从原理上实现了 Docker Container 到宿主机乃至其他机器的网络连通性。然而,由于宿主机的 IP 地址与 veth pair 的 IP 地址均不在同一个网段,故仅仅依靠 veth pair 和 namespace 的技术,还不足以是宿主机以外的网络主动发现 DockerContainer 的存在。
- 为了使得 Docker Container 可以让宿主机以外的世界感知到容器内部暴露的服务, Docker 采用 NAT(Network Address Translation,网络地址转换)的方式,让宿主机以外的世界可以主动将网络报文发送至容器内部。
- 具体来讲,当 Docker Container 需要暴露服务时,内部服务必须监听容器 IP 和端口号port_0,以便外界主动发起访问请求。由于宿主机以外的世界,只知道宿主机 eth0 的网络地址,而并不知道 Docker Container 的 IP 地址,哪怕就算知道 Docker Container的 IP 地址,从二层网络的角度来讲,外界也无法直接通过 Docker Container 的 IP 地址访问容器内部应用。
- 因此, Docker 使用 NAT 方法,将容器内部的服务监听的端口与宿主机的某一个端口 port_1 进行“绑定”。
(2)如此一来,外界访问 Docker Container 内部服务的流程为:
- 外界访问宿主机的 IP 以及宿主机的端口 port_1;
- 当宿主机接收到这样的请求之后,由于 DNAT 规则的存在,会将该请求的目的 IP(宿主机 eth0 的 IP)和目的端口 port_1 进行转换,转换为容器 IP 和容器的端口port_0;
- 由于宿主机认识容器 IP,故可以将请求发送给 veth pair;
- veth pair 的 veth0 将请求发送至容器内部的 eth0,最终交给内部服务进行处理。使用 DNAT 方法,可以使得 Docker 宿主机以外的世界主动访问 Docker Container 内部服务。那么 Docker Container 如何访问宿主机以外的世界呢。
(3)以下简要分析 Docker Container 访问宿主机以外世界的流程:
- Docker Container 内部进程获悉宿主机以外服务的 IP 地址和端口 port_2,于是Docker Container 发起请求。容器的独立网络环境保证了请求中报文的源 IP 地址为容器 IP(即容器内部 eth0),另外 Linux 内核会自动为进程分配一个可用源端口(假设为 port_3) ;
- 请求通过容器内部 eth0 发送至 veth pair 的另一端,到达 veth0,也就是到达了网桥(docker0)处;
- docker0 网桥开启了数据报转发功能(/proc/sys/net/ipv4/ip_forward),故将请求发送至宿主机的 eth0 处;
- 宿主机处理请求时,使用 SNAT 对请求进行源地址 IP 转换,即将请求中源地址 IP(容器 IP 地址)转换为宿主机 eth0 的 IP 地址;
- 宿主机将经过 SNAT 转换后的报文通过请求的目的 IP 地址(宿主机以外世界的 IP地址)发送至外界。
(4)那么对于 Docker Container 内部主动发起对外的网络请求,当请求到达宿主机进行 SNAT 处理后发给外界,当外界响应请求时,响应报文中的目的IP 地址肯定是 Docker 宿主机的 IP 地址,那响应报文回到宿主机的时候,宿主机又是如何转给 Docker Container 的呢?
- 关于这样的响应,由于 port_3 端口并没有在宿主机上做相应的 DNAT 转换,原则上不会被发送至容器内部。为什么说对于这样的响应,不会做 DNAT 转换呢。
- 原因很简单, DNAT 转换是针对容器内部服务监听的特定端口做的,该端口是供服务监听使用,而容器内部发起的请求报文中,源端口号肯定不会占用服务监听的端口,故容器内部发起请求的响应不会在宿主机上经过 DNAT 处理。
(5)其实,这一环节的内容是由 iptables 规则来完成,具体的 iptables 规则如下:
iptables -I FORWARD -o docker0 -m conntrack --ctstate
RELATED,ESTABLISHED -j ACCEPT
- 这条规则的意思是,在宿主机上发往 docker0 网桥的网络数据报文,如果是该数据报文所处的连接已经建立的话,则无条件接受,并由 Linux 内核将其发送到原来的连接上,即回到 Docker Container 内部。
- IP-Forwarding IP 转发。 一种路由协议。 IP 转发是操作系统的一种选项,支持主机起到路由器的功能。在一个系统中含有两块以上的网卡,并将 IP 转发选项打开,这样该系统就可以作为路由器进行使用了。
(6)实战操作:
- iptables 组成 Linux 平台下的包过滤防火墙,与大多数的 Linux 软件一样,这个包过滤防火墙是免费的,它可以代替昂贵的商业防火墙解决方案,完成封包过滤、封包重定向和网络地址转换等功能。
- -t 指定表名,在 iptables 中,有四张表: filter:这里面的链条,规则,可以决定一个数据包是否可以到达目标进程端口; mangle: 这里面的链条,规则,可以修改数据包的内容,比如 ttl; nat:这里面的链条,规则,可以修改源和目标的 ip 地址,从而进行包路由; raw:这里面的链条,规则,能基于数据包的状态进行规则设定。
- -n 使用数字形式(numeric)显示输出结果。
- -v 查看规则表详细信息(verbose)的信息。
- -L 列出(list)指定链中所有的规则进行查看。
(7)下面我们可以通过 iptables 命令来查看 NAT 转换表:
[xiaomaker@xiaomaker-virtual-machine:~]$ sudo iptables -t nat -nvL
[sudo] password for lyc:
Chain PREROUTING (policy ACCEPT 410 packets, 19228 bytes)
pkts bytes target prot opt in out source
destination
1791K 104M DOCKER all -- * * 0.0.0.0/0
0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 410 packets, 19228 bytes)
pkts bytes target prot opt in out source
destination
Chain OUTPUT (policy ACCEPT 359 packets, 22606 bytes)
pkts bytes target prot opt in out source
destination
1308 78480 DOCKER all -- * *
0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type
LOCAL
Chain POSTROUTING (policy ACCEPT 360 packets, 22646 bytes)
pkts bytes target prot opt in out source
destination
0 0 MASQUERADE all -- * !br-0aa8840cec0c
172.18.0.0/16 0.0.0.0/0
2 96 MASQUERADE all -- * !docker0 172.17.0.0/16
0.0.0.0/0
0 0 MASQUERADE all -- * !br-45ac8acddba4
172.19.0.0/16 0.0.0.0/0
2 168 MASQUERADE all -- * !br-5ab488f2287c
172.18.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.7
172.17.0.7 tcp dpt:80
Chain DOCKER (2 references)
pkts bytes target prot opt in out source
destination
0 0 RETURN all -- br-0aa8840cec0c * 0.0.0.0/0
0.0.0.0/0
0 0 RETURN all -- docker0 * 0.0.0.0/0
0.0.0.0/0
4 204 DNAT tcp -- !docker0 * 0.0.0.0/0
0.0.0.0/0 tcp dpt:8088 to:172.17.0.7:80
看到最后的表项, 该表项类型为 DNAT(Destination NAT), tcp dpt:8088
to:172.17.0.7:80 表示将发往本机 8088 端口的流量转发到容器的 80 端口。
6.3.4 实战三、容器的网络命名空间查找
(1)基础知识:
- nsenter 命令是一个可以在指定进程的命名空间下运行指定程序的命令。它位于 utillinux 包中。一个最典型的用途就是进入容器的网络命令空间。相当多的容器为了轻量级,是不包含较为基础的命令的,比如说 ip address, ping, telnet, ss, tcpdump 等等命令,这就给调试容器网络带来相当大的困扰:只能通过 docker inspectContainerID 命令获取到容器 IP,以及无法测试和其他网络的连通性。这时就可以使用 nsenter 命令仅进入该容器的网络命名空间,使用宿主机的命令调试容器网络。
- 语法:
nsenter [options] <program> [<argument>...]
- 常用参数:
参数 | 含义 |
---|---|
-t, --target <pid> | 进入目标进程的命名空间 |
-i, --ipc[=<file>] | 进入 IPC 空间 |
-m, --mount[=<file>] | 进入 Mount 空间 |
-n, --net[=<file>] | 进入 Net 空间 |
-p, --pid[=<file>] | 进入 Pid 空间 |
-u, --uts[=<file>] | 进入 UTS 空间 |
-U, --user[=<file>] | 进入用户空间 |
-V, --version | 版本查看 |
- 案例:
ip netns add ns111
#看到的是宿主机的网络信息
ifconfig -a
ll /var/run/netns/
nsenter --net=/var/run/netns/ns111
#看到的是隔离空间的信息
ifconfig -a
(2)实战:
- ip netns list 查找 docker 网络命名空间:docker 使用 namespace 实现容器网络,但是使用 ip netns 命令却无法在主机上看到任何 docker 容器的 network namespace,这是因为默认 docker 把创建的网络命名空间链接文件隐藏起来了。ip netns 默认是去检查/var/run/netns 目录的。而 Docker 容器对应的 ns 信息记录到了/var/run/docker/netns 目录。所以 ip netns 查出来就是空的:
[xiaomaker@xiaomaker-virtual-machine:netns]$ ll /var/run/docker/netns/
total 0
drwxr-xr-x 2 root root 100 Mar 17 09:51 ./
drwx------ 8 root root 180 Mar 10 16:40 ../
-r--r--r-- 1 root root 0 Mar 16 21:56 22e66c104e59
-r--r--r-- 1 root root 0 Mar 16 22:15 42d0ab39867a
-r--r--r-- 1 root root 0 Mar 17 09:51 a2d2e95c8b31
[xiaomaker@xiaomaker-virtual-machine:netns]$ docker run -itd --name net4 -p 81:80 busybox
36dd63a05de607d2a4d271060d01dd96ea53b49162f825c7625e268461e98e0e
[xiaomaker@xiaomaker-virtual-machine:netns]$ docker inspect net4 | grep Sand -A 10
"SandboxID":
"00899eb26020c8222cb78b60b8bcf053d1086e28c9e26c625a2b59f352ef6231"
,
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "81"
},
{
--
"SandboxKey": "/var/run/docker/netns/00899eb26020",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID":
"d8bdbef789874cc052c0df9ea3390cd6120f74ea9c39d783dc877588baf6c398"
,
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.4",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:04",
- 通过 nsenter 进入命名空间,查看 ip 地址:
[xiaomaker@xiaomaker-virtual-machine:netns]$ nsenter --net=/var/run/docker/netns/00899eb26020 sh
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state
UNKNOWN group default qlen 1000
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
559: eth0@if560: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
noqueue state UP group default
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff linknetnsid 0
inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
[xiaomaker@xiaomaker-virtual-machine:netns]$ docker exec -it net4 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
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
559: eth0@if560: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500
qdisc noqueue
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ #
7. Docker总结
(1)Docker是一个用于开发、交付和运行应用程序的开放平台,它允许开发人员将应用程序及其依赖项打包到一个独立的、可移植的容器中。以下是对Docker的整体总结:
7.1 Docker的基本概念
- 镜像(Image):是一个只读模板,包含了运行容器所需的所有文件和设置。可以把它理解为一个root文件系统,或者是一个包含了应用程序及其依赖项的打包文件。通过镜像,可以创建容器。
- 容器(Container):是一个运行中的实例,基于镜像创建。每个容器都是独立的,拥有自己的文件系统、环境变量和资源隔离,保证了应用程序的运行环境一致性。容器可以被启动、停止、删除和复制。
- 仓库(Repository):是存储和分享镜像的地方。可以从仓库中拉取镜像,并将自己创建的镜像推送到仓库中。Docker Hub是Docker公司提供的官方仓库,此外还有其他公开的或私有的仓库可供选择。
7.2 Docker的优势
- 更快速的交付和部署:传统的应用开发完成后,需要提供一堆安装程序和配置说明文档。而Docker化之后只需要交付少量容器镜像文件,在正式生产环境加载镜像并运行即可,大大节省了部署配置和测试验证时间。
- 更高效的虚拟化:Docker是内核级虚拟化,不需要额外的Hypervisor支持,因此可以在一台物理机上运行很多个容器实例,大大提升了物理服务器的CPU和内存的利用率。
- 更轻松的迁移和扩展:Docker容器可以在任何支持Docker的环境中运行,无论是开发机、测试环境还是生产环境。这意味着可以轻松地在不同的环境中部署和移植应用程序。同时,当现有的容器不足以支撑业务处理时,可通过镜像运行新的容器进行快速扩容。
- 更简单的管理:Docker提供了丰富的命令和工具来管理镜像、容器、网络和存储等资源,使得容器的管理变得非常简单和高效。
7.3 Docker的使用场景
- Web应用的自动化打包和发布:Docker可以方便地打包Web应用及其依赖项,并快速部署到生产环境中。
- 自动化测试和持续集成、发布:Docker可以创建一致的测试环境,使得自动化测试和持续集成变得更加简单和可靠。
- 在服务型环境中部署和调整数据库或其他的后台应用:Docker容器可以提供隔离的运行环境,使得数据库或其他后台应用可以更加安全、稳定地运行。
- 搭建自己的PaaS环境:通过从头编译或者扩展现有的OpenShift或Cloud Foundry平台,可以使用Docker搭建自己的PaaS环境。
7.4 Docker的局限性
- 环境限制:Docker是基于Linux 64bit的,无法在32bit的Linux、Windows或Unix环境下使用。虽然Docker提供了Windows和macOS的客户端,但这些客户端仍然需要依赖一个虚拟的Linux环境来运行容器。
- 隔离性相对有限:虽然Docker容器提供了进程、网络和存储的隔离,但其隔离性相比KVM之类的虚拟化方案还是有些欠缺。所有容器共用一部分的运行库,可能会存在一定的安全风险。
- 对disk的管理比较有限:Docker对磁盘的管理相对简单,可能无法满足一些复杂的存储需求。
7.5 Docker的发展趋势
- 商业化发展:随着Docker在容器市场的领先地位逐渐稳固,Docker的定价政策也在不断调整。除了开源版的Docker外,Docker还推出了面向企业的付费版本——Docker Enterprise,提供了更为专业的技术支持、增强的安全功能以及容器集群的管理工具等服务。
- 与Kubernetes等技术的结合:虽然Kubernetes成为了容器编排的首选工具,但Docker和Kubernetes经常一起使用。Docker负责容器化应用程序,而Kubernetes负责管理这些容器的部署和扩展。这种结合使得容器化部署方案变得更加复杂和灵活。
- 安全性增强:Docker在安全性方面不断加强,提供了更多的安全特性和最佳实践,帮助用户更好地保护容器化应用。