docker - 镜像、存储卷和网络深入理解

镜像原理

docker 是操作系统层的虚拟化,所以 docker 镜像的本质是在模拟操作系统。我们先看 下操作系统是什么。

操作系统基础

操作系统由:进程调度子系统、进程通信子系统、内存管理子系统、设备管理子系统、文件管理子系统、网络通信子系统、作业控制子系统组成。 Linux 的文件管理子系统由 bootfs 和 rootfs 组成。

(1). bootfs:要包含 bootloader 和 kernel, bootloader 主要是引导加载 kernel, Linux 刚 启动时会加载 bootfs 文件系统,在 Docker 镜像的最底层是引导文件系统 bootfs。这 一层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加载器和内核。当 boot 加载 完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时 系统也会卸载 bootfs。

(2). rootfs: 在 bootfs 之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等 标准目录和文件。rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,Centos 等等。

Union FS(联合文件系统)

联合文件系统(Union File System),2004 年由纽约州立大学开发,它可以把多个 目录内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS 可以把只 读和可读写文件系统合并在一起,具有写时复制功能,允许只读文件系统的修改可以 保存到可写文件系统当中。 UnionFS(联合文件系统)是一种分层、轻量级并且高性能的文件系统,它支持对 文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚 拟文件系统下。UnionFS 是一种为 Linux,FreeBSD 和 NetBSD 操作系统设计的把其 他文件系统联合到一个联合挂载点的文件系统服务。它使用 branch 把不同文件系统的 文件和目录"透明地"覆盖,形成一个单一一致的文件系统。这些 branches 或者是 read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时 候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对 任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一 个重要的资管管理技术叫写时复制。

写时复制(copy-on-write,下文简称 CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何 修改,这时候并不需要立即创建一个新的资源;这个资源可以被新旧实例共享。创建 新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方 式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增 加小部分的开销。

docker镜像是什么

image 里面是一层层文件系统 Union FS。联合文件系统,可以将几层目录挂载到 一起,形成一个虚拟文件系统。虚拟文件系统的目录结构就像普通 linux 的目录结构一 样,docker 通过这些文件再加上宿主机的内核提供了一个 linux 的虚拟环境。

每一层文件系统我们叫做一层 layer,联合文件系统可以对每一层文件系统设置三 种权限,只读(readonly)、读写(readwrite)和写出(whiteout-able),但是 docker 镜像中每一层文件系统都是只读的。

构建镜像的时候,从一个最基本的操作系统开始,每个构建的操作都相当于做一层 的修改,增加了一层文件系统。一层层往上叠加,上层的修改会覆盖底层该位置的可 见性,这也很容易理解,就像上层把底层遮住了一样。当你使用的时候,你只会看到 一个完全的整体,你不知道里面有几层,也不清楚每一层所做的修改是什么。

可以看到镜像分层结构有以下特性

(1)镜像共享宿主机的 kernel

(2)base 镜像是 linux 的最小发行版

(3)同一个 docker 主机支持不同的 Linux 发行版

(4)采用分层结构,可以上层引用下层,最大化的共享资源

(5)容器层位于可写层,采用 cow 技术进行修改,该层仅仅保持变化的部分,并不 修改镜像下面的部分

(6)容器层以下都是只读层

(7)docker 从上到下找文件

镜像实现原理

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)"**机制

图中的A表示添加,D表示删除,C表示修改

如果一个文件在最底层是可见的,如果在 layer1 上标记为删除,最高的层是用户看到 的 Layer2 的层,在 layer0 上的文件,在 layer2 上可以删除,但是只是标记删除,用 户是不可见的,总之在到达最顶层之前,把它标记来删除,对于最上层的用户是不可 见的,当标记一删除,只有用户在最上层建一个同名一样的文件,才是可见的。

3、overlay2****实现

(1)架构

OverlayFS 将单个 Linux 主机上的两个目录合并成一个目录。这些目录被称为层,统 一过程被称为联合挂载 OverlayFS 底层目录称为 lowerdir, 高层目录称为 upperdir,合并统一视图称为 merged

图中可以看到三个层结构,即 lowerdir、upperdir、merged 层

(2)分层

• lowerdir 层

其中 lowerdir 是只读的镜像层(image layer),其中就包含 bootfs/rootfs 层, bootfs(boot file system)主要包含 bootloader 和 kernel,bootloader 主要是引导加载 kernel,当 boot 成功 kernel 被加载到内存中,bootfs 就被 umount 了,rootfs(root file system)包含的就是典型 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 层中的。

(3)读写过程

①读

如果文件在 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镜像加载原理

