docker容器底层原理详解

230 阅读14分钟

容器原理概述

rootfs做文件系统,namespace做隔离,cgroup做资源限制,切换进程的根目录

rootfs(根文件系统)

对于操作系统,其rootfs包含了操作系统运行需要的文件、配置和目录,并不包括操作系统内核。

一个容器运行后的根目录就是它的rootfs,这个根文件系统会包含和正常宿主机一样的一些文件,例如/bin/etc/等。在容器中执行的/bin/bash就是容器的rootfs的/bin/bash,它和宿主机的/bin/bash是没有任何关联的。

在linux操作系统中,内核和rootfs是分开存放的,操作系统只有在开机启动的时候才会加载指定版本的内核镜像

Namespace

docker借助linux的namespace机制对资源进行隔离,借助cgroup进一步对cpu、内存等资源进行限制

linux中的namespace的主要作用是做了一层资源的隔离,使得在namespace中的进程从外观上看拥有自己独立的资源。

chroot是把某个目录修改成为根目录。namespace在此基础上提供了对uts、ipc、mount、pid、network、user等的隔离机制。

linux管理namespace的API

clone():    根据传递特定的标志flag创建对应的新的namespace并将子进程添加为其成员,flag值如下表格
setns():    将进程加入一个已存在的namespace中。
unshare():  将进程移动至一个新的namespace中。
分类系统调用参数功能
Mount NamespacesCLONE_NEWNS隔离进程的挂载点视图
UTS NamespacesCLONE_NEWUTS隔离nodename/domainname,每个namespace允许有自己的hostname
IPC NamespacesCLONE_NEWIPC隔离IPC进程间通信
PID NamespacesCLONE_NEWPID隔离进程PID,容器内进程都以1开始编号
Network NamespacesCLONE_NEWNET隔离网络设备、ip、端口
User NamespacesCLONE_NEWUSER隔离用户组id,即进程的use id和group id

mount namespace默认继承宿主机的文件系统,必须执行mount操作之后,才能看到不同的文件系统

unshare隔离进程实验

pid namespace是用来隔离进程,在不同的pid namespace中进程可以拥有相同的pid号。使用pid namespace可以让每个容器中的主进程为1号进程,而容器中的进程在宿主机上拥有不同的pid。

# 下面命令是创建一个bash进程,并新建一个pid namespace
unshare --pid --fork --mount-proc /bin/bash
​
--mount-proc参数说明
Just  before running the program, mount the proc filesystem at mountpoint (default is /proc).  This is useful when creating a new pid namespace.  It also implies creating a new mount names‐pace since the /proc mount would otherwise mess up existing programs on the system.  The new proc filesystem is explicitly mounted as private (by MS_PRIVATE|MS_REC).

再输入命令ps -ef查看系统上的运行的所有进程,发现这个namespace的进程从1开始。

再新开一个窗口执行同样操作,可以看到两个窗口的pid是不同的,即被pid namespace隔离开。

上面的命令中如果没有--mount-proc,则查看ls /proc和宿主机查看结果一样,如果有的话只显示新进程的/proc目录信息

CGroups

docker借助linux的namespace机制对资源进行隔离,借助cgroups进一步对cpu、内存等资源进行限制

CGroups主要包含三大组件,cgroup(注意是小写)提供进程分组管理的机制,subsystem资源控制模块,hierarchy(层次体系)提供cgroup的树状结构(通过这种树结构可以做到Cgroups的继承)。

一个subsystem只能附加到一个hierarchy上面;

一个hierarchy可以附加多个subsystem;

一个进程可以作为多个cgroup的成员,但是这些cgroup必须在不同的hierarchy中;

一个进程fork出子进程时,子进程和父进程在同一个cgroup中,可以根据需要将其移动到其他cgroup中

cgroups的kernel接口

第1步,先创建并挂在一个hierarchy(cgroup树)

mkdir cgroup-test
sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test

mount -t <type> <device> <mount point>

-t cgroup指定挂载的类型

-o none,name=cgroup-test 指定选项

