资讯专栏INFORMATION COLUMN

你应当了解的Docker底层技术

paulli3 / 1252人阅读

摘要:底层技术用于环境隔离,支持的包括以及新加入的等,用于隔离主机名和域名,使用标识,用于隔离进程间通信资源如消息队列等,使用标识,隔离进程,用于隔离网络,用于隔离挂载点,用于隔离用户组。

本文已获得原作者__七把刀__授权。

Docker 容器技术已经发展了好些年,在很多项目都有应用,线上运行也很稳定。整理了部分 Docker 的学习笔记以及新版本特性,对Docker感兴趣的同学可以看看,之前整理过的 Linux namespace 可以见之前的博文。
1容器 & Docker & 虚拟机

Container (容器)是一种轻量级的虚拟化技术,它不需要模拟硬件创建虚拟机。在 Linux 系统里面,使用到 Linux kernel 的 cgroups,namespace(ipc,network, user,pid,mount),capability 等用于隔离运行环境和资源限制的技术,我们称之为容器。容器技术早就出现。例如 Solaris Zones 和 BSD jails 就是非 Linux 操作系统上的容器,而用于 Linux 的容器技术也有很多如 Linux-Vserver、OpenVZ 和 FreeVPS。虽然这些技术都已经成熟,但是这些解决方案还没有将它们的容器支持集成到主流 Linux 内核。总的来说,容器不等同于 Docker,容器更不是虚拟机。

LXC 项目由一个 Linux 内核补丁和一些 userspace 工具组成,它提供一套简化的工具来维护容器,用于虚拟环境的环境隔离、资源限制以及权限控制。LXC 有点类似 chroot,但是它比 chroot 提供了更多的隔离性。

Docker 最初目标是做一个特殊的 LXC 的开源系统,最后慢慢演变为它自己的一套容器运行时环境。Docker 基于 Linux kernel 的 CGroups,Namespace,UnionFileSystem 等技术封装成一种自定义的容器格式,用于提供一整套虚拟运行环境。毫无疑问,近些年来 Docker 已经成为了容器技术的代名词,如其官网介绍的Docker is world"s leading software containerization platform。本文会先简单介绍 Docker 基础概念,然后会分析下 Docker 背后用到的技术。Debian 上安装 Docker 方法参见docker-ce-installation-in-debian。

2 Docker 基础 2.1 Docker Engine

Docker 提供了一个打包和运行应用的隔离环境,称之为容器,Docker 的隔离和安全特性允许你在一个主机同时运行多个容器,而且它并不像虚拟机那样重量级,容器都是基于宿主机的内核运行的,它是轻量的,不管你运行的是ubuntu, debian 还是其他 Linux 系统,用的内核都是宿主机内核。Docker 提供了工具和平台来管理容器,而 Docker Engine 则是一个提供了大部分功能组件的CS架构的应用,如架构图所示,Docker Engine 负责管理镜像,容器,网络以及数据卷等。

2.2 Docker 架构

Docker 更详细的架构如图所示,采用CS架构,client 通过 RESTFUL API 发送 docker 命令到 docker daemon 进程,docker daemon 进程执行镜像编译,容器启停以及分发,数据卷管理等,一个 client 可以与多个 docker daemon 通信。

Docker Daemon:Docker 后台进程,用于管理镜像,容器以及数据卷。

Docker Client:用于与 Docker Daemon 交互。

Docker Registry:用于存储 Docker 镜像,类似 github,公共的 Registry 有 Docker Hub 和 Docker Cloud。

Images:镜像是用于创建容器的一种只读模板。镜像通常基于一个基础镜像,在此基础上安装额外的软件。比如你的 nginx 镜像可能基于 debian 然后安装 nginx 并添加配置,你可以从 Docker Hub 上拉取已有的镜像或者自己通过 Dockerfile 来编译一个镜像。

Containers:容器是镜像的一个可运行示例,我们可通过 Docker client 或者 API 来创建,启停或者删除容器。默认情况下,容器与宿主机以及其他容器已经隔离,当然你可以控制隔离容器的网络或者存储的方式。

Services:服务是 docker swarm 引入的概念,可以用于在多宿主机之间伸缩容器数目,支持负载均衡已经服务路由功能。

2.3 Docker底层技术概览

通过下面命令运行一个 debian 容器,attach 到一个本机的命令行并运行/bin/bash。

docker run -i -t debian /bin/bash

这个命令背后都做了什么?