boots(boot file system)主要包含 bootloader 和 Kernel, bootloader 主要是引导加 kernel,Linux 刚启动时会加 bootfs 文件系统,在 Docker 镜像的最底层是 bootfs。这一 层与我们典型的 Linux/Unix 系统是一样的,包含 boot 加載器和内核。当 boot 加载完 成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系 统也会卸载 bootfs。

rootfs(root file system),在 bootfs 之上。包含的就是典型 Linux 系统中的 /dev,/proc,/bin,/etc 等标准目录和文件。 rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,Centos 等等。 典型的 Linux 在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换 为 "readwrite" 供用户使用。在 docker 中,起初也是将 rootfs 以 readonly 方式加载并 检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的 rootfs 之上,并且允许再次将下层的 file system 设定为 readonly 并且向上叠加, 这样 一组 readonly 和一个 writeable 的结构构成一个 container 的运行目录, 每一个被称作 一个 Layer

下面的这张图片非常好的展示了组装的过程,每一个镜像层都是建立在另一个镜像层 之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户 直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括命名空间、控制组、 rootfs 等等,这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也 能够减少磁盘的占用。

操作

镜像分层存储

tree命令

功能

Linux tree 命令用于以树状图列出目录的内容。 执行 tree 指令,它会列出指定目录下的所有文件,包括子目录里的文件。

语法

tree [-aACdDfFgilnNpqstux][-I <范本样式>][-P <范本样式>][目录...]

参数

• -a 显示所有文件和目录。

• -d 显示目录名称而非内容。

• -D 列出文件或目录的更改时间。

• -f 在每个文件或目录之前,显示完整的相对路径名称。

• -i 不以阶梯状列出文件或目录名称。

• -L level 限制目录显示层级。

• -l 如遇到性质为符号连接的目录,直接列出该连接所指向的原始目录。

• -P<范本样式> 只显示符合范本样式的文件或目录名称。

1、拉取nginx镜像

执行命令 docker pull nginx:1.21.1

2、查看Dockerfile和镜像分层的关系

执行命令 docker image history nginx:1.21.1

可以看到并不是所有层都占用大小,执行的语句都是Dockerfile文件的命令

3、查看overlay2存储信息

查找的存储位置,执行命令,查看镜像详情,执行命令 docker image inspect nginx:1.21.1

4、查找nginx文件,确定镜像文件存储原理

进入这个目录下镜像查找

找nginx的启动文件,执行命令 /var/lib/docker/overlay2# tree -P nginx -f | grep "sbin/nginx"

找nginx的docker-entrypoint.sh文件,执行命令 tree -P "docker-entrypoint.sh" -f | grep "docker-entrypoint.sh"

再查看文件目录所在位置

然后在镜像层目录下的一个目录找到该文件所在目录。

然后再查另外一个脚本文件的目录,同理查找。也会在镜像层目录下找到

查看镜像层该目录文件,执行命令 ls /var/lib/docker/overlay2/f534fcb81c20fd44422b55daa96a1cf15974fe35c745a7aa40395cfab25652b8/diff/usr/sbin/

LowerDir就是镜像层,LowerDir有很多目录,每个目录都是一个镜像层,文件就放在这些镜像层中。这样就完成了镜像文件的存储打包。

这里的层就是Dockerfile执行的命令,但也不是每句Dockerfile命令都会生成一个镜像层。

查看UpperDir,执行命令 ls /var/lib/docker/overlay2/ff759e342500f9142b10cf34f965cb5685ee9ba6aa4aaf60f0cc406712493289/diff/docker-entrypoint.d/

UpperDir就是读写层,对应Dockerfile文件,就是最后层放入的文件

查看MergedDir,执行命令 ls /var/lib/docker/overlay2/ff759e342500f9142b10cf34f965cb5685ee9ba6aa4aaf60f0cc406712493289/merged

MergedDir是镜像启动为容器才会有。是容器层面才具有的

现在启动该镜像为一个容器,再查看该目录

启动镜像,执行命令 docker run --rm -d --name mynginx nginx:1.21.1

查看容器详情的MergedDir字段,执行命令 docker container inspect mynginx

5、配合分析镜像、容器分层原理

进入diff的上一级目录,执行命令 cd /var/lib/docker/overlay2/ff759e342500f9142b10cf34f965cb5685ee9ba6aa4aaf60f0cc406712493289

查看link的值

link的值是一个软连接

这个软链接指向自己diff所在目录,

diff目录存着该层具体的文件内容

并且,在上级目录下的 l 目录都是软链接

查看lower值

这里存着多个软连接,表示着层级关系。存的是父层目录关系

可以将查看镜像详情的LowerDir字段,打印每个目录下的lower的值。

祖先列表从左到右 = 从底到顶 ,最右边那一项就是我的直接父亲

