docker基础实现原理

226 阅读9分钟

linux系统支持命名空间、cgroup和overlay层次文件系统,docker就是调用操作系统提供的这些接口来实现各种隔离效果

命名空间

由linux系统内核提供的功能,用于资源隔离,产生虚拟化技术,命名空间下运行的程序只能感知到当前命名空间下的其他进程和文件系统,主要分为以下几种命名空间:

命名空间函数参数隔离内容
UTSCLONE_NEWUST主机名和域名,是Unix Time-sharing System简写
IPCCLONE_NEWIPC信号量、消息队列和共享内存
PIDCLONE_NEWPID进程,不同命名空间下的进程ID可以相同
NetworkCLONE_NEWNET网络设备和网卡等
MountCLONE_NEWNS文件挂载
UserCLONE_NEWUSER用户和用户组

以上6种隔离在/proc/<进程ID>/ns目录下可以找到对应的文件,通过ll命令查看任意一个进程如下:

ll /proc/1123/ns
lrwxrwxrwx 1 root root 0 Jun 13 14:38 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jun 13 14:38 uts -> 'uts:[4026531838]'

其中像4026531835这样的数字是namespace号,再查看其他进程的ns目录,会发现相同的namespace号,namespace号相同表示两个进程处于同一个命名空间下

linux自带的clone函数可用来创建子进程,其中参数flags就可以指定隔离机制,通过man命令查看clone函数

man clone

网络命名空间

通过网络命名空间对网络进行隔离,其他都不隔离,比如安装的软件,常用的命令如下:

// 列出当前存在的网络命名空间
ip netns ls

// 创建一个叫test的网络命名空间
ip netns add test

// 删除test网络命名空间
ip netns delete test

// 进入test网络命名空间
ip netns exec test bash

// 退出网络命名空间
exit

// 不进入命名空间,直接执行命令,可执行任意命令
ip netns exec test netstat -anp

命名空间内的操作

进入指定的网络命名空间后执行

// 查看当前命名空间下的网络接口
ip link

// 输出以下结果
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

创建一个新的网络命名空间后默认会有一个lo,状态是DOWN,表示未启动,此时ping 127.0.0.1是不通的,可通过以下命令启动

// 启动lo
ip link set lo up

// 关闭lo
ip link set lo down

通过以下命令查看IP地址

// 查看命名空间下的IP
ip addr

命名空间之间通信

在系统命名空间下创建两个虚拟网卡

ip link add interface1 type veth peer name interface2

// 查看接口,可查看到interface2@interface1和interface1@interface2两条记录,分别都是DOWN状态
ip link | grep interface

分别将interface1和interface2移到test1和test2命名空间下

ip link set interface1 netns test1
ip link set interface2 netns test2

// 查看
ip netns exec test1 ip link
ip netns exec test2 ip link

分别设置两个命名空间下网卡的IP,注意这里两个IP必须在同一个网段内,否则ping时肯定会网络不可达

ip netns exec test1 ip addr add dev interface1 192.168.10.10/24
ip netns exec test2 ip addr add dev interface2 192.168.10.20/24

启动两个命名空间下的网卡

ip netns exec test1 ip link set interface1 up
ip netns exec test2 ip link set interface2 up

// 查看网卡状态,状态是UP就表示正常了
ip netns exec test1 ip link
ip netns exec test2 ip link

测试能否连通

ip netns exec test1 ping 192.168.10.20
ip netns exec test2 ping 102.168.10.10

// 下面两个是ping自身,需要启动两个命名空间下的lo网卡才能ping通,这里只是随便说一下
ip netns exec test1 ping 192.168.10.10
ip netns exec test2 ping 192.168.10.20

通过桥进行连接

原理是两个命名空间各自和桥建立连接,再通过桥与对方通信

// 创建桥
ip link add bridge1 type bridge

// 查看
ip link | grep bridge1

// 创建两个网络命名空间
ip netns add test1
ip netns add test2

先将test1与bridge1建立连接

// 创建test1到bridge1和bridge1到test1的网卡
ip link add test1ToBridge1 type veth peer name bridge1ToTest1

// 将test1到bridge1网卡移到test1命名空间下
ip link set test1ToBridge1 netns test1

// 设置IP
ip netns exec test1 ip addr add dev test1ToBridge1 192.168.10.10/24

// 启动两个网卡
ip netns exec test1 ip link set test1ToBridge1 up
ip link set bridge1ToTest1 master bridge1
ip link set bridge1ToTest1 up

// 查看网卡状态
ip netns exec test1 ip link
ip link

将test2与bridge1连接上

// 创建test2到bridge1和bridge1到test2的网卡
ip link add test2ToBridge1 type veth peer name bridge1ToTest2

// 将test2到bridge1网卡移到test2命名空间下
ip link set test2ToBridge1 netns test2

// 设置IP
ip netns exec test2 ip addr add dev test2ToBridge1 192.168.20.20/24

// 启动两个网卡
ip netns exec test2 ip link set test2ToBridge1 up
ip link set bridge1ToTest2 master bridge1
ip link set bridge1ToTest2 up

最后启动桥

ip link set bridge1 up

测试通信

ip netns exec test1 ping 192.168.20.20

