0%

Principles:docker底层原理

Docker vs Hypersior

之前提到过 hypersior 技术:允许多个操作系统共享一个 CPU(多核 CPU 的情况可以是多个 CPU),用以协调多个虚拟机

hypervisor 的每个虚拟机都是一个完整的操作系统,而 docker 采用了“容器”技术,不同容器之间共用一个底层的操作系统:

  • hypervisor 采用的是 硬件资源虚拟化 的方法将硬件资源分配给不同的操作系统使用
  • docker 采用的则是 操作系统虚拟化 的方式实现了对程序运行环境和访问资源在操作系统内部的隔离

docker 底层依赖于 Linux 中的 Namespace,CGroups 和 Union File System(在 window docker 其实是跑了一个 Linux 虚拟机,然后在虚拟机中使用 docker)

Namespace

Namespace 名为命名空间,限制了 Linux 进程的可访问资源

改变一个 Namespace 中的系统资源只会影响当前 Namespace 里的进程,对其他 Namespace 中的进程没有影响,这就为容器技术提供了条件

Linux 一共实现了6种不同类型的 Namespace:

UTS Namespace:主要用来隔离 hostname(主机名) 和 domainname(域名) 两个系统标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  桌面 hostname           
yhellow-virtual-machine
➜ 桌面 sudo unshare -u /bin/zsh /* 创建UTS Namespace */
[sudo] yhellow 的密码:
yhellow-virtual-machine# hostname /* 其hostname拷贝它的父类 */
yhellow-virtual-machine
yhellow-virtual-machine# hostname chunk /* 修改UTS Namespace的hostname */
yhellow-virtual-machine# hostname
chunk
yhellow-virtual-machine# exec $SHELL /* 重新加载Shell环境 */
chunk#
chunk# cat /etc/hostname /* 不会直接修改主机名配置文件 */
yhellow-virtual-machine
chunk# cat /proc/sys/kernel/hostname /* 而是修改内核属性 */
chunk

IPC Namespace:用于隔离 System V IPC 和 POSIX message queues

Mount Namespace:用来隔离各个进程看到的挂载点视图

1
2
3
4
5
6
7
8
9
10
11
12
➜  桌面 ipcs -q /* 消息队列里有一个msg */

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0xafce192c 0 yhellow 644 0 0

➜ 桌面 sudo unshare -iu /bin/bash /* 创建IPC Namespace */
[sudo] yhellow 的密码:
root@yhellow-virtual-machine:/home/yhellow/桌面# ipcs -q /* 消息队列为Null */

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息

PID Namespace:用来隔离进程 ID,同样一个进程在不同的 PID Namespace 里可以拥有不同的 PID

1
2
3
4
5
6
7
8
9
10
11
12
➜  桌面 ps -ef /* 查看所有进程的PID */                     
UID PID PPID C STIME TTY TIME CMD
root 1 0 1 14:52 ? 00:00:01 /sbin/init splash
......
yhellow 4318 4300 1 14:53 pts/0 00:00:00 zsh
yhellow 4415 4318 0 14:53 pts/0 00:00:00 ps -ef
➜ 桌面 sudo unshare --pid --mount-proc --fork /bin/bash /* 同时创建PID Namespace和Mount Namespace */
[sudo] yhellow 的密码:
root@yhellow-virtual-machine:/home/yhellow/桌面# ps -ef /* /bin/bash的PID为'1' */
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:53 pts/0 00:00:00 /bin/bash
root 8 1 0 14:54 pts/0 00:00:00 ps -ef
  • 如果只是创建 PID Namespace,不能保证只看到 Namespace 中的进程
  • 因为类似 ps 这类系统工具读取的是 proc 文件系统,proc 文件系统没有切换的话,虽然有了 PID Namespace,但是不能达到我们在这个 Namespace 中只看到属于自己 Namespace 进程的目的
  • 在创建 PID Namespace 的同时,使用 --mount-proc 选项,会创建新的 Mount Namespace,并自动 mount 新的 proc 文件系统
  • 这样 ps 就可以看到当前 PID Namespace 里面所有的进程了

User Namespace:主要是隔离用户的用户组 ID