lower的值是多个软连接

这些软链接表示这层的父级目录关系。
也就是说diff是存的每个层的具体文件信息,link值是执行当前层的信息,而lower值是表示当前层的父辈关系。就能实现不同层之间的层级关系了。

overlay文件系统

1、准备目录和测试文件

准备目录fs,并进入

然后再新建4个目录,执行命令 mkdir upper lower merged work

为每层写入文件

分别执行命命令 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

2、完成挂载

执行命令 mount -t overlay overlay -o lowerdir=./lower,upperdir=./upper,workdir=./work ./merged

3、查看文件

4、编辑文件

对merged下的in_lower.txt进行修改,同步到upper下的in_lower.txt中,而lower下的in_lower.tx并未变化,是因为upper目录下的in_lower.txt内容是对lower下in_lower.txt的COW(COPY ON WRITE)

5、删除文件

删除merged下的in_lower.txt文件

存储卷原理

卷机制

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 打破。

操作

Linux mount bind

功能

通过 mount --bind 命令来将两个目录连接起来,mount --bind 命令是将前一个目录挂 载到后一个目录上,所有对后一个目录的访问其实都是对前一个目录的访问。可以理 解为硬链接。

语法

mount --bind src dest

1、准备目录和文件

创建目录,执行命令 mkdir data1 data2

向data1中写入一个文件,执行命令 echo "hello docker!" > data1/testfile.txt

2、创建绑定挂载

将两个目录进行绑定,执行命令 mount --bind data1/ data2/

3、查看绑定效果

可以看到data2中也出现了testfile.txt文件,内容也是和hello docker。

单后对data1中的文件进行编辑,内容也同步到了data2中的文件了。

如果需要解除绑定

语法

umount dst

这里就是执行 umount data2

查看docker联合挂载

1、启动一个容器

执行命令 docker run -d --name mynginx1 nginx:1.21.1

2、查看容器的overlay2信息

查看容器详情中的GraphDriver字段,执行命令 docker container inspect mynginx

3、查看该容器的挂载点

输入命令 df -h

将挂载点目录和容器详情中的GraphDriver字段下的MergedDir目录进行对比,发现完全一样

通过mount命令查看overlay挂载信息也是ok的,执行命令 mount | grep overlay

docker commit能提交卷里面的内容么

容器的镜像操作,比如 docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来, 容器中可读写层的 /test 目录(/var/lib/docker/overlay2/{layerid}/[可读写层 ID]/test), 始终是空的。在宿主机的眼里 /test 的 inode 就是原本没有重定向的 inode,你所有的 修改都不在这个文件下。

不过,由于 Docker 一开始还是要创建 /test 这个目录作为挂载点,所以执行了 docker commit 之后,你会发现新产生的镜像里,会多出来一个空的 /test 目录。

1、创建容器绑定存储卷

执行命令 docker run --rm -d --name mynginx -p 8081:80 -v test_volume:/test nginx:1.21.1

2、存储卷写入内容

进入容器,在test目录下写入内容

3、提交新镜像

执行命令 docker commit mynginx mynewnginx:v1

4、使用新镜像创建容器,检查是否有内容

执行命令 docker run -it mynewnginx:v1 ls -l /test

docker网络原理

什么是虚拟网卡

虚拟网卡(又称虚拟网络适配器),即用软件模拟网络环境,模拟网络适配器。简单来 说就是软件模拟的网卡。

虚拟网卡**:tun/tap**

简介

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

操作

基础命令

bash 复制代码
1、添加网卡
# 创建 tap 
ip tuntap add dev tap0 mode tap 
# 创建 tun
ip tuntap add dev tun0 mode tun
2、删除网卡
# 删除 tap
ip tuntap del dev tap0 mode tap
# 删除 tun
ip tuntap del dev tun0 mode tun
3、激活网卡
ip link set tun0 up
4、设置ip
ip addr add 10.5.0.1/24 dev tun0
5、查看帮助
ip tuntap help

1、添加tun设备和查看

执行命令 ip tuntap add dev tun1 mode tun

通过ifconfig -a也能查看到网卡tun1

2、激活设备

执行命令 ip link set tun1 up

然后输入ifconfig 就能直接看到网卡 tun1的信息。前面需要加 -a是因为没有被激活

3、查看设备信息,配置ip

执行命令 ip addr add 10.100.100.10/24 dev tun1

然后再查看ip信息

4、测试ip是否连通

执行命令 ping 10.100.100.10

5、删除设备

执行命令 ip tuntap del dev tun1 mode tun

虚拟网卡**:veth**

简介

在 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 也提供 了网桥的虚拟实现。

操作

常见命令