1.如果本机没有 debian 镜像,则会从你配置的 Registry 里面拉取一个 debian 的 latest 版本的镜像,跟你运行了docker pull debian效果一样。

2.创建容器。跟运行docker create一样。

3.给容器分配一个读写文件系统作为该容器的 final layer,容器可以在它的文件系统创建和修改文件。

4.Docker 为容器创建了一套网络接口,给容器分配一个 ip。默认情况下,容器可以通过默认网络连通到外部网络。

5.Docker启动容器并执行 /bin/bash。因为启动时指定了 -i -t 参数,容器是以交互模式运行且 attach 到本地终端,我们可以在终端上输入命令并看到输出。

6.运行 exit 可以退出容器,但是此时容器并没有被删除,我们可以再次运行它或者删除它。

可以发现,容器的内核版本是跟宿主机一样的,不同的是容器的主机名是独立的,它默认用容器 ID 做主机名。我们运行ps -ef可以发现容器进程是隔离的,容器里面看不到宿主机的进程,而且它自己有 PID 为1的进程。此外,网络也是隔离的,它有独立于宿主机的 IP。文件系统也是隔离的,容器有自己的系统和软件目录,修改容器内的文件并不影响宿主机对应目录的文件。

root@stretch:/home/vagrant# uname -r
4.9.0-6-amd64
root@stretch:/home/vagrant# docker run -it --name demo alpine /bin/ash
/ # uname -r   ## 容器内
4.9.0-6-amd64

/ # ps -ef
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/ash
    7 root       0:00 ps -ef
    
/ # ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    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
6: eth0@if7:  mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

这些隔离机制并不是 Docker 新开发的技术,而是依托 Linux kernel 以及一些已有的技术实现的,主要包括:

Linux Namespaces(Linux2.6.24后引入):命名空间用于进程(PID)、网络(NET)、挂载点(MNT)、UTS、IPC 等隔离。

Linux Control Groups(CGroups):用于限制容器使用的资源,包括内存,CPU等。

Union File Systems:UnionFS 把多个目录结合成一个目录,对外使用,最上层目录为读写层(通常只有1个),下面可以有一个或多个只读层,见容器和镜像分层图。Docker 支持 OverlayFS,AUFS、DeviceMapper、btrfs 等联合文件系统。

Container Format: Docker Engine 组合 Namespaces,CGroup 以及 UnionFS 包装为一个容器格式,默认格式为 libcontainer,后续可能会加入 BSD Jails 或 Solaris Zones 容器格式的支持。

3 Docker底层技术 3.1 Namespaces

Namespaces 用于环境隔离,Linux kernel 支持的 Namespace 包括UTS, IPC, PID, NET, NS, USER 以及新加入的 CGROUP 等,UTS 用于隔离主机名和域名,使用标识 CLONE_NEWUTS,IPC 用于隔离进程间通信资源如消息队列等,使用标识 CLONE_NEWIPC,PID 隔离进程,NET 用于隔离网络,NS 用于隔离挂载点,USER 用于隔离用户组。默认情况下,通过 clone 系统调用创建子进程的 namespace 与父进程是一致的,而你可以在 clone 系统调用中通过flag参数设置隔离的名字空间来隔离,当然也可以更加方便的直接用 unshare 命令来创建新的 namespace。查看一个进程的各 Namespace 命令如下:

root@stretch:/home/vagrant# ls -ls /proc/self/ns/
0 lrwxrwxrwx 1 root root 0 May 17 22:04 cgroup -> cgroup:[4026531835]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 ipc -> ipc:[4026531839]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 mnt -> mnt:[4026531840]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 net -> net:[4026531957]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 pid -> pid:[4026531836]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 user -> user:[4026531837]
0 lrwxrwxrwx 1 root root 0 May 17 22:04 uts -> uts:[4026531838]
PID Namespace

在容器中,有自己的 Pid namespace,因此我们看到的只有 PID 为1的初始进程以及它的子进程,而宿主机的其他进程容器内是看不到的。通常来说, Linux 启动后它会先启动一个 PID 为1的进程,这是系统进程树的根进程,根进程会接着创建子进程来初始化系统服务。PID namespace 允许在新的namespace 创建一棵新的进程树,它可以有自己的PID为1的进程。在 PID namespace 的隔离下,子进程名字空间无法知道父进程名字空间的进程,如在Docker容器中无法看到宿主机的进程,而父进程名字空间可以看到子进程名字空间的所有进程。如图所示:

Linux 内核加入 PID Namespace 后,对 pid 结构进行了修改,新增的 upid 结构用于跟踪 namespace 和 pid。

## 加入PID Namespace之前的pid结构
 struct pid {
    atomic_t count;             /* reference counter */
    int nr;                 /* the pid value */
    struct hlist_node pid_chain;        /* hash chain */
    ...
};

## 加入PID Namespace之后的pid结构
struct upid {
    int nr;                 /* moved from struct pid */
    struct pid_namespace *ns;   
    struct hlist_node pid_chain;        /* moved from struct pid */
};

struct pid {
     ...
     int level;             /* the number of upids */
     struct upid numbers[0];
};

可以通过 unshare 测试下 PID namespace,可以看到新的 bash 进程它的 pid namespace 与父进程的不同了,而且它的 pid 是1。

root@stretch:/home/vagrant# unshare --fork --pid bash
root@stretch:/home/vagrant# echo $$
1
root@stretch:/home/vagrant# ls -ls /proc/self/ns/
0 lrwxrwxrwx 1 root root 0 May 19 15:24 cgroup -> cgroup:[4026531835]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 ipc -> ipc:[4026531839]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 mnt -> mnt:[4026531840]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 net -> net:[4026531957]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 pid -> pid:[4026532232]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 user -> user:[4026531837]
0 lrwxrwxrwx 1 root root 0 May 19 15:24 uts -> uts:[4026531838]
NS Namespace

NS Namespace 用于隔离挂载点,不同 NS Namespace 的挂载点互不影响。创建一个新的 Mount Namespace 效果有点类似 chroot,不过它隔离的比 chroot 更加完全。这是历史上的第一个 Linux Namespace,由此得到了 NS 这个名字而不是用的 Mount

在最初的 NS Namespace 版本中,挂载点是完全隔离的。初始状态下,子进程看到的挂载点与父进程是一样的。在新的 Namespace 中,子进程可以随意 mount/umount 任何目录,而不会影响到父 Namespace。使用 NS Namespace完全隔离挂载点初衷很好,但是也带来了某些情况下不方便,比如我们新加了一块磁盘,如果完全隔离则需要在所有的 Namespace 中都挂载一遍。为此,Linux 在2.6.15版本中加入了一个 shared subtree 特性,通过指定 Propagation 来确定挂载事件如何传播。比如通过指定 MS_SHARED 来允许在一个 peer group (子 namespace 和父 namespace 就属于同一个组)共享挂载点,mount/umount 事件会传播到 peer group 成员中。使用 MS_PRIVATE 不共享挂载点和传播挂载事件。其他还有 MS_SLAVENS_UNBINDABLE 等选项。可以通过查看 cat /proc/self/mountinfo 来看挂载点信息,若没有传播参数则为 MS_PRIVATE 的选项。

例如你在初始 namespace 有两个挂载点,通过 mount --make-shared /dev/sda1 /mntS 设置 /mntS 为shared类型,mount --make-private /dev/sda1 /mntP 设置 /mntP 为 private 类型。当你使用 unshare -m bash 新建一个 namespace 并在它们下面挂载子目录时,可以发现 /mntS 下面的子目录 mount/umount 事件会传播到父 namespace,而 /mntP 则不会。关于 mount 各种模式详解可以参考这篇文章。

在前面例子 Pid namespace 隔离后,我们在新的名字空间执行 ps -ef 可以看到宿主机进程,这是因为 ps 命令是从 /proc 文件系统读取的数据,而文件系统我们还没有隔离,为此,我们需要在新的 NS Namespace 重新挂载 proc 文件系统来模拟类似 Docker 容器的功能。

root@stretch:/home/vagrant# unshare --pid --fork --mount-proc bash
root@stretch:/home/vagrant# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:36 pts/1    00:00:00 bash
root         2     1  0 15:36 pts/1    00:00:00 ps -ef

可以看到,隔离了 NS namespace 并重新挂载了 proc 后,ps 命令只能看到2个进程了,跟我们在 Docker 容器中看到的一致。

NET Namespace

Docker 容器中另一个重要特性是网络独立(之所以不用隔离一词是因为容器的网络还是要借助宿主机的网络来通信的),使用到 Linux 的 NET Namespace 以及 vet。veth 主要的目的是为了跨 NET namespace 之间提供一种类似于 Linux 进程间通信的技术,所以 veth 总是成对出现,如下面的 veth0 和 veth1。它们位于不同的 NET namespace 中,在 veth 设备任意一端接收到的数据,都会从另一端发送出去。veth 实现了不同namespace的网络数据传输。

