GodsLeft 2014-12-29
我想我已经找到了一个相当引人注目的docker使用案例。但在此之前,如果你还是认为这又是一个人云亦云docker美德的博客帖子的话,我想明确指出,这个帖子确实是关于把文件系统作为持久性数据结构的赞美帖。
因此,这篇文章的见解同样适用于其他的 copy-on-write文件系统,如BTRFS和ZFS。
问题
让我们从这个我试图解决的问题开始。我开发了包括了众多的步骤的长时运行的构建脚本。
花费1-2个小时运行。
它从互联网下载了很多相当大的文件。(超过300M)。
后期严重依赖早期构建的库。
但最显著的特点是,它需要花很长的时间来运行。
文件系统是固有状态
我们通常是以一种状态的方式与文件系统进行交互的。我们可以添加,删除或移动文件。我们可以改变文件的权限或者它的访问时间。
隔离下的大部分操作都可以撤销。例如你可以移动文件到其其他的地方后,将文件恢复到原来的位置。通常我们不会做的是采取一个快照,并恢复到那个状态。这篇文章建议更多地利用这一特性对开发长时运行脚本有巨大好处。
使用联合文件系统的快照
Docker采用的是所谓的联合文件系统叫做AUFS。联合文件系统实现了被称为联合挂载的文件系统。顾名思义,这意味着文件和独立的文件系统的目录被分层于互相形成的单个连贯文件系统之上。
这是以分层方式完成的。如果一个文件出现在两个文件系统,后来添加的文件将会呈现 (该文件其他版本是存在于层级中的,不改变,只是看不到的)。
Docker称呼在联合挂载文件系统里的每个文件系统为layers(层)。使用这种技术的结果是,它的副作用可以实现快照。每个快照对于所有层是一个简单的联合挂载文件系统挂载到某个层次结构中。
生成脚本的快照
快 照使开发一个长时运行的构建脚本成为梦想。总的想法是,分解大脚本为更小的脚本(我喜欢称之为scriptlets)并且单独地运行每一个,每一个运行后 快照其文件系统。 (Docker会自动执行此操作。)如果你发现一个scriptlet运行失败,简单的可以回到最后的快照(仍处于原始状态!),然后再试一次。
一旦你完成你的构建脚本,你可以保证,脚本正常工作,现在可以分配给其他主机。
相对于如果你没有使用快照会发生什么。除了在我们中间那些有和尚般的耐心的人,当它在1个半小时后失败了,没有人会去从头开始运行他们的构建脚本。当然,我们会尽最大努力把系统恢复到失败前的状态。例如我们可以删除一个目录或运行 make clean。
但是,我们可能没有真正地理解我们正在构建的组件。它可能复杂的Makefile:把文件放到文件系统中我们不知道的地方。唯一真正确定的途径是恢复到快照。
使用快照构建脚本的docker
在 本节中,我将介绍我是如何使用Docker实现GHC7.8.3 ARM交叉编译器的构建脚本。对于这个任务Docker相当不错的,但并不是完美的。我做了一些事情,可能看起来浪费的或不雅的,但都是必要的,以保持开 发脚本的总时间到最低限度。构建脚本可以在这里找到。
用Dockerfile构建
Docker 读取一个名为Dockerfile来构建镜像。Dockerfile包含一些命令词汇来具体指定哪些行动应该被执行。一个完整的参考可以在这里找到。其中 在我的脚本主要用了WORKDIR,ADD和RUN。ADD命令非常有用因为它可以让你在运行之前将外部文件添加到当前Docker镜像中然后转换成镜像 的文件系统。你可以在这里看到很多scriptlets构成的构建脚本。
设计
1.在RUN之前ADD scriptlets
如 果你太早ADD所有的scriptlets在Dockerfile,您可能会遇到以下问题:你的脚本失败,你回去修改scriptlet并再次运行 docker build .。但是你发现,Docker开始在首次加入scriptlets的地方构建!这会浪费了大量的时间和违背了使用快照的目的。
出 现这种情况的原因是因为Docker如何追踪它的中间镜像(快照)。当Docker通过Dockerfile构建镜像时它会与中间镜像比较当前命令是否一 致。然而,在ADD命令的情况下被装进镜像的文件里的内容也会被检查。这是有道理的。如果文件已改变就现有的中间镜像那么Docker将别无选择,只能从 从这点开始建立一个新的镜像。只是没有办法可以知道这些变化不会影响到构建。这是必须要保守的即使他们没有。
此外,使用RUN命令 要注意,每次运行时它将导致文件系统有不同的更改。在这种情况下,Docker会发现中间镜像并使用它,但是这将是错误的。RUN命令每次运行时必须造成 文件系统相同的改变。举个例子,我确保在我的scriptlets我总是下载了一个已知版本的文件与一个特定MD5校验。
对Docker 构建缓存更详细的解释可以在这里找到。
2.不要使用ENV命令来设置环境变量。使用scriptlet。
它似乎看起来很有诱惑力:使用ENV命令来设置所有构建脚本需要的环境变量。但是,它不支持变量替换的方式,例如 ENV BASE=$HOME/base 将设置BASE的值为$HOME/base着很可能不是你想要的。
相反,我用ADD命令添加一个名为set-env.sh文件。此文件被包含在每个后续的scriptlet中:
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source $THIS_DIR/set-env-1.sh
如果你没有在第一时间获取set-env.sh会怎么样呢?自从它很早就被加入Dockerfile并不意味着修改它将会使随后的快照无效?
是的,这将导致一些不雅。在开发脚本时,我发现,我已经错过了在set-env.sh添加一个有用的环境变量。解决方案是创建一个新的文件set-env-1.sh包含:
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source $THIS_DIR/set-env.sh if ! [ -e "$CONFIG_SUB_SRC/config.sub" ] ; then CONFIG_SUB_SRC=${CONFIG_SUB_SRC:-$NCURSES_SRC} fi
然后,在所有后续的scriptlets文件中包含了此文件。现在,我已经完成了构建脚本,我可以回去解决这个问题了,但是,在某种意义上,它会破坏最初的目标。我将不得不从头开始运行构建脚本看看这种变化是否能成功。
缺点
一个主要缺点是这种方法是,所构建的镜像尺寸是大于它实际需求的尺寸。在我的情况下尤其如此,因为我在最后删除了大量文件的。然而,这些文件都仍然存在于联合挂载文件系统的底层文件系统内,所以整个镜像是大于它实际需要的大小至少多余的是删除文件的大小。
然而,有一个变通。我没有公布此镜像到Docker Hub Registry。相反,我:
来产生尺寸尽可能小的镜像。
结论
这种方法的优点是双重的:
此外,正如我前面提到的Docker不仅使写这些构建脚本更加容易。有了合适的工具同样可以在任何提供快照的文件系统实现。