bash 复制代码
1、veth操作

# 添加 veth
ip link add <veth name> type veth peer name <peer name>
# 删除 veth
ip link delete <veth name>
# 查看 veth
ip link show

2、命名空间操作
#添加 ns
ip netns add <name>
#删除 ns
ip netns del <name>
#执行命令
ip netns exec <name> <cmd>
#遍历 ns
ip netns list

1、创建和查看命令空间

分别执行命令 ip netns add ns1 ip netns add ns2

然后查看,执行命令 ip netns list

2、创建和查看veth

执行命令 ip link add veth11 type veth peer name veth12

然后查看,执行命令 ip link show

3、veth设置到不同的命名空间

分别执行命令 ip link set veth11 netns ns1 ip link set veth12 netns ns2

然后查看ns1空间的veth,执行命令 ip netns exec ns1 ip link show

查看ns2空间的veth,执行命令 ip netns exec ns2 ip link show

4、激活网卡和分配ip

激活网卡,分别执行命令 ip netns exec ns1 ip link set veth11 up ip netns exec ns2 ip link set veth12 up

然后查看ns1和ns2网络空间的veth,执行命令 ip netns exec ns1 ifconfig ip netns exec ns2 ifconfig

分配ip,分别执行命令 ip netns exec ns1 ip addr add 10.5.0.1/24 dev veth11 ip netns exec ns2 ip addr add 10.5.0.2/24 dev veth12

然后查看ns1和ns2网络空间的veth,执行命令 ip netns exec ns1 ifconfig ip netns exec ns2 ifconfig

5、在ns1中使用veth11 ping ns2的veth12

执行命令 ip netns exec ns1 ping 10.5.0.2

虚拟交换机

简介

使用 veth pair 将两个隔离的 netns 连接在了一起,在现实世界里等同于用一根网线把两 台电脑连接在了一起,但是在现实世界里往往很少会有人这样使用。因为一台设备不 仅仅只需要和另一台设备通信,它需要和很多很多的网络设备进行通信,如果还使用 这样的方式,需要十分复杂的网络接线,并且现实世界中的普通网络设备也没有那么 多网络接口。

那么,想要让某一台设备和很多网络设备都可以通信需要如何去做呢?在我们的日常 生活中,除了手机和电脑,最常见的网络设备就是路由器了,我们的手机连上 WI-FI, 电脑插到路由器上,等待从路由器的 DHCP 服务器上获取到 IP,他们就可以相互通信 了,这便是路由器的二层交换功能在工作。Linux Bridge 最主要的功能就是二层交换, 是对现实世界二层交换机的模拟.

Linux Bridge ,由 brctl 命令创建和管理。Linux Bridge 创建以后,真实的物理设备 (如 eth0)抑或是虚拟的设备(veth 或者 tap)都能与 Linux Bridge 配合工作。

有了虚拟化网络设备后,下一步就是要使用这些设备组成网络。

操作

linux 提供了 brctl 工具来管理和查看网桥

bash 复制代码
1. 安装方式
# centos
yum install -y bridge-utils
# ubuntu
apt-get install -y bridge-utils

2. 新建一个网桥
brctl addbr <bridge>

3. 添加一个设备(例如 eth0)到网桥
brctl addif <bridge> eth0

4. 显示当前存在的网桥及其所连接的网络端口
brctl show

5. 启动网桥
ip link set <bridge> up

6. 删除网桥,需要先关闭它
ip link set <bridge> down
brctl delbr <bridge>
或者使用 ip link del 命令直接删除网桥
ip link del <bridge>

创建三个netns,三对veth pair,分别一端在netns中,另一端连接在网桥上。

拓扑如下

1、创建和查看网络命名空间

创建网络空间,分别执行命令 ip netns add ns1 ip netns add ns2 ip netns add ns3

然后查看,执行命令 ip netns list

2、创建和查看veth

创建veth,分别执行命令 ip link add veth1-ns type veth peer name veth1-br ip link add veth2-ns type veth peer name veth2-br ip link add veth3-ns type veth peer name veth3-br

查看veth,执行命令 ip link list

3、veth设置到不同的网络命名空间

分别执行命令 ip link set dev veth1-ns netns ns1 ip link set dev veth2-ns netns ns2 ip link set dev veth3-ns netns ns3

然后查看ns1空间的veth,执行命令 ip netns exec ns1 ip link show

然后查看ns2空间的veth,执行命令 ip netns exec ns2 ip link show

然后查看ns3空间的veth,执行命令 ip netns exec ns3 ip link show

4、激活网卡和分配ip

激活网卡,分别执行命令 ip netns exec ns1 ip link set dev veth1-ns up ip netns exec ns2 ip link set dev veth2-ns up ip netns exec ns3 ip link set dev veth3-ns up

