本文已得到原做者__七把刀__受权。node
Docker 容器技术已经发展了好些年,在不少项目都有应用,线上运行也很稳定。整理了部分 Docker 的学习笔记以及新版本特性,对Docker感兴趣的同窗能够看看,以前整理过的 Linux namespace 能够见以前的博文。
Container (容器)是一种轻量级的虚拟化技术,它不须要模拟硬件建立虚拟机。在 Linux 系统里面,使用到 Linux kernel 的 cgroups,namespace(ipc,network, user,pid,mount),capability 等用于隔离运行环境和资源限制的技术,咱们称之为容器。容器技术早就出现。例如 Solaris Zones 和 BSD jails 就是非 Linux 操做系统上的容器,而用于 Linux 的容器技术也有不少如 Linux-Vserver、OpenVZ 和 FreeVPS。虽然这些技术都已经成熟,可是这些解决方案尚未将它们的容器支持集成到主流 Linux 内核。总的来讲,容器不等同于 Docker,容器更不是虚拟机。linux
LXC 项目由一个 Linux 内核补丁和一些 userspace 工具组成,它提供一套简化的工具来维护容器,用于虚拟环境的环境隔离、资源限制以及权限控制。LXC 有点相似 chroot,可是它比 chroot 提供了更多的隔离性。nginx
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。git
Docker 提供了一个打包和运行应用的隔离环境,称之为容器,Docker 的隔离和安全特性容许你在一个主机同时运行多个容器,并且它并不像虚拟机那样重量级,容器都是基于宿主机的内核运行的,它是轻量的,无论你运行的是ubuntu, debian 仍是其余 Linux 系统,用的内核都是宿主机内核。Docker 提供了工具和平台来管理容器,而 Docker Engine 则是一个提供了大部分功能组件的CS架构的应用,如架构图所示,Docker Engine 负责管理镜像,容器,网络以及数据卷等。github
Docker 更详细的架构如图所示,采用CS架构,client 经过 RESTFUL API 发送 docker 命令到 docker daemon 进程,docker daemon 进程执行镜像编译,容器启停以及分发,数据卷管理等,一个 client 能够与多个 docker daemon 通讯。docker
经过下面命令运行一个 debian 容器,attach 到一个本机的命令行并运行/bin/bash。ubuntu
docker run -i -t debian /bin/bash
这个命令背后都作了什么?安全
docker pull debian
效果同样。docker create
同样。-i -t
参数,容器是以交互模式运行且 attach 到本地终端,咱们能够在终端上输入命令并看到输出。能够发现,容器的内核版本是跟宿主机同样的,不一样的是容器的主机名是独立的,它默认用容器 ID 作主机名。咱们运行ps -ef
能够发现容器进程是隔离的,容器里面看不到宿主机的进程,并且它本身有 PID 为1的进程。此外,网络也是隔离的,它有独立于宿主机的 IP。文件系统也是隔离的,容器有本身的系统和软件目录,修改容器内的文件并不影响宿主机对应目录的文件。bash
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: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> 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 以及一些已有的技术实现的,主要包括:微信
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 为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 的挂载点互不影响。建立一个新的 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_SLAVE
和 NS_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 容器中看到的一致。
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 用于隔离用户和组信息,在不一样的 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
不是全部的进程都能随便修改映射文件的,必须同时具有以下条件:
CAP_SETUID/CAP_SETGID
权限。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
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,这里就再也不展开。
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_us
和 cfs_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
咱们在启动容器时会时常看到这样的参数 --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
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 目录来操做文件能够发现:
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 目录找不到,则会直接从下层的镜像层中读取。
随着版本不断更新,Docker 的一些技术细节也在变化,如镜像层存储目录的变化,默认 UnionFileSystem 换成 OverlayFS,新的 Namespace 的支持等。这篇文章主要对之前的学习笔记和 Docker 的一些新的变化作了些许总结,如想了解更详细内容,能够查看参考资料和 Docker 官方相关文档。
做者:__七把刀__
连接:https://www.jianshu.com/p/7a1...
來源:简书
更多相关阅读:
Docker 容器操做
Docker 的那点事儿
Docker 基础技术-Linux Namespace
docker-compose.yml 配置详解
若是你还想了解更多,想和技术同僚分享切磋,可扫下方二维码加好友,回复yw,加入掘金运维技术交流群