在 Docker 中,宿主机的 veth 端会桥接到网桥中,接收到容器中的 veth 端发过来的数据后会经由网桥 docker0 再转发到宿主机网卡 eth0,最终通过 eth0 发送数据。当然在发送数据前,需要经过iptables MASQUERADE 规则将源地址改成宿主机 ip,这样才能接收到响应数据包。而宿主机网卡接收到的数据会通过iptables DNAT 根据端口号修改目的地址和端口为容器的ip 和端口,然后根据路由规则发送到网桥 docker0 中,并最终由网桥 docker0 发送到对应的容器中。

Docker 里面网络模式分为 bridge,host,overlay 等几种模式,默认是采用 bridge 模式网络如图所示。如果使用 host 模式,则不隔离直接使用宿主机网络。overlay 网络则是更加高级的模式,可以实现跨主机的容器通信,后面会多带带总结下 Docker 网络这个专题。

USER Namespace

user namespace 用于隔离用户和组信息,在不同的 namespace 中用户可以有相同的 UID 和 GID,它们之间互相不影响。父子 namespace 之间可以进行用户映射,如父 namespace (宿主机)的普通用户映射到子 namespace (容器)的 root 用户,以减少子 namespace 的 root 用户操作父 namespace 的风险。user namespace 功能虽然在很早就出现了,但是直到 Linux kernel 3.8之后这个功能才趋于完善。

创建新的 user namespace 之后第一步就是设置好 user 和 group 的映射关系。这个映射通过设置 /proc/PID/uid_map(gid_map) 实现,格式如下,ID-inside-ns 是容器内的 uid/gid,而 ID-outside-ns 则是容器外映射的真实 uid/gid。比如0 1000 1表示将真实的 uid =1000映射为容器内的 uid=0,length 为映射的范围。

ID-inside-ns   ID-outside-ns   length

不是所有的进程都能随便修改映射文件的,必须同时具备如下条件:

修改映射文件的进程必须有 PID 进程所在 user namespace 的 CAP_SETUID/CAP_SETGID 权限。

修改映射文件的进程必须是跟 PID 在同一个 user namespace 或者 PID 的父 namespace。

映射文件 uid_map 和 gid_map 只能写入一次,再次写入会报错。
docker 1.10之后的版本可以通过在 docker daemon 启动时加上 --userns-remap=[USERNAME] 来实现 USER Namespace 的隔离。我们指定了 username = ssj 启动 dockerd,查看 subuid 文件可以发现 ssj 映射的 uid 范围是165536到165536+65536= 231072,而且在docker目录下面对应 ssj 有一个独立的目录165536.165536存在。

root@stretch:/home/vagrant# cat /etc/subuid
vagrant:100000:65536
ssj:165536:65536

root@stretch:/home/vagrant# ls /var/lib/docker/165536.165536/
builder/  containerd/  containers/  image/  network/  ...

运行 docker images -a 等命令可以发现在启用 user namespace 之前的镜像都看不到了。此时只能看到在新的 user namespace 里面创建的 docker 镜像和容器。而此时我们创建一个测试容器,可以在容器外看到容器进程的 uid_map 已经设置为 ssj,这样容器中的 root 用户映射到宿主机就是 ssj 这个用户了,此时如果要删除我们挂载的 /bin 目录中的文件,会提示没有权限,增强了安全性。

### dockerd 启动时加了 --userns-remap=ssj
root@stretch:/home/vagrant# docker run -it -v /bin:/host/bin --name demo alpine /bin/ash
/ # rm /host/bin/which 
rm: remove "/host/bin/which"? y
rm: can"t remove "/host/bin/which": Permission denied

### 宿主机查看容器进程uid_map文件
root@stretch:/home/vagrant# CPID=`ps -ef|grep "/bin/ash"|awk "{printf $2}"`
root@stretch:/home/vagrant# cat /proc/$CPID/uid_map
         0     165536      65536
其他Namespace

UTS namespace 用于隔离主机名等。可以看到在新的 uts namespace 修改主机名并不影响原 namespace 的主机名。

root@stretch:/home/vagrant# unshare --uts --fork bash
root@stretch:/home/vagrant# hostname
stretch
root@stretch:/home/vagrant# hostname modified
root@stretch:/home/vagrant# hostname
modified
root@stretch:/home/vagrant# exit
root@stretch:/home/vagrant# hostname
stretch