1
2
3
4
5
➜  桌面 unshare -r --user /bin/bash /* 创建User Namespace */
root@yhellow-virtual-machine:~/桌面# id /* root权限 */
用户id=0(root) 组id=0(root) 组=0(root),65534(nogroup)
root@yhellow-virtual-machine:~/桌面# echo $$
5366
1
2
3
4
5
6
➜  桌面 ps -ef | grep 5366 | grep -v grep /* 普通权限 */
yhellow 5366 5330 0 15:22 pts/0 00:00:00 /bin/bash
➜ 桌面 cat /proc/5330/uid_map
0 0 4294967295
➜ 桌面 cat /proc/5330/gid_map
0 0 4294967295
  • 进程 5366 在容器(user namespace)外属于一个普通用户,但是在 user namespace 里却属于 root 用户

Net Namespace:用来隔离网络设备,IP地址,端口等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
➜  桌面 sudo ip link add veth0_11 type veth peer name veth1_11 /* 创建一对网卡,分别命名为veth0_11/veth1_11 */
➜ 桌面 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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
......
13: veth1_11@veth0_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 56:8e:30:b2:d0:4e brd ff:ff:ff:ff:ff:ff
14: veth0_11@veth1_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 56:f8:e9:f4:5c:f4 brd ff:ff:ff:ff:ff:ff
➜ 桌面 sudo ip netns add r1 /* 创建两个Net Namespace */
[sudo] yhellow 的密码:
➜ 桌面 sudo ip netns add r2
➜ 桌面 sudo ip link set veth0_11 netns r1 /* 将两个网卡分别加入到对应的netns中 */
➜ 桌面 sudo ip link set veth1_11 netns r2
➜ 桌面 ip a /* 再次查看网卡,在bash当前的namespace中已经看不到veth0_11和veth1_11了 */
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
......
➜ 桌面 sudo nsenter --net=/var/run/netns/r1 /bin/bash /* 切换到对应的netns中 */
root@yhellow-virtual-machine:/home/yhellow/桌面# ip a /* 展示了我们上面加入到r1中的网卡 */
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
14: veth0_11@if13: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 56:f8:e9:f4:5c:f4 brd ff:ff:ff:ff:ff:ff link-netns r2

CGroups

CGroups 全称 Control Groups,是 Linux 下用来控制进程对 CPU、内存、块设备 I/O、网络等资源使用限制的机制

通过使用 CGroups,可以实现为进程组设置内存上限、配置文件系统缓存、调节 CPU 使用率和磁盘 IO 吞吐率等功能,以及对进程组进行快照或者重启等功能(具体的资源控制器由不同的子系统 subsystem 完成)

相关的概念如下:

  • 一个 subsystem 就是一个内核模块,他被关联到一颗 cgroup 树之后,就会在树的每个节点(进程组)上做具体的操作
  • 一个 hierarchy 可以理解为一棵 cgroup 树,树的每个节点就是一个进程组,每棵树都会与零到多个 subsystem 关联
  • 一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的 subsystem 关联

查看当前系统支持的 subsystem(子模块):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  桌面 cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 12 3 1 /* 分配单独的CPU和内存 */
cpu 8 79 1 /* CPU调度控制 */
cpuacct 8 79 1 /* CPU调度控制 */
blkio 2 81 1 /* 设置块设备的输入/输出 */
memory 6 123 1 /* 设置内存限制 */
devices 10 80 1 /* 对访问设备的管控 */
freezer 13 4 1 /* 实现进程组的暂停和恢复 */
net_cls 5 3 1 /* 标记网络数据包并设置网络接口的优先级 */
perf_event 4 3 1
net_prio 5 3 1 /* 标记网络数据包并设置网络接口的优先级 */
hugetlb 11 3 1
pids 9 82 1
rdma 7 3 1
misc 3 1 1
  • subsys_name:subsystem 的名称
  • hierarchy:subsystem 所关联到的 cgroup 树的 ID
    • 如果多个 subsystem 关联到同一颗 cgroup 树,那么他们的这个字段将一样
    • 这个字段将为 “0”,将会是下面3种情况:
      • 当前 subsystem 没有和任何 cgroup 树绑定
      • 当前 subsystem 已经和 cgroup v2 的树绑定
      • 当前 subsystem 没有被内核开启
  • num_cgroups:subsystem 所关联的 cgroup 树中进程组的个数,也即树上节点的个数
  • enabled:“1” 表示开启,“0” 表示没有被开启(可以通过设置内核的启动参数 cgroup_disable 来控制 subsystem 的开启)

Union File System

Union File System,简称 UnionFS,他是一种为 Linux,FreeBSD 和 NetBSD 操作系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务(它用到了一个重要的资源管理技术,叫写时复制 COW)

Linux 启动会先用只读模式挂载 rootfs,运行完完整性检查之后,再切换成读写模式

