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 [sudo] yhellow 的密码: yhellow-virtual -machine# hostname yhellow-virtual -machine yhellow-virtual -machine# hostname chunk yhellow-virtual -machine# hostname chunk yhellow-virtual -machine# exec $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 --------- 消息队列 ----------- 键 msqid 拥有者 权限 已用字节数 消息 0xafce192c 0 yhellow 644 0 0 ➜ 桌面 sudo unshare -iu /bin/bash [sudo] yhellow 的密码: root@yhellow-virtual -machine:/home/yhellow/桌面# ipcs -q --------- 消息队列 ----------- 键 msqid 拥有者 权限 已用字节数 消息
PID Namespace:用来隔离进程 ID,同样一个进程在不同的 PID Namespace 里可以拥有不同的 PID
1 2 3 4 5 6 7 8 9 10 11 12 ➜ 桌面 ps -ef 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 [sudo] yhellow 的密码: root@yhellow-virtual -machine:/home/yhellow/桌面# ps -ef 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 root@yhellow-virtual -machine:~/桌面# id 用户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 ➜ 桌面 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 :8 e:30 :b2:d0:4 e 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:5 c:f4 brd ff:ff:ff:ff:ff:ff ➜ 桌面 sudo ip netns add r1 [sudo] yhellow 的密码: ➜ 桌面 sudo ip netns add r2 ➜ 桌面 sudo ip link set veth0_11 netns r1 ➜ 桌面 sudo ip link set veth1_11 netns r2 ➜ 桌面 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 ...... ➜ 桌面 sudo nsenter --net=/var/run/netns/r1 /bin/bash root@yhellow-virtual -machine:/home/yhellow/桌面# ip a 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:5 c: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 8 79 1 cpuacct 8 79 1 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