IPC Namespace 用于隔离 IPC 消息队列等。可以看到,新老 ipc namespace 的消息队列互不影响。

root@stretch:/home/vagrant# ipcmk -Q
Message queue id: 0
root@stretch:/home/vagrant# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x26c3371c 0          root       644        0            0           

root@stretch:/home/vagrant# unshare --ipc --fork bash
root@stretch:/home/vagrant# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages  

CGROUP Namespace 是 Linux 4.6以后才支持的新 namespace。容器技术使用 namespace 和 cgroup 实现环境隔离和资源限制,但是对于 cgroup 本身并没有隔离。没有 cgroup namespace 前,容器中一旦挂载 cgroup 文件系统,便可以修改整全局的 cgroup 配置。有了 cgroup namespace 后,每个 namespace 中的进程都有自己的 cgroup 文件系统视图,增强了安全性,同时也让容器迁移更加方便。在我测试的 Docker 18.03.1-ce 版本中容器暂时没有用到 cgroup namespace,这里就不再展开。

3.2 CGroups

Linux CGroups 用于资源限制,包括限制 CPU、内存、blkio 以及网络等。通过工具 cgroup-bin (sudo apt-get install cgroup-bin) 可以创建 CGroup 并进入该 CGroup 执行命令。

root@stretch:/home/vagrant# cgcreate -a vagrant -g cpu:cg1
root@stretch:/home/vagrant# ls /sys/fs/cgroup/cpu/cg1/
cgroup.clone_children  cpu.cfs_period_us  cpu.shares  cpuacct.stat   cpuacct.usage_all     cpuacct.usage_percpu_sys   cpuacct.usage_sys   notify_on_release
cgroup.procs           cpu.cfs_quota_us   cpu.stat    cpuacct.usage  cpuacct.usage_percpu  cpuacct.usage_percpu_user  cpuacct.usage_user  tasks

cpu.cfs_period_us 和 cpu.cfs_quota_us,它们分别用来限制该组中的所有进程在单位时间里可以使用的 cpu 时间,这里的 cfs(Completely Fair Scheduler) 是完全公平调度器的意思。cpu.cfs_period_us 是时间周期,默认为100000,即100毫秒。而 cpu.cfs_quota_us 是在时间周期内可以使用的时间,默认为-1即无限制。cpu.shares 用于限制cpu使用的,它用于控制各个组之间的配额。比如组 cg1的 cpu.shares 为1024,组 cg2的cpu.shares 也是1024,如果都有进程在运行则它们都可以使用最多50%的限额。如果 cg2 组内进程比较空闲,那么 cg1 组可以将使用几乎整个 cpu,tasks 存储的是该组里面的进程 ID。( 注: debian8 默认没有 cfs 和 memory cgroup 支持,需要重新编译内核及修改启动参数,debian9 默认已经支持)

我们先在默认的分组里面运行一个死循环程序 loop.py,因为默认分组 /sys/fs/cgroup/cpu/cpu.cfs_period_uscfs_quota_us 是默认值,所以是没有限制 cpu 使用的。可以发现1个 cpu us 立马接近100%了。

# loop.py
while True: pass

设置 cg1 组 的cfs_quota_us 位50000,即表示该组内进程最多使用50%的 cpu 时间,运行 cgexec 命令进入 cg1 的 cpu 组,然后运行 loop.py,可以发现 cpu us 在50%以内了,此时也可以在 tasks 文件中看到我们刚刚 cgexec 创建的进程 ID。

root@stretch:/home/vagrant# echo 50000 > /sys/fs/cgroup/cpu/cg1/cpu.cfs_quota_us
root@stretch:/home/vagrant# cgexec -g cpu:cg1 /bin/bash

Docker 里面要限制内存和 CPU 使用,可以在启动时指定相关参数即可。比如限制 cpu 使用率,加 cpu-period 和 cpu-quota 参数,限制执行的 cpu 核,加 --cpuset-cpus 参数。限制内存使用,加--memory参数。当然,我们可以看到在 /sys/fs/cgroup/cpu/docker/ 目录下有个以 containerid 为名的分组,该分组下面的 cpu.cfs_period_us 和 cpu.cfs_quota_us 的值就是我们在启动容器时指定的值。

root@stretch:/home/vagrant# docker run -i -t --cpu-period=100000 --cpu-quota=50000 --memory=512000000 alpine /bin/ash
3.3 Capabilities

