博主一直都很喜欢思考怎样管理装在自己电脑上的桌面系统,这篇算是前作 能当主力,能入虚拟机,还能随时打包带走,Linux 就是这么强大 的后续探索吧。
近些年来,Docker 由于提供了一套非常方便地创建并运行应用容器的方法,而在全球掀起了一股容器化的热潮。容器通过将软件及其所需要的运行环境一同打包带走,从而将人们从依赖的苦海中拯救出来。虽然 Docker 设计的初衷并不是操作系统容器,更不是一个直接运行在裸机上的操作系统,但是 docker 这套强大的工具也会给我们管理操作系统带来巨大的便利。
为什么要用 Docker 镜像当作桌面系统?这就要从普通桌面系统的不方便之处说起。通常我们都拥有不止一台电脑,我们希望这些电脑能够保持一致。这里所说的 "一致",用一个例子来讲,就是我在一台电脑上编辑了一半的文件,不需要认为拷贝到另一台电脑上,而是直接打开电脑就能编辑。如果这个文件只是一个纯文本文件,或者一个 Microsoft Word 文档,那么实现这个一致性非常简单:把文件扔到 Dropbox 之类的云同步盘就好。然而对于专业用户来讲,这种一致性的保持并非单纯的扔到 Dropbox 里面那么简单:比如说你最近忙于一个项目,这个项目要用到若干编程语言,然后在电脑里装了一堆库,一堆工具软件,有图形界面的,也有命令行的。在工作的过程中,你有可能不断安装新的工具,或者决定弃用某个之前计划使用的库或者工具。要让你的工作在你的若干台电脑上都能工作,就要一直维护不同机器的环境的一致性:在一台机器上安装的工具,要在所有机器上重新安装一遍。在一台机器上升级了的库,要在所有机器上都升级,稍微有所差池,就有可能出现某个脚本 / 程序在一台机器上跑的好好的,在另一台机器上却无法运行的问题。
不熟悉 docker 的读者可以戳 这里 来了解 docker。Docker 的使用非常简单:我们通过写一个 Dockerfile,在 Dockerfile 中写入相应的命令来安装以及配置我们想要的库跟工具。不熟悉 docker 的读者可以看一下下面这个 抄来的 Dockerfile 的例子 ,来了解一下 Dockerfile 长啥样子:
- FROM ubuntu
- MAINTAINER Kimbro Staken
- RUN apt-get install -y software-properties-common python
- RUN add-apt-repository ppa:chris-lea/node.js
- RUN echo "deb http://us.archive.ubuntu.com/ubuntu/ precise universe" >> /etc/apt/sources.list
- RUN apt-get update
- RUN apt-get install -y nodejs
- #RUN apt-get install -y nodejs=0.6.12~dfsg1-1ubuntu1
- RUN mkdir /var/www
- ADD app.js /var/www/app.js
- CMD ["/usr/bin/node", "/var/www/app.js"]
1 2 3 4 5 6 7 8 9 10 11 |
FROM ubuntu MAINTAINER Kimbro Staken RUN apt-get install -y software-properties-common python RUN add-apt-repository ppa:chris-lea/node.js RUN echo "deb http://us.archive.ubuntu.com/ubuntu/ precise universe" >> /etc/apt/sources.list RUN apt-get update RUN apt-get install -y nodejs #RUN apt-get install -y nodejs=0.6.12~dfsg1-1ubuntu1 RUN mkdir /var/www ADD app.js /var/www/app.js CMD ["/usr/bin/node", "/var/www/app.js"] |
有了 Dockerfile,只需要 docker build 一条命令就可以创建一个 docker 镜像。同时 Docker 公司提供一个叫做 DockerHub 的服务,可以免费托管公开镜像。只需要使用 docker push 就可以直接把镜像上传到 DockerHub。在不同的电脑上只需要 docker pull 就可以从 DockerHub 获取最新版的镜像。DockerHub 还支持自动构建,通过把 DockerHub 帐号跟 GitHub 帐号关联起来,就可以让 DockerHub 在 GitHub 上面的 Dockerfile 出现更改的时候自动重新生成镜像。
本文开头所说的这种一致性的维护,docker 实际上已经在给我们提供答案了:我们通过构建一个 docker 镜像,让这个镜像包含着我们项目所需要的所有的一切。这样的话,我们开发,测试,部署等等一切任务,都可以在先用 docker run 来开启一个容器,然后在容里面进行所有的工作。当我们决定修改运行环境,比如引入新的库的时候,就在 Dockerfile 中进行相应的修改,重新生成镜像,然后在不同的机器上用 docker pull 来更新一下就好。这种使用哲学,通过一个中心化了的仓库,非常优雅地解决了不同机器上环境一致的问题。美中不足的是,并不是所有的程序都能在容器里运行的,也并不是所有的程序都方便在容器里运行的。如果你用到了图形界面的程序,或者说是一些系统级别的程序,那么在容器里面使用这些程序会麻烦很多,有的甚至根本无法实现。于是自然地就会想到,如果我们能够在每次开机的时候,直接把某个 docker 生成的镜像挂载起来当根目录来使用,就可以让这个镜像直接在裸机上(而不是在容器中)运行,来做我们的日常桌面系统了。
这种做法,除了在保持一致性方面带来的便利以外,还有一些其他的好处:
Docker 的存储驱动 官方有介绍其工作原理 ,这里只是简单概括一下。Docker 使用了层的概念,docker 在构建镜像的时候,会逐行执行我们的 Dockerfile 中的每一行,每执行一行的时候,docker 就会创建出一个新的层来存放新的内容。当我们执行 docker pull 或者 docker push 的时候,docker 实际上传跟下载的是这些层之间的增量。每当执行 docker run,docker 就会把这些下载下来的层组合到一起,组合成一个完整的镜像,然后新建一个读写层,所有运行过程中的写入都会被写入到读写层中,而镜像本身则是保持只读,不会被更改。"层" 这个概念具体实现起来,根据 docker 目录(通常为 / var/lib/docker 这个目录)所在的文件系统的不同而不同,具体的实现在 docker 中被称为 graph driver,docker 自带的 graph driver 包括 aufs、 overlay、btrfs、zfs、devicemapper 等。这些 graph driver 大多使用了写时复制的技术,这样在把各个层组合在一起的过程不需要重新拷贝一份数据,实际的拷贝是在写入的时候发生的。
由于笔者使用的是 btrfs,所以本文就以 btrfs 为例子来介绍怎么让系统启动到 docker 镜像上去。btrfs 是一个写时复制的系统,由于 docker 的镜像是由一个一个的层叠在一起组成的,docker 在使用 btrfs 的时候,每往上叠一层,docker 就会创建一个原来层的快照,然后把新层的内容写到快照里面去。然后 docker 会在从镜像创建容器的时候,给镜像的最顶层做个快照,把这个快照当作容器读写层来用。
明白了 docker 存储驱动的工作原理,还需要知道 Linux 的启动过程才能达成我们的目标。Linux 在启动的时候,一般会让启动器给内核装载一个内存盘 initramfs,然后内核完成简单的早期初始化以后,就会解压内存盘的内容到根目录 /,然后启动内存盘中的 init 程序(一般为 / init),这个 init 程序会进行进一步的初始化(比如说加载文件系统的驱动,对文件系统进行 fsck 等),这一步初始化完成了以后,这个 init 程序就会根据内核选项中的 root、rootflags 等内容挂载真正的根目录,然后通过 switch_root 程序启动真正根目录中的 init 程序,这个 init 程序则会完成最后的初始化工作,比如挂载 fstab、加载图形界面等等。很多发行版都提供制作 initramfs 的工具,比如 archlinux 的 mkinitcpio,这些工具通常都是模块化的,允许用户自己添加 hook。
让系统启动到 docker 镜像所需要的知识已经完备了。思路也清晰了:通过给 initramfs 中添加 hook,让 initramfs 中的 init 在挂载 root 之前从 docker 本地缓存中的镜像中创建出一个快照作为读写层,然后把这个读写层当作真正的 root 来挂载。具体操作上,在启动管理器里面写启动项的内核选项的时候,root 就写 / var/lib/docker 所在的分区,而 rootflags 里面至少要有一项 subvol=XXXXX,其中 XXXXX 是我们打算创建的读写层的位置。然后重中之重则是,写一个 hook,这个 hook 干的事情是:找到想要的 docker 镜像对应的 btrfs 子卷,给这个子卷创建一个快照,命名为 XXXXX(跟内核选项中的名字保持一致)。这样的话,在 Linux 把控制权交给 initramfs 中的 init 程序以后,init 程序会先去从 docker 缓存中的子卷创造出 XXXXX 快照,然后把 XXXXX 快照当作 root 来挂载以及进行接下来的操作。如果读者跟笔者一样使用 Arch Linux 的话,那么所有的这些工作笔者已经做了,读者可以直接拿来用。
笔者的源码位于 GitHub: https://github.com/zasdfgbnm/mkinitcpio-docker-hooks ,同时读者也可以直接从 AUR 中搜索
来安装笔者的 hook。下面就来介绍一下这个 hook 的使用方法。
- mkinitcpio - docker - hooks
mkinitcpio-docker-hooks 的使用流程大概分为如下几步:
要想启动到 docker 镜像中去,首先你得有一个适合在裸机上启动的 docker 镜像。很多 docker 镜像,为了减小镜像的大小,是不会附带只有裸机上才能用到的软件包(比如 dhcpcd)的,所以读者可能需要在 Dockerfile 中手动安装这些软件包,对于 Arch Linux 来讲,只需要安装 base 组就可以了。由于接下来要安装
,这里推荐使用一个已经内置 yaourt 的镜像,笔者使用的是自己的 archlinux-yaourt 镜像 zasdfgbnm/archlinux-yaourt 。所以,Dockerfile 的开头看起来是这个样子的:
- mkinitcpio - docker - hooks
- FROM zasdfgbnm/archlinux-yaourt
- USER root
- RUN pacman -Syu --noconfirm base
1 2 3 |
FROM zasdfgbnm/archlinux-yaourt USER root RUN pacman -Syu --noconfirm base |
作为示例,这里就不安装 base 之外的其他软件了,读者请自己根据需要安装其他软件。
的安装是在 docker 里面而不是当前运行在裸机上的系统中进行的。之所以要把这个软件包安装在 docker 镜像里面,很重要的原因是因为 Linux 内核不提供 ABI 的稳定性,所以内核模块跟内核的版本必须严格对应,不然模块是无法加载的。为了保持这种一致性,我们采取的措施是,在 docker 里面安装
- mkinitcpio - docker - hooks
,在 docker 中生成 initramfs,并且在启动的时候用镜像里面的内核启动,就可以确保内核、initramfs 中的模块、启动到镜像以后的 / lib/modules 三者保持一致。安装过程在 Dockerfile 中的写法如下:
- mkinitcpio - docker - hooks
- RUN sudo -u user yaourt -S --noconfirm mkinitcpio-docker-hooks
1 |
RUN sudo -u user yaourt -S --noconfirm mkinitcpio-docker-hooks |
安装完了
以后还需要配置,配置文件在
- mkinitcpio - docker - hooks
,初始内容如下:
- /etc/docker - btrfs.json
- {
- "docker_image": "archlinux/base",
- "docker_tag": "latest"
- }
1 2 3 4 |
{ "docker_image": "archlinux/base", "docker_tag": "latest" } |
我们需要做的就是把这两个变量的值替换为我们想要的值,比如说这里我打算把我的 docker 镜像的名字叫做 "sample_image"。同时,我们还需要在
中添加 docker-btrfs 这个 hook。
- /etc/mkinitcpio.conf
综上,可以在 Dockerfile 中使用如下命令来配置:
- RUN sed -i 's/archlinux\/base/sample_image/g' /etc/docker-btrfs.json
- RUN perl -i -p -e 's/(?<=^HOOKS=<span class='MathJax_Preview'>\()(.*)(?=\)</span>)/$1 docker-btrfs/g' /etc/mkinitcpio.conf
1 2 |
RUN sed -i 's/archlinux\/base/sample_image/g' /etc/docker-btrfs.json RUN perl -i -p -e 's/(?<=^HOOKS=\()(.*)(?=\()/$1 docker-btrfs/g' /etc/mkinitcpio.conf |
Dockerfile
- FROM zasdfgbnm/archlinux-yaourt
- USER root
- RUN pacman -Syu --noconfirm base
- RUN sudo -u user yaourt -S --noconfirm mkinitcpio-docker-hooks
- RUN sed -i 's/archlinux\/base/sample_image/g' /etc/docker-btrfs.json
- RUN perl -i -p -e 's/(?<=^HOOKS=\)</span>)(.*)(?=<span class='MathJax_Preview'>\()/$1 docker-btrfs/g' /etc/mkinitcpio.conf
1 2 3 4 5 6 |
FROM zasdfgbnm/archlinux-yaourt USER root RUN pacman -Syu --noconfirm base RUN sudo -u user yaourt -S --noconfirm mkinitcpio-docker-hooks RUN sed -i 's/archlinux\/base/sample_image/g' /etc/docker-btrfs.json RUN perl -i -p -e 's/(?<=^HOOKS=\()(.*)(?=\))/$1 docker-btrfs/g' /etc/mkinitcpio.conf |
来源: http://blog.jobbole.com/113448/