然后查看ns1、ns2和ns3网络空间的veth,执行命令 ip netns exec ns1 ifconfig ip netns exec ns2 ifconfig ip netns exec ns3 ifconfig

分配ip,分别执行命令 ip netns exec ns1 ip addr add 10.100.0.11/24 dev veth1-ns ip netns exec ns2 ip addr add 10.100.0.12/24 dev veth2-ns ip netns exec ns3 ip addr add 10.100.0.13/24 dev veth3-ns

5、测试网络连通性

在ns3中ping 自己,执行命令 ip netns exec ns3 ping 10.100.13

然后依次对ns1和ns2中的回环网络进行激活,分别执行命令 ip netns exec ns2 ip link set dev lo up ip netns exec ns2 ip link set dev lo up

然后在ns3中ping ns1的网络空间,执行命令 ip netns exec ns3 ping 10.100.11

因为网络是隔离的

6、创建和启动网桥

创建网桥,执行命令 brctl addbr testbr0

然后启动,执行命令 ip link set dev testbr0 up

然后查看网桥,执行命令 brctl show

7、网桥配置ip

执行命令 ip addr add 10.100.0.1/24 dev testbr0

然后查看网桥,执行命令 ifconfig

8、网卡插入网桥,测试连通性

给网桥添加网卡,分别执行命令 brctl addif testbr0 veth1-br brctl addif testbr0 veth2-br brctl addif testbr0 veth3-br

再激活插入的网卡,分别执行命令 ip link set dev veth1-br up ip link set dev veth2-br up ip link set dev veth3-br up

然后在ns3中ping ns1的网络空间,执行命令 ip netns exec ns3 ping 10.100.11

9、资源清理

ip link del veth1-br

ip link del veth2-br

ip link del veth3-br

ip link del testbr0

ip netns del ns1

ip netns del ns2

ip netns del ns3

虚拟组网****VxLan

物理网络的拓扑结构是相对固定的。云原生时代的分布式系统的逻辑拓扑结构变动频 率,譬如服务的扩缩、断路、限流,等等,都可能要求网络跟随做出相应的变化。

正因如此,软件定义网络(Software Defined Network,SDN)的需求在云计算和分布 式时代变得前所未有地迫切,SDN 的核心思路是在物理的网络之上再构造一层虚拟化 的网络。

SDN 里位于下层的物理网络被称为 Underlay,它着重解决网络的连通性与可管理性, 位于上层的逻辑网络被称为 Overlay,它着重为应用提供与软件需求相符的传输服务和 网络拓扑。

vlan

交换机是一个 L2 设备,插在同一个交换机的网络设备组成了一个 L2 网络,L2 网络之 间通过 MAC 地址通信,同时这个 L2 网络也是一个广播域。

同属于一个广播域的两个设备想要通信,一设备须得向网络中的所有设备发送请求信 息,只有对应 MAC 地址的设备才是真正的接收方,但实际上却是数据帧传遍整个网络, 所有设备都会收到,且直接丢弃。 如此一来,将造成一系列不好的后果:网络整体带宽被占用、潜在的信息安全风险、 占用 CPU 资源......

因此,VLAN 应运而生!

Vlan(Virtual Local Area Network)即虚拟局域网,是一个将物理局域网在逻辑上划分成 多个广播域技术。通过在交换机上配置 Vlan,可以实现在同一 Vlan 用户可以进行二层 互访,在不同 Vlan 间的用户被二层隔离,这样既能够隔离广播域,又可以提升网络安 全性。

VLAN 究竟能够解决什么问题?

1、限制广播域。广播域被限制在一个局域网内,节省了带宽,提高了网络处理能力。

2、增强局域网的安全性。不同局域网内的报文在传输时是相互隔离的,即一个 VLAN 内的用户不能和其它 VLAN 内的用户直接通信,如果不同 VLAN 要进行通信,则需要 通过路由器或三层交换机等三层设备。

3、灵活构建虚拟工作组。用局域网可以划分不同的用户到不同的工作组,同一工作组 的用户也不必局限于某一固定的物理范围,网络构建和维护更方便灵活。

不过 VLAN 也并非没有缺点。

1、随着虚拟化技术的发展,一台物理服务器往往承载了多台虚拟机,公有云或其它大 型虚拟化云数据中心动辄需容纳上万甚至更多租户,VLAN 技术最多支持 4000 多个 VLAN,逐渐无法满足需求。