我们在启动容器时会时常看到这样的参数 --cap-add=NET_ADMIN,这是用到了 Linux 的 capability 特性。 capability 是为了实现更精细化的权限控制而加入的。我们以前熟知通过设置文件的 SUID 位,这样非 root 用户的可执行文件运行后的 euid 会成为文件的拥有者 ID,比如 passwd 命令运行起来后有 root 权限,有 SUID 权限的可执行文件如果存在漏洞会有安全风险。(查看文件的 capability 的命令为 filecap -a,而查看进程 capability 的命令为 pscap -a,pscap 和 filecap工具需要安装 libcap-ng-utils这个包)。

对于 capability,可以看一个简单的例子便于理解。如 Debian 系统中自带的 ping 工具,它是有设置 SUID 位的。这里拷贝 ping 重命名为 anotherping,anotherping 的 SUID 位没有设置,运行会提示权限错误。这里,我们只要将其加上 cap_net_raw 权限即可,不需要设置 SUID 位那么大的权限。

vagrant@stretch:~$ ls -ls /bin/ping
60 -rwsr-xr-x 1 root root 61240 Nov 10  2016 /bin/ping
vagrant@stretch:~$ cp /bin/ping anotherping
vagrant@stretch:~$ ls -ls anotherping 
60 -rwxr-xr-x 1 vagrant vagrant 61240 May 19 10:18 anotherping
vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com
ping: socket: Operation not permitted
vagrant@stretch:~$ sudo setcap cap_net_raw+ep ./anotherping 
vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com
PING yue.uu.163.com (59.111.137.252) 56(84) bytes of data.
64 bytes from 59.111.137.252 (59.111.137.252): icmp_seq=1 ttl=63 time=53.9 ms

--- yue.uu.163.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 53.919/53.919/53.919/0.000 ms
3.4 Union File System

UnionFS (联合文件系统)简单来说就是支持将不同的目录挂载到同一个目录中的技术。Docker 支持的 UnionFS 包 括OverlayFS,AUFS,devicemapper,vfs 以及 btrfs 等,查看 UnionFS 版本可以用 docker info 查看对应输出中的 Storage 项即可,早期的 Docker 版本用 AUFS 和 devicemapper 居多,新版本 Docker 在 Linux 3.18之后版本基本默认用 OverlayFS,这里以 OverlayFS 来分析。

OverlayFS 与早期用过的 AUFS 类似,不过它比 AUFS 更简单,读写性能更好,在 docker-ce18.03 版本中默认用的存储驱动是 overlay2,老版本 overlay 官方已经不推荐使用。它将两个目录 upperdir 和 lowdir 联合挂载到一个 merged 目录,提供统一视图。其中 upperdir 是可读写层,对容器修改写入在该目录中,它也会隐藏 lowerdir 中相同的文件。而 lowdir 是只读层, Docker 镜像在这层。

在看 Docker 镜像和容器存储结构前,可以先简单操作下 OverlayFS 看下基本概念。创建了 lowerdir 和 upperdir 两个目录,然后用 overlayfs 挂载到 merged 目录,这样在 merged 目录可以看到两个目录的所有文件 both.txt 和 only.txt。其中 upperdir 是可读写的,而 lowerdir 只读。通过 merged 目录来操作文件可以发现:

读取文件时,如果 upperdir 不存在该文件,则会从 lowerdir 直接读取。

修改文件时并不影响 lowerdir 中的文件,因为它是只读的。

如果修改的文件在 upperdir 不存在,则会从 lowerdir 拷贝到 upperdir,然后在 upperdir 里面修改该文件,并不影响 lowerdir 目录的文件。

删除文件则是将 upperdir 中将对应文件设置成了 c 类型,即字符设备类型来隐藏已经删除的文件(与 AUFS 创建一个 whiteout 文件略有不同)。