这里目前是ping不通的,因为跨了网段,需要指定路由,先在bridge1上添加两个IP

ip addr add dev bridge1 192.168.10.1/24
ip addr add dev bridge1 192.168.20.1/24

在主命名空间下设置路由

// 去往192.168.10.0/24这个网段的数据走bridge1网卡
route add -net 192.168.10.0/24 dev bridge1
// 去往192.168.20.0/24这个网段的数据走bridge1网卡
route add -net 192.168.20.0/24 dev bridge1

再到test1和test2两个命名空间下设置到达对方的路由

ip netns exec test1 route add -net 192.168.20.0/24 gw 192.168.10.1
ip netns exec test2 route add -net 192.168.10.0/24 gw 192.168.20.1

最后再使用DNAT技术修改源IP

// 从192.168.10.0/24发到192.168.20.0/24的数据包,修改IP为192.168.10.1
iptables -t nat -I PREROUTING -s 192.168.10.0/24 -d 192.168.20.0/24 -j DNAT --to 192.168.10.1

此时再ping就可以通了

ip netns exec test1 ping 192.168.20.20

PING 192.168.20.20 (192.168.20.20) 56(84) bytes of data.
64 bytes from 192.168.20.20: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 192.168.20.20: icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from 192.168.20.20: icmp_seq=3 ttl=64 time=0.089 ms
64 bytes from 192.168.20.20: icmp_seq=4 ttl=64 time=0.084 ms
64 bytes from 192.168.20.20: icmp_seq=5 ttl=64 time=0.080 ms

cgroup

是control group的简写,对系统资源进行限制,比如CPU和内存在某个命名空间下进行限制

对应的文件在/sys/fs/cgroup目录下,这些文件不能使用vi命令编辑,可以通过cat命令查看内容,echo命令修改内容

下面就来创建一个cgroup,体验一下:

apt install cgroup-tools
cgcreate -g cpu:test1

这会在/sys/fs/cgroup/cpu目录下创建test1目录,该目录下的文件就是用来做资源限制的配置文件,并且子目录会继承父目录中的配置文件(即子目录下没有某一项的配置文件,就会找其父目录下的对应配置文件)

tasks目录下ll可以看到当前cgroup下管理的所有进程的ID

限制CPU

进入/sys/fs/cgroup/cpu/test1目录,这个目录是上面刚创建的,ll查看目录下的文件,其中有以下两个重要文件:

  1. cpu.cfs_quota_us 是占用CPU的时间,单位:微秒,默认是-1表示不限制
  2. cpu.cfs_period_us 是CPU分配的周期,默认是100000,一般不修改,修改上面那个就好

我们通过echo 30000 > cpu.cfs_quota_uscpu.cfs_quota_us修改为30000表示该cgroup下的所有进程最大一共只能占用30% CPU

现在将一个进程加到当前cgroup中:

cgclassify -g cpu:test1 进程ID

通过上面的方式可以将进程加到cgroup中,也可以echo 进程ID >> tasks文件>>是追加一行,注意不要用>,这是覆盖,下面绑定CPU和限制内存也可以这么将进程加到cgroup中

绑定CPU

/sys/fs/cgroup/cpuset目录下,修改cpuset.cpus文件,如下:

echo '1-2' > cpuset.cpus

表示该cgroup下进程只能占用1和2两个CPU核心

限制内存

/sys/fs/cgroup/memory目录下,修改memory.limit_in_bytes文件,如下:

echo $((256 * 1024 * 1024)) > memory.limit_in_bytes

overlay

层次文件系统,从下往上分为lower dirupper dirmerged,其中lower dir是只读的,upper dir可读可写,mergedlowerupper的组合,lower层可以是多个目录,层次文件系统的特点是:上下合并、同名覆盖和写时拷贝

  • 上下合并指的是lower和upper中的文件和目录会合并到merged层

  • 同名覆盖指的是lower和upper中有同名文件时,upper的文件会覆盖掉lower的文件

  • 写时拷贝指的是如果要修改的文件在upper层,则直接修改;如果要修改的文件在lower层,则会将文件拷贝到upper层再对该文件修改

创建文件时,在merged层创建,会保存到upper层

删除文件时,如果文件在upper层,则直接删除;如果文件在lower层,则在upper层创建一个任何用户都没有权限的同名,且大小为0的文件

docker的镜像就是由overlay2层次文件系统组合出来的,构建镜像时每一步操作都会产生一层,当有相同操作时,docker就可以使用缓存了,docker的本地镜像目录默认是/var/lib/docker/overlay2,进入这个目录可以查看到很多子目录,这些都是一层一层的文件

下面自己动手来创建一个层次目录,测试在不同层次创建和修改文件等操作

// 创建4个目录
mkdir lower upper merged worker

// 创建一些文件,测试合并后的结果
echo 'a-lower' > lower/a.txt
echo 'a-upper' > upper/a.txt
touch lower/b.txt

创建分层的文件,如果lower层有多个目录可用:隔开

mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=worker merged

一行命令就可以创建出分层的文件,如果执行报错,可查看内核报错信息

journalctl -xe

现在查看merged目录

ll merged

现在可以测试在merged中创建文件

echo 'c-merged' > merged/c.txt

// 查看创建的文件
ll merged

// 查看upper目录
ll upper