2、公有云提供商的业务要求将实体网络租借给多个不同的用户,这些用户对于网络的要求有所不同,而不同用户租借的网络有很大的可能会出现 IP 地址、MAC 地址的重 叠。传统的 VLAN 并没有涉及这个问题,因此需要一种新的技术来保证在多个租户网 络中存在地址重叠的情况下依旧能有效通信的技术。

3、虚拟化技术使得单台主机可以虚拟化出多台虚拟机同时运行,而每台虚拟机都会有 其唯一的 MAC 地址。这样,为了保证集群中所有虚机可以正常通信,交换机必须保存 每台虚机的 MAC 地址,这样就导致了交换机中的 MAC 表异常庞大,从而影响交换机 的转发性能。

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 广播域。

MacVLan

MACVLAN 允许对同一个网卡设置多个 IP 地址,还允许对同一张网卡上设置多个 MAC 地址,这也是 MACVLAN 名字的由来。原本 MAC 地址是网卡接口的"身份证", 应该是严格的一对一关系,而 MACVLAN 打破这层关系,方法是在物理设备之上、网 络栈之下生成多个虚拟的 Device,每个 Device 都有一个 MAC 地址,新增 Device 的 操作本质上相当于在系统内核中注册了一个收发特定数据包的回调函数,每个回调函 数都能对一个 MAC 地址的数据包进行响应,当物理设备收到数据包时,会先根据 MAC 地址进行一次判断,确定交给哪个 Device 来处理。

许多网卡在硬件层面对支持的 MAC 地址数量存在限制。超过该限制会导致性能的下降。 所以 macvlan 虽然近乎完美还是没有实际推广完成。

IPVLan

Ipvlan 与 macvlan 非常相似,但又存在显著不同。Ipvlan 的子接口上并不拥有独立的 MAC 地址。所有共享父接口 MAC 地址的子接口拥有各自独立的 IP。

共享 MAC 地址会影响 DHCP 相关的操作。如果虚拟机、容器需通过 DHCP 获取网络 配置,请确保它们在 DHCP 请求中使用各自独立的 ClientID;DHCP 服务器会根据请 求中的 ClientID 而非 MAC 地址来分配 IP 地址。某些设备不支持分配多个 ip 地址所以 IPVlan 也没有实际大规模推广。

深入理解docker bridge网络

docker网络分类

bridge网络

bridge 驱动会在 Docker 管理的主机上创建一个 Linux 网桥。默认情况下,网桥上 的容器可以相互通信。也可以通过 bridge 驱动程序配置,实现对外部容器的访问。桥 接网络如下

创建命令如下:docker network create -d bridge bridgenet1

host 网络

如果启动容器的时候使用 host 模式,那么这个容器将不会获得一个独立的 Network Namespace,而是和宿主机共用一个 Network Namespace。容器将不会虚 拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。但是,容器的其 他方面,如文件系统、进程列表等还是和宿主机隔离的。host 网络结构如下:

host 网络创建命令如下:docker run --name c2 -itd --network=host busybox

container 网络

这个模式指定新创建的容器和引进存在的一个容器共享一个 network namespace , 而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 ip,而是和一 个指定的容器共享 ip,端口等,两个容器除了网络方面,其他的如文件系统、进程列 表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。容器网络原理如下:

容器网络创建命令参考如下:

docker run -itd --name netcontainer2 --network

container:netcontainer1 busybox

none 网络

Docker 容器拥有自己的 Network Namespace,但是,并不为 Docker 容器进行任 何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己 为 Docker 容器添加网卡、配置 IP 等。

docker run -itd --name c3 --network none busybox

overlay 网络

Overlay 驱动创建一个支持多主机网络的覆盖网络。

在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装 在 IP 报文之上的新的数据格式。

Overlay 网络实际上是目前最主流的容器跨节点数据传输和路由方案,底层原理就是使用VXLAN.

Overlay 网络将多个 Docker 守护进程连接在一起,允许不同机器上相互通讯,同 时支持对消息进行加密,实现跨主机的 docker 容器之间的通信,Overlay 网络将多个 Docker 守护进程连接在一起,使 swarm 服务能够相互通信。这种策略消除了在这些 容器之间进行操作系统级路由的需要。下图是个典型的 overlay 网络

overlay 网络的创建命令如下

docker network create -d overlay ovnet1

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

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 bridgeveth pair****如何对应

1、运行busybox容器

执行命令 docker run -itd --name b1 busybox:1.36

2、进入容器查看veth信息

执行命令 exec -it b1 sh

再推出容器,查看宿主机网卡信息,执行命令 ip link

宿主机上表示855和2配对,这就知道了容器内的网卡和宿主机的网卡的配对了。

再查看宿主机docker0网桥上的网卡,执行命令 brctl show

可以看到宿主机上的这块网卡是属于docker0网桥的

