Linux的容器化之路:从Docker容器到可引导Linux磁盘镜像


【编者的话】自这篇文章发表后的两年多时间里,我看到人们对于从容器和Dockerfiles构建VM镜像很感兴趣。这有利于鼓励我继续这个工具的第二版开发,新的版本将涵盖更多的实际应用场景,具备更友好的用户使用体验。

虽然还没有没有看到上述方法的任何实际应用,但我坚定地认为对这一方法的持续探索是获得系统内部深层次知识的唯一途径。本文的讨论将主要涉及Docker和Linux。

我们是否可以获取一个真正意义上的Docker基础镜像?这个镜像只包含单行FROM debian:latest的Dockerfile,并能够转在物理机或虚拟机上启动运行。换句话说,是否可以创建一个磁盘镜像,既具备与容器相同的Linux用户环境,又具备引导能力?为此,实现目标的第一步就是DUMP容器根文件系统,这个步骤约等于执行docker export。不过后面还需要一系列其他的步骤,才能最终实现目标。

理论

开始之前引入一点支撑理论。Linux操作系统安装完成后,基本上是文件系统下一系列文件的组合,包括Linux内核二进制文件、初始裸盘二进制文件以及用户空间程序和调用库(通常是GNU内核应用)等,其中也包括重要的引导加载程序:
图片_1.png

提供运行tree -L 1 /命令,验证检查一下根目录的结构:
$ cat /etc/os-release | grep NAME
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"

$ tree -L 1 /
/
├── bin
├── boot
├── data
├── dev
├── etc
├── home
├── initrd.img -> boot/initrd.img-4.9.0-9-amd64  # initial ramdisk
├── lib
├── lib64
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
├── var
└── vmlinuz -> boot/vmlinuz-4.9.0-9-amd64        # kernel binary

接下来,再简要地看看Docker环境。Docker采用操作系统级虚拟化方式来封装其容器。这意味着容器运行共享了宿主机的内核,用户空间来自于Linux发行版系统,是完全隔离的:
图片_2.png

我们启动一个容器,来探究一下容器环境的根目录:
$ docker run -it debian:latest bash

root@62376e4c451b:/# cat /etc/os-release | grep NAME
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"