root@stretch:/home/vagrant/overlaytest# tree -a
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|-- upperdir
|   `-- both.txt
`-- workdir
    `-- work

5 directories, 3 files
root@stretch:/home/vagrant/overlaytest# mount -t overlay overlay -olowerdir=./lowerdir,upperdir=./upperdir,workdir=./workdir ./merged
root@stretch:/home/vagrant/overlaytest# tree
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|   |-- both.txt
|   `-- only.txt
|-- upperdir
|   `-- both.txt
`-- workdir
    `-- work

5 directories, 5 files
root@stretch:/home/vagrant/overlaytest# tree -a
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|   |-- both.txt
|   `-- only.txt
|-- upperdir
|   `-- both.txt
`-- workdir
    `-- work

5 directories, 5 files
root@stretch:/home/vagrant/overlaytest# echo "modified both" > merged/both.txt 
root@stretch:/home/vagrant/overlaytest# cat upperdir/both.txt 
modified both
root@stretch:/home/vagrant/overlaytest# cat lowerdir/both.txt 
lower both.txt
root@stretch:/home/vagrant/overlaytest# echo "modified only" > merged/only.txt 
root@stretch:/home/vagrant/overlaytest# tree
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|   |-- both.txt
|   `-- only.txt
|-- upperdir
|   |-- both.txt
|   `-- only.txt
`-- workdir
    `-- work

5 directories, 6 files
root@stretch:/home/vagrant/overlaytest# cat upperdir/only.txt 
modified only
root@stretch:/home/vagrant/overlaytest# cat lowerdir/only.txt 
lower only.txt
root@stretch:/home/vagrant/overlaytest# tree -a
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|   |-- both.txt
|   `-- only.txt
|-- upperdir
|   |-- both.txt
|   `-- only.txt
`-- workdir
    `-- work

5 directories, 6 files
root@stretch:/home/vagrant/overlaytest# rm merged/both.txt 
root@stretch:/home/vagrant/overlaytest# tree -a
.
|-- lowerdir
|   |-- both.txt
|   `-- only.txt
|-- merged
|   `-- only.txt
|-- upperdir
|   |-- both.txt
|   `-- only.txt
`-- workdir
    `-- work
root@stretch:/home/vagrant/overlaytest# ls -ls upperdir/both.txt 
0 c--------- 1 root root 0, 0 May 19 02:31 upperdir/both.txt

回到 Docker 里面,我们拉取一个 nginx 镜像,有三层镜像,可以看到在 overlay2 对应每一层都有个目录(注意,这个目录名跟镜像层名从 docker1.10 版本后名字已经不对应了),另外的 l 目录是指向镜像层的软链接。最底层存储的是基础镜像 debian/alpine,上一层是安装了 nginx 增加的可执行文件和配置文件,而最上层是链接 /dev/stdout 到 nginx 日志文件。而每个子目录下面的 diff 目录用于存储镜像内容,work 目录是 OverlayFS 内部使用的,而 link 文件存储的是该镜像层对应的短名称,lower 文件存储的是下一层的短名称。

root@stretch:/home/vagrant# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
f2aa67a397c4: Pull complete 
3c091c23e29d: Pull complete 
4a99993b8636: Pull complete 
Digest: sha256:0fb320e2a1b1620b4905facb3447e3d84ad36da0b2c8aa8fe3a5a81d1187b884
Status: Downloaded newer image for nginx:latest

root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/
total 16
4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0
4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554
4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe
4 drwx------ 2 root root 4096 May 19 04:17 l

root@stretch:/var/lib/docker/overlay2# ls  09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/
diff  link  lower  work

从我们示例可以看到,三层中 f311是最顶层,下面分别是0949和8af9这两层。

root@stretch:/var/lib/docker/overlay2# cat f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe/lower 
l/7B2WM6DC226TCJU6QHJ4ABKRI6:l/4FHO2G5SWWRIX44IFDHU62Z7X2
root@stretch:/var/lib/docker/overlay2# cat 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/lower 
l/4FHO2G5SWWRIX44IFDHU62Z7X2
root@stretch:/var/lib/docker/overlay2# cat 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554/link 
4FHO2G5SWWRIX44IFDHU62Z7X2

此时我们启动一个 nginx 容器,可以看到 overlay2 目录多了两个目录,多出来的就是容器层的目录和只读的容器 init 层。容器目录下面的 merged 就是我们前面提到的联合挂载目录了,而 lowdir 则是它下层目录。而容器 init 层用来存储与这个容器内环境相关的内容,如 /etc/hosts和/etc/resolv.conf 文件,它居于其他镜像层之上,容器层之下。

root@stretch:/var/lib/docker/overlay2# docker run -idt --name nginx nginx 
01a873eeba41f00a5a3deb083adf5ed892c55b4680fbc2f1880e282195d3087b

root@stretch:/var/lib/docker/overlay2# ls -ls
4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0
4 drwx------ 5 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1
4 drwx------ 4 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1-init
4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554
4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe
4 drwx------ 2 root root 4096 May 19 09:11 l