cgroup-test即设备名称的位置这个字符串可以是任意值

./cgroup-test是挂载点

挂载之后系统在这个目录下生成一些默认文件,

$ ls
cgroup.clone_children  cgroup.sane_behavior  release_agent
cgroup.procs           notify_on_release     tasks

cgroup.clone_child: cpuset的subsystem会读取这个配置文件,默认值为0;是1的情况下子cgroup会继承父cgroup的cpuset的配置。

cgroup.procs: 树中当前节点cgroup中的进程组id,根节点的cgroup.procs值包含所有进程组的id。

release_agent是一个路径,用作进程退出后自动清理掉不再使用的cgroup

notify_on_release标识这个cgroup标识的进程组中最后一个进程退出后是否执行release_agent。

tasks标识cgroup节点包含的进程的id,就是设置这个值来将进程加入一个group中。

第2步,在树的根节点创建两个子cgroup,此时会自动装填cgroup节点的配置文件值

$ tree            
.
├── cgroup-1
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup-2
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks

第3步,在cgroup中添加和移动进程

一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在。

系统的所有进程都默认在根节点上存在,可以通过将进程id写到指定的cgroup节点的tasks文件中来将进程移动到其他cgroup节点

接下来将当前终端shell的进程加到cgroup-1中,先cd到cgroup-1这个目录下,然后命令sudo sh -c "echo $$ >> tasks",查看cat /proc/$$/cgroup,可以看到被加入到cgroup-1中。

cgroups限制cpu实验

通过subsystem限制cgroup中进程的cpu占用资源

CGroups的所有操作都是基于cgroup virtual filesystem,这个文件系统一般挂载再/sys/fs/cgroup目录下,通过ls查看目录信息。

cd /sys/fs/cgroup/cpu
mkdir cpu-test

创建文件夹A后,会自动在目录下面创建一些文件

# ls cpu-test
cgroup.clone_children  cpuacct.stat          cpu.cfs_period_us  cpu.rt_runtime_us  notify_on_release
cgroup.event_control   cpuacct.usage         cpu.cfs_quota_us   cpu.shares         tasks
cgroup.procs           cpuacct.usage_percpu  cpu.rt_period_us   cpu.stat

准备一个耗费cpu的程序

#include <stdio.h>
#include <stdbool.h>
int main(){
    int n = 0;
    while (true){
        n++;
        if (n > 1000000){
            n = 0;
        }
    }
    return 0;
}

后台启动后top命令,发现a进程基本占用100%的cpu

./a &

把进程号放到cgroup.procs文件中

cd cpu-test
sh -c "echo 4357 > cgroup.procs"

cgroup提供了cpu.cfs_quota_uscpu.cfs_period_us两个参数限制cpu占用上限。

cpu.cfs_quota_us表示运行周期,单位微妙,默认10^6us

cpu.cfs_period_us表示运行周期内这个cgroup组所有进程可运行的时间总总量,单位微妙,默认值-1,即不设置上限。

设置cpu.cfs_quota_us,这是top命令的结果就是a进程的cpu占用率低了下来

# 每十万个cpu时间里这个进程最多能用一万,预期CPU使用率降到10%
sh -c "echo 10000 > cpu.cfs_quota_us"# 恢复    
sh -c "echo -1 > cpu.cfs_quota_us"

docker使用cgroups限制容器

docker运行一个内存做出限制的容器

sudo docker run -itd -m 128m ubuntu

然后去cgroup查看这个容器的信息,可以看到docker对于内存的限制措施是在系统的memory下创建一个子cgroup为docker,然后再在docker这个节点下创建每个容器的节点,在这个容器节点下做出限制。

$ sudo sudo docker run -itd -m 128m ubuntu
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
7b1a6ab2e44d: Pull complete 
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:latest
e228cc516a83df78ce4e090a2ce2527c61cf35bdadcd9b6558fa96c00181da9c
$ sudo cat /sys/fs/cgroup/memory/docker/e228cc516a83df78ce4e090a2ce2527c61cf35bdadcd9b6558fa96c00181da9c/memory.limit_in_bytes 
134217728
$ sudo cat /sys/fs/cgroup/memory/docker/e228cc516a83df78ce4e090a2ce2527c61cf35bdadcd9b6558fa96c00181da9c/memory.usage_in_bytes 
1359872
$ sudo cat tasks
7094