容器与宿主机**/**外界主机通信原理

Bridge 桥接模式,从原理上实现了 Docker Container 到宿主机乃至其他机器的网络连 通性。然而,由于宿主机的 IP 地址与 veth pair 的 IP 地址均不在同一个网段,故仅仅 依靠 veth pair 和 namespace 的技术,还不足以是宿主机以外的网络主动发现 Docker Container 的存在。为了使得 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 进行"绑定"。

如此一来,外界访问 Docker Container 内部服务的流程为:

  1. 外界访问宿主机的 IP 以及宿主机的端口 port_1;

  2. 当宿主机接收到这样的请求之后,由于 DNAT 规则的存在,会将该请求的目的 IP (宿主机 eth0 的 IP)和目的端口 port_1 进行转换,转换为容器 IP 和容器的端口 port_0;

  3. 由于宿主机认识容器 IP,故可以将请求发送给 veth pair;

  4. veth pair 的 veth0 将请求发送至容器内部的 eth0,最终交给内部服务进行处理。 使用 DNAT 方法,可以使得 Docker 宿主机以外的世界主动访问 Docker Container 内 部服务。

那么 Docker Container 如何访问宿主机以外的世界呢。 以下简要分析 Docker Container 访问宿主机以外世界的流程:

  1. Docker Container 内部进程获悉宿主机以外服务的 IP 地址和端口 port_2,于是 Docker Container 发起请求。容器的独立网络环境保证了请求中报文的源 IP 地址为容 器 IP(即容器内部 eth0),另外 Linux 内核会自动为进程分配一个可用源端口(假设 为 port_3);

  2. 请求通过容器内部 eth0 发送至 veth pair 的另一端,到达 veth0,也就是到达了网 桥(docker0)处;

  3. docker0 网桥开启了数据报转发功能(/proc/sys/net/ipv4/ip_forward),故将请求 发送至宿主机的 eth0 处;

  4. 宿主机处理请求时,使用 SNAT 对请求进行源地址 IP 转换,即将请求中源地址 IP (容器 IP 地址)转换为宿主机 eth0 的 IP 地址;

  5. 宿主机将经过 SNAT 转换后的报文通过请求的目的 IP 地址(宿主机以外世界的 IP 地址)发送至外界。 在这里,很多人肯定会问:对于 Docker Container 内部主动发起对外的网络请求,当 请求到达宿主机进行 SNAT 处理后发给外界,当外界响应请求时,响应报文中的目的 IP 地址肯定是 Docker 宿主机的 IP 地址,那响应报文回到宿主机的时候,宿主机又是 如何转给 Docker Container 的呢?关于这样的响应,由于 port_3 端口并没有在宿主机 上做相应的 DNAT 转换,原则上不会被发送至容器内部。为什么说对于这样的响应, 不会做 DNAT 转换呢。原因很简单,DNAT 转换是针对容器内部服务监听的特定端口 做的,该端口是供服务监听使用,而容器内部发起的请求报文中,源端口号肯定不会 占用服务监听的端口,故容器内部发起请求的响应不会在宿主机上经过 DNAT 处理。

其实,这一环节的内容是由 iptables 规则来完成,具体的 iptables 规则如下:

iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

这条规则的意思是,在宿主机上发往 docker0 网桥的网络数据报文,如果是该数据报 文所处的连接已经建立的话,则无条件接受,并由 Linux 内核将其发送到原来的连接 上,即回到 Docker Container 内部。 IP-ForwardingIP 转发。 一种路由协议。IP 转发是操作系统的一种选项,支持主机 起到路由器的功能。在一个系统中含有两块以上的网卡,并将 IP 转发选项打开,这样 该系统就可以作为路由器进行使用了。

iptables 组成 Linux 平台下的包过滤防火墙,与大多数的 Linux 软件一样,这个包过 滤防火墙是免费的,它可以代替昂贵的商业防火墙解决方案,完成封包过滤、封包重 定向和网络地址转换等功能。

参数如下

• -t 指定表名,在 iptables 中,有四张表:filter:这里面的链条,规则,可以决定 一个数据包是否可以到达目标进程端口;mangle: 这里面的链条,规则,可以修改数 据包的内容,比如 ttl;nat:这里面的链条,规则,可以修改源和目标的 ip 地址,从而 进行包路由;raw:这里面的链条,规则,能基于数据包的状态进行规则设定

• -n 使用数字形式(numeric)显示输出结果

• -v 查看规则表详细信息(verbose)的信息

• -L 列出(list)指定链中所有的规则进行查看

MASQUERADE规则是一个特殊的SNAT规则,默认的SNAT规则是需要给一个静态ip地址,而MASQUERADE规则可以支持动态ip地址,可以做到ip自动识别。

执行命令 iptables -t nat -nvL

这是一条外部网络访问容器的DNAT转换规则

target DNAT 动作是 DNAT(Destination NAT,目的地址转换)。

prot 6 协议号 6 → TCP(6 就是 TCP 的协议号)。

in !docker0 入站接口 不能是 docker0(即"非 docker0 接口",就是指明是外部网络)。

说明这条规则只对 从主机其他网卡进来 的流量生效,避免把 docker0 自己内部的流量再 NAT 一次。

out * 表示出站接口不限。

source 0.0.0.0/0 表示源地址不限。

destination 0.0.0.0/0 表示目的地址不限(实际由后面 dpt 限定)。

tcp dpt:8081 to:172.17.0.2:80 表示匹配 TCP 目的端口 8081 的流量,把它的 目的地址/端口 改成 172.17.0.2:80。

任何从主机外部(非 docker0)访问本机 8081/TCP 的流量,都会被重定向到 docker 容器 172.17.0.2 的 80 端口------典型的"把容器端口映射到宿主机"场景。

这是一条容器访问外部网络的SNAT转换规则

target MASQUERADE 动作为 MASQUERADE,一种动态 SNAT,把包的 源地址 改成 出接口的当前 IP。

prot 0 协议号 0 表示"任意协议"(tcp/udp/icmp 都匹配)。

in * / out !docker0 出站接口 不能是 docker0(即"离开主机的真实网卡"时才生效,也就是访问外部网络)。避免把 docker 内部流量又伪装一次。

source 172.17.0.0/16 只对 docker0 网段(默认 172.17.0.0/16)发出的包生效。

destination 0.0.0.0/0 目的地址不限。

当容器(172.17.0.0/16)访问外部网络时,在数据包离开主机的真实网卡前,把它的源地址自动改成主机那块网卡的 IP,实现"容器共享宿主机 IP 上网"。

容器的网络命名空间查找

nsenter命令

nsenter 命令是一个可以在指定进程的命名空间下运行指定程序的命令。它位于 util-linux 包中。一个最典型的用途就是进入容器的网络命令空间。相当多的容器为了轻量 级,是不包含较为基础的命令的,比如说 ip address,ping,telnet,ss,tcpdump 等 等命令,这就给调试容器网络带来相当大的困扰:只能通过 docker inspect

ContainerID 命令获取到容器 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 | 版本查看 |

docker 使用 namespace 实现容器网络,但是使用 ip netns 命令却无法在主机上看到 任何 docker 容器的 network namespace,这是因为默认 docker 把创建的网络命名空 间链接文件隐藏起来了。 ip netns 默认是去检查/var/run/netns 目录的。而 Docker 容器对应的 ns 信息记录到了 /var/run/docker/netns 目录。所以 ip netns 查出来就是空的

1、启动一个容器

执行命令 docker run -itd --name b2 busybox:1.36

2、查看容器详情的SandboxKey字段

执行命令 docker container inspect b2

这就是该容器下的网络命名空间

3、进入该网络命名空间

执行命令 nsenter --net=/var/run/docker/netns/57b7bd27a5ab

找到该命名空间的ip

4、进入容器,查看ip信息

执行命令 docker exec -it b2 sh

由此可以确定该网络命名空间就是和容器的网络命名空间,就可以在宿主机上对容器的网络环境进行调试。

相关推荐
一只废狗狗狗狗狗狗狗狗狗3 小时前
基于docker desktop的hadoop集群结点启动失败问题
hadoop·docker·docker desktop
九思x3 小时前
Linux 系统安装 JDK 17
linux·运维
HIT_Weston4 小时前
77、【Ubuntu】【Hugo】搭建私人博客:Detached HEAD
linux·运维·ubuntu
再睡一夏就好4 小时前
多线程并发编程核心:互斥与同步的深度解析及生产者消费者模型两种实现
linux·运维·服务器·jvm·c++·笔记
木童6624 小时前
Ruo-Yi 项目 CICD 完整部署文档(含命令详解)
ci/cd·docker·容器
码农胖虎-java5 小时前
技术深析:Delayed ACK与Nagle算法的“相爱相杀”
运维·服务器·网络
漂视数字孪生世界5 小时前
项目案例|某水轮机数字孪生平台
运维·信息可视化·自动化·数字孪生·三维可视化
幺零九零零6 小时前
Docker底层- 命令详解
运维·docker·容器
爱喝水的鱼丶6 小时前
SAP-ABAP:在SAP世界里与特殊字符“斗智斗勇”:一份来自实战的避坑指南
运维·服务器·数据库·学习·sap·abap·特殊字符