root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/
4 drwxr-xr-x 4 root root 4096 May 19 09:11 diff
4 -rw-r--r-- 1 root root   26 May 19 09:11 link
4 -rw-r--r-- 1 root root  115 May 19 09:11 lower
4 drwxr-xr-x 1 root root 4096 May 19 09:11 merged
4 drwx------ 3 root root 4096 May 19 09:11 work

root@stretch:/var/lib/docker/overlay2# ls  11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/merged/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

root@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/diff/
run  var

如果我们在容器中修改文件,则会反映到容器层的 merged 目录相关文件,容器层的 diff 目录相当于 upperdir,其他层是 lowerdir。如果之前容器层 diff 目录不存在该文件,则会拷贝该文件到 diff 目录并修改。读取文件时,如果 upperdir 目录找不到,则会直接从下层的镜像层中读取。

4 总结

随着版本不断更新,Docker 的一些技术细节也在变化,如镜像层存储目录的变化,默认 UnionFileSystem 换成 OverlayFS,新的 Namespace 的支持等。这篇文章主要对以前的学习笔记和 Docker 的一些新的变化做了些许总结,如想了解更详细内容,可以查看参考资料和 Docker 官方相关文档。

作者:__七把刀__
链接:https://www.jianshu.com/p/7a1...
來源:简书

更多相关阅读:
Docker 容器操作
Docker 的那点事儿
Docker 基础技术-Linux Namespace
docker-compose.yml 配置详解

如果你还想了解更多,想和技术同僚分享切磋,可扫下方二维码加好友,回复yw,加入掘金运维技术交流群

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/27364.html

相关文章

  • 应当了解Docker底层技术

    摘要:底层技术用于环境隔离,支持的包括以及新加入的等,用于隔离主机名和域名,使用标识,用于隔离进程间通信资源如消息队列等,使用标识,隔离进程,用于隔离网络,用于隔离挂载点,用于隔离用户组。 本文已获得原作者__七把刀__授权。 Docker 容器技术已经发展了好些年,在很多项目都有应用,线上运行也很稳定。整理了部分 Docker 的学习笔记以及新版本特性,对Docker感兴趣的同学可以看看,...

    MiracleWong 评论0 收藏0
  • 高效编写Dockerfile几条准则

    摘要:本文已获得原作者授权。在构建镜像的过程中会缓存一系列中间镜像。镜像时,会顺序执行中的指令,并同时比较当前指令和其基础镜像的所有子镜像,若发现有一个子镜像也是由相同的指令生成,则命中缓存,同时可以直接使用该子镜像而避免再去重新生成了。 本文已获得原作者 CodeSheep 授权。 概述 Dockerfile 是专门用来进行自动化构建镜像的编排文件(就像 Jenkins 2.0时代的 J...

    RyanQ 评论0 收藏0
  • 技术干货 | Docker容器中需要避免十种常见误区

    摘要:第二具备轻量化特性容器的体积非常小巧。他们大多认为自己应该将应用程序部署至当前正在运行的容器当中。不要创建大型镜像体积过大的镜像会加大其发布难度。总体来讲,在向生产环境中部署容器时,必须避免使用最新标签。 当下最火爆的Docker,是一个开源的应用容器引擎。大家已经开始认同并接受容器技术,并意识到它能够解决多种现实问题并具备一系列无可比拟的优势。今天小数就和大家聊一聊容器技术的优势和误...

    Gu_Yan 评论0 收藏0
  • 猫头鹰深夜翻译:持久化容器存储

    摘要:如果我们的容器使用,文件如下在这个例子中,我们可以重复创建和销毁,同一个持久存储会被提供给新的,无论容器位于哪个节点上。 前言 临时性存储是容器的一个很大的买点。根据一个镜像启动容器,随意变更,然后停止变更重启一个容器。你看,一个全新的文件系统又诞生了。 在docker的语境下: # docker run -it centos [root@d42876f95c6a /]# echo H...

    tianhang 评论0 收藏0
  • 猫头鹰深夜翻译:持久化容器存储

    摘要:如果我们的容器使用,文件如下在这个例子中,我们可以重复创建和销毁,同一个持久存储会被提供给新的,无论容器位于哪个节点上。 前言 临时性存储是容器的一个很大的买点。根据一个镜像启动容器,随意变更,然后停止变更重启一个容器。你看,一个全新的文件系统又诞生了。 在docker的语境下: # docker run -it centos [root@d42876f95c6a /]# echo H...

    xiao7cn 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<