众所周知,Docker 始于2013年的 dotCloud,迄今刚刚七年,若是你恰好在圈中经历了2013-2015年这段早期岁月的话,天然应该知道,最初的 Docker = LXC + aufs,前者就是所谓的 Linux 容器了,然后者则是我今天要聊的镜像。node
说到 Linux distro,除了作差别化的界面主题以外,核心差别通常都在于:python
而在 distro 界却有一股清流,超脱于这两件事情以外,它们就是 Live CD,它们装在一张光盘里,或者是一个 U盘上,不须要安装、也不会改变。以前创业的时候,我司的运维大佬——彤哥曾经说过:linux
第一次见到 liveCD 时我心里是震惊的。。git
这我固然是赞同的,那时我也是震惊的同窗之一,要知道 Knoppix 在 2000 千禧年就来到了世界,而它所基于的著名的 Debian,直到2005年6月,Sarge (3.1) 发布的时候才正式在 stable release 里带上了图形界面的安装程序 debian-installer (简称 d-i),此前版本的安装还在用文本菜单。就在这个年代,这样一个开光盘即用、启动起来就是图形界面的系统,给咱们这些玩家带来的震撼,固然是可想而知的。那时候的 Live CD 就是十三年后的 Docker,绝对配得上“惊艳”两个字。github
要知道,一张 700MB 左右的光盘里塞下个完整的操做系统并不容易(固然有人开头以后也不太难,后来我爱的 DSL 能够作到 50MB)。Knoppix 有个很棒的想法——把装好的操做系统给压缩一下,放在光盘里, 随用随解压,这样,一张 700MB 光盘里能够放下大约 2GB 的根文件系统,这样就跑 KDE 桌面也就没啥问题了,当是时,distrowatch.com 上能够看到,一大片 distro 都是基于 Knoppix 魔改的,足见其影响力。golang
Knoppix 在诞生之初的一个执念是“毫不碰本地存储一根指头”,而光盘,CD-ROM,所使用的 ISO9600 文件系统也是只读的,这无疑暗合了当今流行的“不可变基础设施”的潮流,可是,即便在今天,没有可写文件系统对于不少 Linux 软件仍然是很是困难的,毕竟随随便便一个程序也要写一点配置文件、状态信息、锁、日志之类的嘛。而诞生之初的 Knoppix 是不可写的,那么,要有什么东西要罗盘,就得手工挖掘出本地硬盘来挂上,或者是挂个 NAS 到 /home 或其余挂载点上来,当你不想只是作个紧急启动盘的时候,这就有点麻烦了。算法
若是咱们从今天穿越回去,绝不费力就能够指出,用 overlayfs 加上一个 tmpfs 作可写层嘛。可是,overlayfs 要到2010年才首次提交 patchset,2014年才被合并到 3.18内核(这中间,当时的淘宝内核组也有很多贡献和踩坑呢)。固然,比 overlay 早的相似的 unionfs 仍是有的,Docker 最先采用的 Aufs 就是其中之一,它是2006年出现的,这里 AUFS 的 A,能够理解成 Advanced,但它最先的意思实际是 Another——是的,“另外一个 UFS”,而它的前身就是 UnionFS。sql
在2005年5月,也就是十五年前,Knoppix 创造性地引入了 UnionFS,而在一年半之后的 5.1 版本中,Knoppix 引入了当年诞生的更稳定的 aufs,此后,包括你们熟悉的 Ubuntu LiveCD、Gentoo LiveCD 全都使用了 aufs。能够说,正是 Live CD 们,提早了8年,为 Docker 和 Docker Image 的诞生,作好了存储上的准备。docker
这里简单说一句给不了解的人听,所谓 union fs,是指多个不一样文件系统联合(堆叠)在一块儿,呈现为一个文件系统,它和通常的 FHS 规定的那种树装组织方式是不一样的,以下图,对于左边的标准的目录树结构,任何一个文件系统,挂载在树上的一个挂载点,根据路径,就能够指到一个肯定的文件系统,好比,下图中,全部的 /usr/local/
下面的路径都在一个文件系统上,而其余 /usr
就会在另外一个文件系统上;而 UnionFS 是多层堆叠的,你写的文件会停留在最上层,好比图中,你修改过的 /etc/passwd
就会在最上的可写层,其余的文件就要去下面的层中去找,也就是说,它容许同一个目录中的不一样文件在不一样层中,这样,Live CD 操做系统跑起来就像真正的本地操做系统同样能够读写全部路径了。api
让咱们把目光放在只读层上,这一层是 Live CD 的基础,在 Live CD 尚未 union FS 来作分层的时候就已经存在这个只读 rootfs 了。对 Knoppix 来讲,这一层是不能直接放完整、无压缩的操做系统的,由于在21世纪初,你们都还在用 24x 到 40x 速光驱的时代,Knoppix Live CD 面临的一个大问题是 700MB 的光盘和庞大的桌面操做系统之间的矛盾。
开头咱们提到了,Knoppix 的想法就是“把装好的操做系统给压缩一下,放在光盘里, 随用随解压”,这样精心选择后的 2GB 的 Distro 就能够压到一张光盘里了,不过“随用随解压“不是说有就有的,文件系统访问块设备,是要找到块的偏移量的,压缩了以后,这个偏移量就并不那么好找了,全解压到内存里再找偏移并不那么容易。
回到2000年,那时候仍是2.2内核,Knoppix 的做者 Klaus Knopper 在创立 Knoppix 之初就引入了一种压缩的(compressed) loop 设备,称为 cloop,这种设备是一种特殊的格式,它包含了一个索引,从而让解压缩的过程对用户来讲是透明的,Knoppix 的 cloop 设备看起来就是一个大约 2GB 大的块设备,当应用读写一个偏移的数据的时候,它只须要根据索引解压缩对应的数据块,并返回数据给用户就能够了,无需全盘解压缩。
尽管 Knoppix 把众多 distro 带上了 Live CD 的船,可是,众多后继者,诸如 arch、Debian、Fedora、Gentoo、Ubuntu 等等 distro 的 LiveCD,以及你们熟悉的路由器上玩的 OpenWrt,都并无选择 cloop 文件,它们选择了和应用语义更接近的文件系统级的解决方案——Squashfs。Squashfs 压缩了文件、inode 和目录,并支持从 4K 到 1M 的压缩单元尺寸。一样,它也是根据索引按需解压的,和 cloop 的不一样之处是,当用户访问一个文件的时候,它来根据索引解压相应的文件所在的块,而非再通过一层本地文件系统到压缩块的寻址,更加简单直接。事实上,Knoppix 里也一直有呼声想要切换到 squashfs,好比,2004年就有开发者在转换 knoppix 到squashfs 上,并且,一些测试数据彷佛也代表 Squashfs 的性能彷佛要更好一些,尤为在元数据操做方面。
The design of the cloop driver requires that compressed blocks be read whole from disk. This makes cloop access inherently slower when there are many scattered reads, which can happen if the system is low on memory or when a large program with many shared libraries is starting. A big issue is the seek time for CD-ROM drives (~80 ms), which exceeds that of hard disks (~10 ms) by a large factor. On the other hand, because files are packed together, reading a compressed block may thus bring in more than one file into the cache. The effects of tail packing are known to improve seek times (cf. reiserfs, btrfs), especially for small files. Some performance tests related to cloop have been conducted.
我来多此一举地翻译一下:
cloop 的设计要求从磁盘上以压缩块为单位读取数据。这样,当有不少随机的读操做时,cloop 就会显著地变慢,当系统内存不足或者一个有不少共享库的大型程序启动的时候都极可能发生。cloop 面临的一个很大的问题是 CD-ROM 的寻道时间(约80毫秒),这在很大程度上超过了硬盘的查找时间(约10毫秒)。另外一方面,因为文件能够被打包在一块儿,所以读取压缩块实际可能能够将多个文件带入缓存。这样,那些支持 tail packing 的文件系统(好比 reiserfs,btrfs)可能能够显著改善 seek 操做的时间,尤为是对于小文件更是如此。已经有一些与 cloop 相关的性能测试也证实了这些观点。
固然,尽管有这些争论,cloop 也仍然在 Knoppix 上存在,不过,这个争论最终随着2009年 squashfs 被并入 2.6.29 主线内核,应该算是分出胜负了,进入 kernel 带来的开箱即用换来的是压倒性的占有率和更好的支持,Squashfs 的优点不只在上述的 distro 用户之多,也在于支持了了各类的压缩算法,只用于不一样的场景。
斗转星移,再也不年轻的 Live CD 也由于如此普及,而让人以为并不新奇了。可是,技术圈也有轮回通常,当年被 Live CD 带红的 Union FS 们再一次被 Docker 捧红,焕发了第二春。通常地说,虽然 aufs 们支持多个只读层,但普通的 Live CD 只要一个只读镜像和一个可写层留给用户就能够了。然而, 以 Solomon 为首的 dotCloud 的朋友们充分发挥了他们卓越的想象力,把整个文件系统变成了“软件包”的基本单位,从而作到了 #MUGA (Make Unionfs Great Again)。
回想一下,从1993年的 Slackware 到今天的 RHEL,(服务端的)Distro 的所做所为,不外乎我开头提到的两件事情——安装和升级。从 rpm 到 APT/deb 再到 Snappy,初始化好系统以后的工做的精髓就在于怎么更平滑地安装和升级、确保没有依赖问题,又不额外占据太多的空间。解决这个问题的基础就是 rpm/deb 这样的包以及包之间的依赖关系,然而,相似“A 依赖 B 和 C,但 B 却和 C 冲突” 这样的事情仍然层出不穷,让人们不停地尝试解决了二十年。
但 Docker 跳出了软件包这个思路,他们是这么看的——
大概的示意图是这样的:
这样,若是在同一台机器上跑这三个应用(容器),那么这些共享的只读层都不须要重复下载。
更进一步说,Docker 这种分层结构的另外一个优势在于,它自己是很是开发者友好的,能够看到,下面这个是一个 Dockerfile 的示意,FROM 表明最底下的基础层,以后的 RUN, ADD 这样改变 rootfs 的操做,都会将结果存成一个新的中间层,最终造成一个镜像。这样,开发者对于软件依赖性的组织能够很是清晰地展示在镜像的分层关系中,好比下面这个 Dockerfile 是一个 packaging 用的 image,它先装了软件的依赖包、语言环境,而后初始化了打包操做的用户环境,而后拉源代码,最后把制做软件包的脚本放到镜像里。这个组织方式是从通用到特定任务的组织方式,镜像制做者但愿让这些层能够尽可能通用一些,底层的内容能够在其余镜像中也用得上,而上层则是和本镜像的工做最直接相关的内容,其余开发者在看到这个 Dockerfile 的时候已经能够基本知道这个镜像里有什么、要干什么,以及本身是否能够借鉴了。这个镜像的设计是 Docker 设计里最巧妙的地方之一,也是为何你们都愿意认同,Solomon 要作的就是开发者体验(DX, Developer Experiences)。
FROM debian:jessie
MAINTAINER Hyper Developers <dev@hyper.sh>
RUN apt-get update &&\ apt-get install -y autoconf automake pkg-config dh-make cpio git \ libdevmapper-dev libsqlite3-dev libvirt-dev python-pip && \ pip install awscli && \ apt-get clean && rm -fr /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN curl -sL https://storage.googleapis.com/golang/go1.8.linux-amd64.tar.gz | tar -C /usr/local -zxf -
RUN useradd makedeb && mkdir -p ~makedeb/.aws && chown -R makedeb.makedeb ~makedeb && chmod og-rw ~makedeb/.aws RUN mkdir -p /hypersrc/hyperd/../hyperstart &&\ cd /hypersrc/hyperd && git init && git remote add origin https://github.com/hyperhq/hyperd.git && \ cd /hypersrc/hyperstart && git init && git remote add origin https://github.com/hyperhq/hyperstart.git && \ chown makedeb.makedeb -R /hypersrc
ENV PATH /usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV USER makedeb
ADD entrypoint.sh /
USER makedeb
WORKDIR /hypersrc ENTRYPOINT ["/entrypoint.sh"] 复制代码
一个规范的 Docker Image 或者脱胎于其中的 OCI Image,实际上就是一组元数据和一些层数据,这些层数据每一个都是一个文件系统内容的打包,从某种意义上说,典型的 Live CD 的 OS,基本上能够理解成一个只读层加上一个可写层的 Docker Container 的 rootfs。在 Docker 这里,Union FS 能够说是 Great Again 了。
然而,尽管 Docker Image (或者说 OCI Image)的设计蕴含了“完整的操做系统就是一个包”的优秀思想,又利用 Union FS 实现了“分层”这种既实现漂亮的开发者体验,又能节约时间空间的精巧设计,但随着时间的推移,仍是暴露出来了一些问题。从去年(2019年)年初,OCI 社区中开始有人讨论下一代镜像格式的问题,这个热烈的讨论中,集中讨论了 OCIv1(实际也是 Docker 的)镜像格式的一些问题,Aleksa Sarai 也专门写了一篇博客来讨论这个话题,具体说,除了 tar 格式自己的标准化问题外,你们对当前的镜像的主要不满集中在:
上述这些问题用一句话来总结就是“层是镜像的基本单位”,然而,镜像的数据的实际使用率是很低的,好比 Cern 的这篇论文中就提到,通常镜像只有**6%**的内容会被实际用到,这就产生了实质性的升级镜像数据结构,再也不以层为基本单位的动力。
可见,下一代镜像的一个趋势就是打破层的结构来对这些只读进行更进一步的优化,是的,反应快的同窗可能已经回想起了前面提到的 Live CD 里经常使用的 Squashfs,它能够根据读取的须要来解压相应的文件块来放入内存、供应用使用,这里是否是能够扩展一下,变成在须要的时候再去远端拉回镜像的内容来供应用使用呢——从文件的 Lazy decompress 到 Lazy Load,一步之遥,水到渠城。
是的,蚂蚁的镜像加速实践里就采起了这样的架构。在过去,庞大的镜像不只让拉取过程变慢,并且若是这一过程一样风险重重,贡献了大半的 Pod 启动失败率,而今天,当咱们把延迟加载的 rootfs 引入进来的时候,这些失败几乎被彻底消灭掉了。在去年年底的第10届中国开源黑客松里,咱们也演示了经过 virtio-fs 把这套系统对接到 Kata Containers 安全容器里的实现。
如图,相似 Squashfs,这种新的 Image 格式中,压缩数据块是基本单位,一个文件能够对应0到多个数据块,在数据块以外,引入了一些附加的元数据来作目录树到数据块的映射关系,从而能够在没有下载完整镜像数据的时候也给应用呈现完整的文件系统结构,并在发生具体读取的时候,根据索引,去获取相应的数据来提供给应用使用。这个镜像系统能够带来这些好处:
并且,这套系统在设计之初,咱们就发现,由于咱们能够获取到应用文件数据访问模式的,而基于文件的一个好处是,即便镜像升级了,它的数据访问模式也是倾向于不太变化的,因此,咱们能够利用应用文件数据的访问模式作一些文件预读之类的针对性操做。
能够看到,系统存储这个领域二十年来发生了一个螺旋式的进化,发生在 Live CD 上的进化,在容器这里也又来了一次,恍如隔世。目前,咱们正在积极地参与 OCI Image v2 的标准推进,也正在把咱们的参考实现和 DragonFly P2P 分发结合在一块儿,并成为 CNCF 的开源项目 Dragonfly 的一部分,但愿在将来能够和 OCI 社区进一步互动,让咱们的需求和优点成为社区规范的一部分,也让咱们能够和社区保持一致、可平滑过渡,将来能够统一在 OCI-Image-v2 镜像之下。
王旭,蚂蚁金服资深技术专家,也是开源项目 Kata Containers 的架构委员会创始成员,在过去几年中活跃在国内的开源开发社区与标准化工做中。在加入蚂蚁金服以前,他是音速神童的联合创始人和 CTO,他们在 2015 年开源了基于虚拟化技术的容器引擎 runV,在 2017 年 12 月,他们和 Intel 一块儿宣布 runV 与 Clear Containers 项目合并,成为 Kata Containers 项目,该项目于 2019 年 4 月被董事会经过成为了 OpenStack 基金会 2012 年以来的首个新开放基础设施顶级项目。在创立音速神童以前,王旭曾工做于盛大云计算和中国移动研究院的云计算团队。2011 年王旭曾经主持过杭州 QCon 的云计算主题,同时,也曾经是一位活跃的技术做者、译者和老 blogger。