然后再查看这个容器的pid,可以看到这个容器的进程号就时tasks文件中的进程号

sudo docker inspect <容器id>

AUFS

Union File System使用写时复制的机制使得对文件修改操作不会改变原来文件的内容。之后的更稳定可靠的版本称为AUFS。

写时复制:对于一个没有修改的资源,多个实例共享;如果有一个实例修改了这个资源,才会对该资源复制一份并进行修改。

aufs是重写早期的unionFS1.x版本得出的文件系统

联合挂载

有如下目录和文件结构

$ tree
.
├── A
│  ├── a
│  └── x
└── B
  ├── b
  └── x

使用联合挂载的方式,将这两个目录挂载到一个公共的目录C

$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C

查看目录C的内容,就能看到目录A和B下的文件被合并到了一起,在这个合并后的目录C里,有a、b、x三个文件,并且x文件只有一份。(虽然一般情况下不会将两个同名文件进行挂载)

$ tree ./C
./C
├── a
├── b
└── x

此时向C目录中的x文件中写内容会发现,A目录中的x文件内容改动,B目录中的x文件内容未改动

如果向C目录中的b文件写内容,会发现B目录中的b文件内容未改动,A目录下多出一个b文件

查看挂载信息,A目录为read-write,B目录为read-only,验证以上结果