root@62376e4c451b:/# apt-get update && apt-get install -y tree
root@62376e4c451b:/# tree -L 1
.
|-- bin
|-- boot
|-- dev
|-- etc
|-- home
|-- lib
|-- lib64
|-- media
|-- mnt
|-- opt
|-- proc
|-- root
|-- run
|-- sbin
|-- srv
|-- sys
|-- tmp
|-- usr
`--  var

19 directories, 0 files

root@62376e4c451b:/# tree -L 1 /boot
boot/
0 directories, 0 files

可以发现上图所示的内容是Debian的用户空间,但没有内核相关的数据。然而,这还不是唯一的区别。当Linux操作系统将INIT守护进程作为PID 1进程运行时,Docker容器通常将shell或用户定义可执行进程作为PID 1进程。因此,我们还需要解决这个差异,以便使容器的环境状态尽可能接近完整的Debian系统部署。

实践

开始动手实践的第一步是创建一个具有以下内容的Dockerfile,此步骤具备重复操作性:
FROM debian:stretch

用docker build -t mydebian命令构建一个容器镜像。用wagoodman/dive命令查看这个镜像:dive mydebian。
图片_3.png

只包含Debian用户空间的镜像

如图所示,尽管该镜像包含了功能完整的Debian用户空间,镜像的总大小也只有101 MB。但这个镜像缺少内核,需要下载并安装系统的内核二进制文件,通过以下方式修改Dockerfile可以很容易实现:
FROM debian:stretch
RUN apt-get -y update
RUN apt-get -y install --no-install-recommends \
linux-image-amd64

重新构建并查看更新后的新镜像:
图片_4.gif

包括Debian用户空间和内核的镜像

目测linux-image-amd64包带来了232 MB的数据,其中24 MB来自/boot文件夹,大约200 MB来自/lib文件夹。继续往下分析……
图片_5.png

包括Debian用户空间和内核的镜像(详细)

请注意/boot/vmlinux -4.9.0-9-amd64内核包只有4.2 MB,/boot/initrd.img-4.9.0-9-amd64初始化裸盘包约16 MB,剩下的约200 MB是/lib/modules中的内核模块,以及常见的驱动文件。

接下来安装并分析一下init守护进程- systemd:
FROM debian:stretch
RUN apt-get -y update
RUN apt-get -y install --no-install-recommends \
linux-image-amd64
RUN apt-get -y install --no-install-recommends \
systemd-sysv

重新构建镜像并查看更新后的新镜像:
图片_6.gif

包括Debian用户空间、systemd和内核的镜像

如图所示,这部分约有30M数据。我们导出容器的文件系统到一个tar包:
$ CID=$(docker run -d mydebian /bin/true)
$ docker export -o linux.tar ${CID}

# List files in the archive:
$ tar -tf linux.tar | grep -E '^[^/]*/?$'
.dockerenv
bin/
boot/
dev/
etc/
home/
initrd.img
initrd.img.old
lib/
lib64/
media/
mnt/
opt/
proc/
root/
run/
sbin/
srv/
sys/
tmp/
usr/
var/
vmlinuz
vmlinuz.old

完成上述安装步骤之后,我们开始从tar文件中创建一个可引导的磁盘镜像。下面的步骤可以在Linux宿主机上直接执行,但由于我使用的是macOS,所以我将采用另一个Debian容器作为构建用机器:
$ docker run -it -v `pwd`:/os:rw            \
--cap-add SYS_ADMIN --device /dev/loop0 \
debian:stretch bash

第1步:创建一个足够大的磁盘镜像文件:
$ IMG_SIZE=$(expr 1024 \* 1024 \* 1024)
$ dd if=/dev/zero of=/os/linux.img bs=${IMG_SIZE} count=1

第2步:在新创建的磁盘镜像上创建一个磁盘分区:
$ sfdisk /os/linux.img <<EOF
label: dos
label-id: 0x5d8b75fc
device: new.img
unit: sectors

linux.img1 : start=2048, size=2095104, type=83, bootable
EOF

Checking that no-one is using this disk right now ... OK

Disk /os/linux.img: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

>>> Script header accepted.
>>> Script header accepted.
>>> Script header accepted.
>>> Script header accepted.
>>> Created a new DOS disklabel with disk identifier 0x5d8b75fc.
/os/linux.img1: Created a new partition 1 of type 'Linux' and of size 1023 MiB.
/os/linux.img2: Done.

New situation:

Device         Boot Start     End Sectors  Size Id Type
/os/linux.img1 *     2048 2097151 2095104 1023M 83 Linux

The partition table has been altered.
Syncing disks.

第3步:挂载镜像,使用ext3文件系统格式化,并将前面步骤中导出的tar文件内容复制到文件系统中:
$ OFFSET=$(expr 512 \* 2048)
$ losetup -o ${OFFSET} /dev/loop0 /os/linux.img
$ mkfs.ext3 /dev/loop0
$ mkdir /os/mnt
$ mount -t auto /dev/loop0 /os/mnt/
$ tar -xvf /os/linux.tar -C /os/mnt/

第4步:安装引导加载程序并卸载镜像:
$ apt-get update -y
$ apt-get install -y extlinux

$ extlinux --install /os/mnt/boot/
$ cat > /os/mnt/boot/syslinux.cfg <<EOF
DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /vmlinuz
APPEND ro root=/dev/sda1 initrd=/initrd.img
EOF

$ dd if=/usr/lib/syslinux/mbr/mbr.bin of=/os/linux.img bs=440 count=1 conv=notrunc

$ umount /os/mnt
$ losetup -D

上述步骤的结果将在工作目录生成一个linux.img的磁盘镜像文件,实现预期目标。

结果

我们按照前文的步骤,创建了一个可引导的Linux磁盘镜像。这个镜像支持转储到一个物理或虚拟的磁盘驱动器上。我们可以用这个镜像文件启动一台QEMU虚拟机:
$ qemu-system-x86_64 -drive file=linux.img,index=0,media=disk,format=raw

图片_7.gif

虚拟机运行linux.img镜像

或者用VirtualBox将将这个镜像转换为VDI磁盘:
$ VBoxManage convertfromraw --format vdi linux.img linux.vdi

福利:小巧的Alpine Linux

对于用户而言,如果约400 MB大小的Debian镜像太大的话,那么100 MB 大小的Alpine Linux也可以提供类似的功能:
FROM alpine:3.9.4
RUN apk update
RUN apk add linux-virt
RUN apk add openrc

图片_8.gif

结束语

本人创建了一个使用Docker自动创建磁盘镜像的项目,目前已经完成Debian和Alpine发行版的自动化创建,有兴趣的读者可以在GitHub上获取。并提供一下本人关于容器的更多文章:


原文链接:From Docker Container to Bootable Linux Disk Image(翻译:易理林)

0 个评论

要回复文章请先登录注册