Docker deamon 为 container 挂载 rootfs 时,也会先挂载为只读模式,但是与 Linux 做法不同的是:

  • 在挂载完只读的 rootfs 之后,Docker deamon 会利用联合挂载技术(Union Mount)
  • 在已有的 rootfs 上再挂一个读写层
  • container 在运行过程中文件系统发生的变化只会写到读写层,并通过 whiteout 技术隐藏只读层中的旧版本文件

Docker 镜像的设计中,引入了层(layer)的概念:

  • 用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs(一个目录)
  • 这样应用 A 和应用 B 所在的容器共同引用相同的 Debian 操作系统层,只读层(存放程序的环境),而各自有各自应用程序层,和读写层

Docker 的镜像就采用了 UnionFS 技术,从而实现了分层的镜像

使用 docker inspect 这个命令来查看 ubuntu 这个镜像文件,输出了以下内容:

1
2
3
4
5
6
7
8
9
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e-init/diff:/var/lib/docker/overlay2/7531a29df933101bd420e45e8a815a17050dec2c140b5d8a32537bc259a4fb96/diff",
"MergedDir": "/var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/merged",
"UpperDir": "/var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/diff",
"WorkDir": "/var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/work"
},
"Name": "overlay2"
},
  • 这些镜像层都位于 /var/lib/docker/overlay2 目录中(OverlayFS 是 Docker 目前的联合文件系统解决方案)

Docker 默认安装的情况下,会使用 /var/lib/docker/ 目录作为存储目录,用以存放拉取的镜像和创建的容器等

1
2
3
➜  桌面 sudo ls /var/lib/docker/        
buildkit engine-id network plugins swarm trust
containers image overlay2 runtimes tmp volumes
  • /var/lib/docker/overlay2 通常用于存放容器虚拟文件系统的相关信息

先看一个案例:

1
2
3
4
5
6
7
8
9
10
➜  桌面 docker start 96e1f1382d93                        
96e1f1382d93
➜ 桌面 docker exec -it 96e1f1382d93 /bin/sh
# ls
bin dev home lib32 libx32 mnt proc run srv tmp var
boot etc lib lib64 media opt root sbin sys usr
# touch /home/1234
# echo 'hello word' > /home/1234
# cat /home/1234
hello word
  • 开启一个容器,往 /home/1234 写入 “hello word”
1
2
3
4
5
6
➜  桌面 sudo ls /var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e
diff link lower merged work
➜ 桌面 sudo ls /var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/diff
home
➜ 桌面 sudo cat /var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/diff/home/1234
hello word
  • 查看 /var/lib/docker/overlay2 中的数据,发现 /home/1234 被写入其中
  • 由此可以发现,docker 容器的读写层其实是挂载到 /var/lib/docker/overlay2 中的
  • 对一个容器的修改只会影响其读写层,而这些变化也直接体现在 /var/lib/docker/overlay2
1
2
3
4
➜  桌面 sudo rm /var/lib/docker/overlay2/a11758995ecf6105ad01ed03f9e8f4304d4685f58eae032dff5e29ec4b55cf8e/diff/home/1234
➜ 桌面 docker exec -it 96e1f1382d93 /bin/sh
# cat /home/1234
hello word
  • 若删除 /home/1234 后重新进入容器,会发现 /home/1234 依然存在
  • 这时因为文件 /home/1234 仍被加载在内存中
1
2
3
4
5
6
7
➜  桌面 docker stop $(docker ps -q)         
96e1f1382d93
➜ 桌面 docker start 96e1f1382d93
96e1f1382d93
➜ 桌面 docker exec -it 96e1f1382d93 /bin/sh
# cat /home/1234
cat: /home/1234: No such file or directory
  • 若此时关闭容器,重新打开并进入后会发现 /home/1234 消失

docker 处理异构操作系统

对于虚拟化程序来说,处理异构二进制代码的能力是必不可少的,Qemu 就内置了一个翻译器,专门用于把异构的二进制代码给翻译为本架构能够理解的形式

而 docker 也是借用了 Qemu 的翻译器(qumu-user-static)来处理异构程序,使用方法如下:

1
docker pull multiarch/qemu-user-static:register
  • PS:每次重启机器,需重新注册
  • 注册成功后,可以使用如下命令查询 aarch64 对应的解释器:(其他架构同理)
1
cat /proc/sys/fs/binfmt_misc/qemu-aarch64
  • 撤销平台的解释器:
1
docker run --rm --privileged multiarch/qemu-user-static:register