$ sudo cat /sys/fs/aufs/si_bea80ffc240d08af/*
/home/aha/桌面/lab/A=rw
/home/aha/桌面/lab/B=ro
64
65
/home/aha/桌面/lab/A/.aufs.xino

这里可以将目录A理解为容器的可读写层,将目录B理解为镜像的只读层,将目录C理解为只读层和可读写层联合挂载后的容器目录

docker使用aufs原理

旧版docker使用aufs的cow技术来实现image layer共享和减少磁盘空间占用。旧版docker在pull一个镜像会下载多个rootfs的增量层,每一层都是镜像操作系统文件与目录的一部分。在使用镜像时,docker会把这些增量层联合挂在在一个统一的挂载点上/mnt/lib/docker/aufs/mnt/

容器信息的mount目录也是/var/lib/docker/aufs/mnt(也就是联合挂载后的目录),container的metadata和配置都存放在/var/lib/docker/containers/<container-id>目录中。这一部分是read-writeinit

可以通过cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*查看被联合挂载的各个层的信息,这些层来自于目录/var/lib/docker/aufs/diff/

所有层都放在/var/lib/docker/aufs/diff/

另外一个目录/var/lib/docker/aufs/layers存储着每个镜像的image layer如何堆栈这些layer的信息

由此可得出镜像的层的堆栈如下

可读写层rw
init层-ro+wh
只读层ro+wh

wh=whiteout

如果要删除一个文件file_x,则aufs机制会在容器的read-write层生成一个.wh.file_x文件来隐藏所有read-only层的file_x文件。

init层用来存放/etc/hosts等针对于当前容器的配置信息。因为这些修改往往只针对当前容器生效,这样在执行docker commit时只会体提交可读写层,不提交init层

相关概念

proc文件系统

proc文件系统是由内核提供的,并不真实存在于磁盘上。很多命令就是通过读取这个虚拟的文件系统来获取文件的信息。

/proc/n		pid为n的进程信息
/proc/n/cmdline		进程的启动命令
/proc/n/cwd		链接进程当前的工作目录
/proc/n/environ		进程环境变量列表
/proc/n/exe		链接到进程的执行命令文件
/proc/n/fd		进程相关的所有文件描述符
/proc/n/maps	与进程相关的内存映射信息
/proc/n/mem		进程持有的内存
/proc/n/root	进程能看到的根目录
/proc/n/stat	进程的状态
/proc/n/statm	进程占用的内存状态
/proc/n/status	详细进程状态
/proc/self	链接到当前正在运行的进程

chroot、pivot_root

chroot的作用是改变进程的根目录视图,使它不能访问该目录之外的其他文件。

chroot <newroot> [<command><arg>...]
newroot:	要切换到新的root目录
command:	切换root目录后要执行的命令
示例:	sudo chroot /home/jgc/lab/rootfs_lab /bin/bash

但是chroot只是改变当前进程的根目录视图。

pivot_root的功能和chroot类似,但是pivot_root把整个系统切换到一个新的根目录,这样就可以去掉对之前rootfs的依赖,即可以umount卸载挂载点了。

pivot_root实验的一个链接https://blog.csdn.net/m0_62969222/article/details/132039479

chroot实验

创建一个lab文件夹作为实验目录,在其中创建rootfs_lab作为要实验的根文件系统目录,再创建/bin目录,并把ls和bash两个二进制可执行文件拷贝过来。

mkdir lab
cd lab
mkdir rootfs_lab
cd rootfs_lab

创建bin目录,并把主要文件拷贝过来
mkdir bin
sudo cp /bin/ls ./bin/
sudo cp /bin/bash ./bin/

同时要把两个可执行文件依赖的动态库拷贝到相同的目录

查看ls和bash文件依赖的动态库文件
ldd /bin/bash
ldd /bin/ls

拷贝过来之后用tree命令查看,如下就是制作的将来要以rootfs_lab为根目录的目录结构。

$ tree ./lab
./lab
├── bin
│   ├── bash
│   └── ls
├── lib
│   ├── libc.so.6
│   ├── libdl.so.2
│   ├── libpcre2-8.so.0
│   ├── libpthread.so.0
│   ├── libselinux.so.1
│   └── libtinfo.so.6
└── lib64
    └── ld-linux-x86-64.so.2

使用chroot命令把根文件系统目录进行修改

chroot ./rootfs_lab /bin/bash

这样就把当前进程的根目录改成了/rootfs_lab,并且进入新的rootfs目录后执行了/bin/bash命令。这是这个进程cd /的时候就到了rootfs_lab这个根目录,并且cd ..也出不去。

busybox

busybox是一个集成了很多linux命令和工具的软件,适用于嵌入式设备、移动设备(安卓)、小型linux(alpine linux)。

基本使用如下,首先编译安装一个busybox

yum -y install wget make gcc perl glibc-static ncurses-devel libgcrypt-devel

wget http://busybox.net/downloads/busybox-1.21.0.tar.bz2

tar -jxvf busybox-1.21.0.tar.bz2

mv busybox-1.21.0 busybox

cd busybox

make menuconfig
把busybox编译成静态二进制、不用共享库
Settings --> Build Options --> [*] Build BusyBox as a static binary (no shared libs) 按Y选中

make

# make install	这里好像不需要安装,用编译好的_install目录就可以了

编译安装后会在当前顶层目录下生成一个_install目录,其中就包含了bin等目录,并把一些二进制程序装在里面。把它拷贝出来mv _install ../busybox2

接下来用chroot命令切换根文件系统chroot busybox2 /bin/bash

containerd

docker包含containerd,containerd专注于运行时的容器管理,而docker除了容器管理之外还可以完成镜像构建之类的功能。

containerd提供的api偏底层,不是给用户直接使用的,主要是给容器编排系统的开发者。现在k8s就是用的containerd。

容器原理剖析

  • docker run启动容器,容器的隔离与资源限制

fork一个子进程,设置namespace隔离,用cgroups进行资源的限制,并进行aufs类型的挂载

  • docker -vvolume挂载卷

就是用到普通的挂载,在宿主机上创建一个目录,通过mount(也是aufs类型)挂载到容器指定的挂载卷

  • docker -d

在一个主进程fork出子进程之后,如果父进程退出,则这个子进程就变成了孤儿进程,由pid为1的init进程接管,这就实现了容器后台运行。

  • docker ps

定义和容器信息相关的结构体,在宿主机上找一个目录(比如/var/run/)来作为持久化存储,容器运行时就往目录中/var/run/容器名/config.json里写数据,容器关闭时就删除这个容器对应的目录。

  • docker logs

将容器运行过程中往标准输出中输出的内容保存下来到某个指定目录中的文件中即可,比如/var/lib/docker/containers/<容器id>/<容器id>-json.log

  • docker exec

需要实现容器创建后台运行的容器后,还能进入到容器内部

setns是一个系统调用,可以根据提供的pid进入到指定的namespace中。

原理是打开/proc/<pid>/ns/文件夹下对应的文件,然后使当前进程进入到指定的namespace中。

对于mount namespace来说,一个具有多线程的进程无法使用setns调用进入到对应的namspace。但是go语言每启动一个程序就会进入多线程状态,这里需要借助Cgo,Cgo允许go语言去调用C的函数与标准库

  • docker stop

查找到容器的主进程pid,发送sigterm信号,等待进程结束以及一些相关存储信息文件的更新操作

  • docker rm

查找容器存储信息的位置路径,删除掉这些信息

  • docker commit容器打包镜像

将指定的容器创建的read-write层进行打包

  • docker -e指定环境变量

默认情况下新启动的进程的环境变量都继承自原来父进程的环境变量,但是如果指定了新的环境变量,则会覆盖掉原来的父进程的环境变量。可以先获取父进程的环境变量,在把自定义的变量加进去。

容器的网络配置

linux是通过网络设备操作和使用网卡,系统安装一个网卡后为其生成一个网络设备,例如eth0/ens33;也可以虚拟出一个网络设备,例如veth,bridge。

veth

veth是成对出现的虚拟网络设备,在一端发送数据,在另一端会获取到数据;通常使用veth连接不同的Net Namespace

bridge

虚拟bridge相当于交换机,连接不同的网络设备;网络数据包到达bridge时,通过报文中的mac进行广播或者转发。

路由表

可以通过定义路由表来决定某个网络的namespace中的数据包的流向。

iptables

iptables是对linux内核的netfilter模块进行操作和展示的工具,用来管理包的流动和传送。在网络包传输的各个阶段可以使用不同的策略对包进行加工、传送或丢弃

容器虚拟化常用的两种策略MASQUERADEDNAT来实现容器和宿主机外部的网络通信。

MASQUERADE策略可以将请求包中的源地址转换成一个网络设备的地址。

DNAT也是做网络地址转换,用于把内部网络的端口映射出去。

网络配置实验

image-20230914164309425.png

容器间的网络互通

# 创建两个net namespace,相当于两个容器
sudo ip netns add ns1
sudo ip netns add ns2
# 查看
sudo ip netns show
# 创建veth pairs
sudo ip link add veth1 type veth peer name veth2
sudo ip link add veth3 type veth peer name veth4
# 查看
ip addr
# 将veth的一端移动到对应的namespac中
sudo ip link set veth1 netns ns1
sudo ip link set veth3 netns ns2

# 查看,发现少了两个网卡
ip addr

# 查看对应naemspace里的网卡
sudo ip netns exec ns1 ip addr
sudo ip netns exec ns2 ip addr
# 创建虚拟网桥br0
sudo brctl addbr br0
# 将veth的另一端接入bridge
sudo brctl addif br0 veth2
sudo brctl addif br0 veth4
# 查看效果
$ sudo brctl show
bridge name	bridge id		STP enabled	interfaces
br0		8000.16dd2f6cb8cd	no		veth2
							        veth4
# 为bridge分配ip地址,激活
sudo ip addr add 172.18.0.1/24 dev br0
sudo ip link set br0 up
# 为ns1和ns2内的网卡分配ip,激活上线
sudo ip netns exec ns1 ip addr add 172.18.0.2/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up

sudo ip netns exec ns2 ip addr add 172.18.0.3/24 dev veth3
sudo ip netns exec ns2 ip link set veth3 up
# veth另一端网卡激活上线
sudo ip link set veth2 up
sudo ip link set veth4 up

测试容器互通

#先开一个终端抓包
sudo tcpdump -i br0 -n

# 测试
$sudo ip netns exec ns1 ping -c 2 172.18.0.3
15:09:45.437986 ARP, Request who-has 172.18.0.3 tell 172.18.0.2, length 28
15:09:45.438015 ARP, Reply 172.18.0.3 is-at 96:c7:b8:a6:d5:2f, length 28
15:09:45.438018 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 4490, seq 1, length 64
15:09:45.438078 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 4490, seq 1, length 64
15:09:46.464827 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 4490, seq 2, length 64
15:09:46.464885 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 4490, seq 2, length 64
15:09:47.039855 IP6 fe80::94c7:b8ff:fea6:d52f > ff02::2: ICMP6, router solicitation, length 16
15:09:50.624304 ARP, Request who-has 172.18.0.2 tell 172.18.0.3, length 28
15:09:50.624381 ARP, Reply 172.18.0.2 is-at 26:d1:5a:25:62:bd, length 28

可以看到,先是172.18.0.2发起的ARP请求,询问172.18.0.3MAC地址,然后是ICMP的请求和响应,最后是172.18.0.3的ARP请求。因为接在同一个bridge br0上,所以是二层互通的局域网。

宿主机访问容器

# 在ns1中监听80端口
sudo ip netns exec ns1 nc -lp 80

# 宿主机执行telnet
telnet 172.18.0.2 80

容器访问外网

(1)配置容器内路由,把网络包从容器内转发出来

将bridge设置为“容器”的缺省网关。让非172.18.0.0/24网段的数据包都路由给bridge,这样数据就从“容器”跑到宿主机上来了。

$ sudo ip netns exec ns1 route add default gw 172.18.0.1 veth1
$ sudo ip netns exec ns2 route add default gw 172.18.0.1 veth3

查看容器中的路由规则,非172.18.0.0网段的数据都会走默认规则,也就是发送给网关172.18.0.1

$ sudo ip netns exec ns1 route -n
内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0         172.18.0.1      0.0.0.0         UG    0      0        0 veth1
172.18.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth1

(2)宿主机开启转发功能,配置转发规则

允许IP forwarding,这样才能把网络包转发出去

$ sudo sysctl net.ipv4.conf.all.forwarding=1

配置iptables FORWARD规则

# 首先确认iptables FORWARD的缺省策略
$ sudo iptables -t filter -L FORWARD
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

# 如果不是policy ACCEPT就设置成ACCEPT
$ sudo iptables -t filter -P FORWARD ACCEPT

(3)宿主机配置SNAT规则,在nat表的POSTROUTING链增加规则,当数据包的源地址为172.18.0.0/24网段,出口设备不是br0时,就执行MASQUERADE动作

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/24 ! -o br0 -j MASQUERADE

MASQUERADE也是一种源地址转换动作,它会动态选择宿主机的一个IP做源地址转换;

SNAT动作必须在命令中指定固定的IP地址

测试ns1能访问宿主机同一网段其他主机,也能访问外网ip

sudo ip netns exec ns1 ping -c 2 192.168.217.132

外部访问容器

外部访问容器需要进行DNAT,把目的IP地址从宿主机地址转换成容器地址

nat表的PREROUTING链增加规则,当输入设备不是br0,目的端口为80时,做目的地址转换,将宿主机IP替换为容器IP

$ sudo iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80

测试外部访问容器,可以访问到

# 在容器中开nc监听
$ sudo ip netns exec ns1 nc -lp 80

# 和在宿主机同一个局域网的主机访问宿主机ip:80
nc 192.168.217.135 80

恢复环境

删除虚拟网络设备

$ sudo ip link set br0 down 
$ sudo brctl delbr br0 
$ sudo ip link  del veth1 
$ sudo ip link  del veth3 

iptablersNamesapce的配置在机器重启后被清除。

docker网络情况

在docker中新开两个httpd容器

root@125df1b4f278:/usr/local/apache2# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.3  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:03  txqueuelen 0  (Ethernet)
        RX packets 3376  bytes 9611158 (9.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2886  bytes 157278 (153.5 KiB)
        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
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 3355  bytes 9610366 (9.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2817  bytes 153552 (149.9 KiB)
        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

宿主机的网卡信息如下,会添加两个veth,并且是开启一个容器,就会创建一个新的vethxxx,关掉一个容器就会删除一个vethxxx。

也能看出来宿主机用ens33跟外界通信,docker0和vethxxx是用于容器通信的,具体的还需要再看。

而容器内部的eth0是跟外界通信的虚拟网卡了

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:8dff:fe73:4181  prefixlen 64  scopeid 0x20<link>
        ether 02:42:8d:73:41:81  txqueuelen 0  (以太网)
        RX packets 5703  bytes 230988 (230.9 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 6686  bytes 19216104 (19.2 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.217.135  netmask 255.255.255.0  broadcast 192.168.217.255
        inet6 fe80::27e1:995d:2de7:9ad6  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:37:9a:0c  txqueuelen 1000  (以太网)
        RX packets 736108  bytes 106417820 (106.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 665963  bytes 100899941 (100.8 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  (本地环回)
        RX packets 1176248  bytes 110200088 (110.2 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1176248  bytes 110200088 (110.2 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth095c6bc: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::74b0:e9ff:fe0a:7c73  prefixlen 64  scopeid 0x20<link>
        ether 76:b0:e9:0a:7c:73  txqueuelen 0  (以太网)
        RX packets 2817  bytes 153552 (153.5 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3358  bytes 9610585 (9.6 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth7e1848d: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::7037:21ff:fe32:f25  prefixlen 64  scopeid 0x20<link>
        ether 72:37:21:32:0f:25  txqueuelen 0  (以太网)
        RX packets 2886  bytes 157278 (157.2 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3376  bytes 9611158 (9.6 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

常用命令

未实验验证,参考链接http://kuring.me/post/namespace_network/

列出当前的network namespace

  • 使用lsns命令

lsns命令通过读取/proc/${pid}/ns目录下进程所属的命名空间来实现,如果是通过ip netns add场景的命名空间,但是没有使用该命名空间的进程,该命令是看不到的。

  • 通过ip netns命令

该命令仅会列出有名字的namespace,对于未命名的不能显示。

ip netns identify ${pid} 可以找到进程所属的网络命名空间

ip netns list: 显示所有有名字的namespace

通过pid进入具体的network namespace

  • 通过nsenter命令

nsenter --target $PID --net可以进入到对应的命名空间

  • docker --net参数

docker提供了--net参数用于加入另一个容器的网络命名空间docker run -it --net container:7835490487c1 busybox ifconfig

  • setns系统调用

编写setns.c程序,该程序会进入到进程id所在的网络命令空间,并使用gcc setns.c -o setns进行编译,编译完成后执行./setns /proc/4913/ns/net ifconfig可以看到网卡的信息为容器中的网卡信息。

#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
  int fd = open(argv[1], O_RDONLY);
  if (setns(fd, 0) == -1) {
    perror("setns");
    exit(-1);
  }
  execvp(argv[2], &argv[2]);
  printf("execvp exit\n");
}

如果执行./setns /proc/4913/ns/net /bin/bash,在宿主机上查看docker进程和/bin/bash进程的网络命名空间/proc/${pid}/ns/net,会发现都指向lrwxrwxrwx 1 root root 0 Sep 14 14:42 net -> net:[4026532133]同一个位置。

获取容器的pid

  • /proc/[pid]/ns

可以使用如下命令查看当前容器在宿主机上的进程id。

docker inspect --format '{{.State.Pid}}' a1bf0119d891

每个进程在/proc/${pid}/ns/目录下都会创建其对应的虚拟文件,并链接到一个真实的namespace文件上,如果两个进程下的链接文件链接到同一个地方,说明两个进程同属于一个namespace。