执行Docker容器作为QEMU MicroVMS

2021-06-17 01:18:41

Warning: Can only detect less than 5000 characters

让我们开始吧!与大多数常规VM不同,我们跳过BIOS / UEFI初始化并直接引导到内核中。这是Qemu的一个很好的特征,但提出了我们应该启动的内核的问题? Docker容器通常不包括一个。答案很简单:我们构建自己的内核,只包含我们实际需要的驱动程序。要使您的生活更轻松,您可以从这里获取我的内核配置,并使用下面的命令编译它:

由于我们现在在Arch / X86_64 / Boot / Bzimage上有一个编译的内核映像,我们可以尝试使用qemu microvm引导内核的第一步。由于我们不提供任何文件系统,因此系统将恐慌,但它为我们提供了一个基线,用于将容器才能启动多长时间。

$ qemu-system-x86_64-m microvm,x-option-roms =关闭,ISA-Serial = OFF,RTC = OFF -NO-ACPI -ENABLY-KVM -CPU主机--Nodefaults -NO-User-Config -Nographic -No -reboot -device Virtio-erial-device -Chardev stdio,ID = Virtiocon0 -Device VirtConsole,Chardev = VirtioCon0 -Kernel Kernel / Bzimage -Append"控制台= HVC0 ACPI = OFF重启= T PANIC = -1" [0.043774]加载了X.509 Cert'构建时间自动化内核键:A55DE9768F536D965F27C2FE6FC963974D95B367' [0.044079]键类型._fscrypt注册[0.044211]键类型.fscrypt注册[0.044294]关键类型fscrypt-配置已注册[0.044559]密钥类型加密已注册[0.044888] VFS:无法打开根设备"(null)"或未知块(0,0):错误-6 [0.045056]请附加正确的" root ="启动选项;以下是可用的分区:[0.045224]内核恐慌 - 不同步:VFS:无法在未知块(0,0)上装入根FS [0.045386] CPU:0 PID:1 Comm:Swapper / 0不受污染5.12.10 #1 [0.045479]呼叫跟踪:[...]

正如常规的那样,具有VFS的内核恐慌:无法打开根设备"(null)"或未知块(0,0):错误-6。更有趣的细节是,内核花费不到50毫秒才能初始化!如果添加Qemu的启动时间,您将获得左右180ms。您可以尝试进一步通过压缩内核进行优化,但这足以让我们的用例。

让我们仔细看看用于启动Qemu的命令行选项,因为这些是很多:

切换到MicroVM模式并禁用所有不必要的设备(BIOS选项ROM,ISA串行设备和实时时钟)

禁用ACPI支持,通常用于控制主机的电源状态(即待机模式)

添加一个串行设备,该设备与主机上的STDIN / STDOUT以名称的VirtioCon0名称

内核命令行。使用HVC0(= VirtioCon0)为控制台,禁用ACPI无论如何都没有,使用三重CPU故障重新启动(正常重启需要ACPI)并在发生恐慌时立即重新启动

由于内核似乎工作,我们可以从我们的Todo列表中勾选并继续init系统。

在安装内核命令行指定的文件系统后,Linux将尝试执行/索引/ init和恐慌,如果它终止或根本不存在。我们现在可以尝试使用Docker Image的入口点作为init,但这可能会失败,因为我们的Linux环境尚未完全初始化。我们需要注入一个Init系统,该系统考虑安装各种目录并设置其他系统设置。可以使用SystemD或BusyBox init的内容,但在大多数情况下,这将是矫枉过正的。对于开始,我们使用自己的自我写入初始系统:

#include< errno.h> #include< stdio.h> #include< stdlib.h> #include< unistd.h> #include< sys / mount.h> #include< sys / stat.h> #include< sys / stat.h> char * const default_environment [] = {" path = / usr / local / bin:/ usr / local / sbin:/ usr / bin:/ usr / sbin:/ bin:/ sbin",null,} ; void mount_check(const char * source,const char * target,const char * filesystemtype,unsigned long mountflags,const void * data){struct stat info; if(统计(目标,&信息)== - 1&& errno == Enoent){printf("创建%s \ n"目标); if(mkdir(target,0755)< 0){perror("创建目录失败");出口(1); printf("安装%s \ n"目标); if(mount(源,目标,filesystemtype,mountflags,data)< 0){perror("装载失败");出口(1); int main(int argc,char * argv []){mount_check(" none" / proc&#34 ;," proc&#34 ;, 0,&#34 ;"); mount_check("没有#34; / dev / pts"" devpts&#34 ;, 0,""); mount_check("没有" / dev / muqueue&#34 ;,#34; muque&#34 ;, 0,"); mount_check("没有#34; / dev / shm&#34 ;,#34; tmpfs&#34 ;, 0,"); mount_check("没有" / sys&#34 ;,#34; sysfs&#34 ;, 0,""); mount_check("没有" / sys / fs / cgroup" cgroup" 0,"""); sethostname(" microvm" sizeof(" microvm")); execle(" / bin / sh"" / bin / sh",null,default_environment); perror(" exec失败");返回1;}

当启动容器时,该小C应用程序基本上与Docker相同。使测试更容易,我们将硬件/垃圾箱/ sh作为入口点。从长远来看,您希望将其替换为其他应用程序或启动脚本。请注意,我们使用execle替换我们的init应用程序,以便目标进程成为新的init进程。如果应用程序不等待子进程(呼叫Sigchld上的WADEPID()),您可能最终可以使用大量僵尸进程,因为所有孤立进程都重新定义到init进程。如果事实证明是一个问题,可以扩展小的C脚本,或者您可以通过Bash运行所有内容,这也可以照顾此操作。

在我们可以测试我们的init应用程序之前,我们首先需要一个文件系统,我们可以注入它。文件系统还需要提供/ bin / sh。让我们弄清楚如何将码头图像转换为QEMU磁盘图像!

我们现在编译了我们的内核并创建了一个简单而简约的初始系统。最后缺少部分是在我们第一次测试我们的方法之前将现有的Docker映像转换为虚拟机磁盘映像。在此过程中,我们将注入自己的init系统二进制文件,以便我们不需要单独的initramfs文件系统。我们将我们的方法分为两个步骤,首先我们尝试生成包含基本文件系统的Tar存档,然后我们将其转换为磁盘映像。

有多种方式如何将码头图像转换为tar存档。最简单的方法是使用Docker构建。它为我们提供了一种简单的方法,如何注入我们的init系统,我们也可以自定义图像。另一种方法是使用Docker容器创建和Docker容器导出以导出基本文件系统或手动组合由Docker Save导出的图层。对于本教程,我们将使用Docker构建,但您也可以使用脚本从Docker注册表中获取不同的图层,而不是使用Docker。

对于我们的第一次测试,我们将使用Alpine Linux并告诉Docker Build来注入我们的init系统并将文件系统作为tar存档提取:

我们几乎已经完成,只需将TAR存档转换为QCOW2图像:

第一个命令将tar存档转换为qcow2图像。 QCOW2图像格式与使用原始图像的效率不高,但它有各种优势,我们可以从中受益。与Docker容器相比,使用VM的一个问题是您不能轻易在访客和主机之间共享资源。 VM无法简单地使用所有可用的磁盘空间,但仅限于磁盘映像的大小。从安全性的角度来看,限制容器可用的最大磁盘空间是有意义的,但有时候很难提前预测所需的磁盘空间量,并且您可能希望更多地分配才能确定(见大小参数)。使用原始图像时,您将最终包含几乎没有任何内容的巨大图像文件,如果您不希望丢失大量磁盘空间,则需要依赖您的文件系统作为稀疏处理文件。图像格式QCow2已经支持稀疏图像本身,至少在理论上。出于某种原因,Virt-Make-FS不会产生稀疏图像,并且生成的Alpine-Light.qcow2的大小为207MB。要解决此问题,我们调用qemu-img转换重新编码我们的图像文件,它会缩小到9.9MB。使用QCOW2的另一个优点是它支持差异图像,因为我们稍后会看到。

我们现在收集了所有依赖关系,并可以第一次运行VM。我们只需要调整我们的QEMU命令行有点以附加磁盘映像。

qemu-system-x86_64-m microvm,x-option-roms =关闭,ISA-Serial = OFF,RTC = OFF -NO-ACPI -ENABLY-KVM -CPU主机--Nodefaults -No-User-Config -Nographic -No- Reboot -Device Virtio-Serial-Device -Chardev STDIO,ID = VirtioCon0 -Device VirtConsole,Chardev = VirtioCon0 -Drive ID = root,file = Alpine.qcow2,format = qcow2,if = none -device Virtio-Blk-Device,驱动器= root -kernel kernel / bzimage -append"控制台= hvc0 root = / dev / vda rw acpi =关闭重启= t panic = -1"

我们的概念证明作品!我们可以在Qemu运行的Alpine Docker集装箱内执行应用程序。这是一个非常简单的例子。为了做点什么,我们需要延长我们的方法。

我们已经证明,将Docker图像转换为VM的基本思想,但有各种功能缺少,以便真正利用这种方法。到目前为止,我们只能使用STDIO与VM通信,并且每次启动新实例时都需要重新创建图像。让我们进一步掌握这种方法,并学习如何模仿Docker的各种功能。

如果您熟悉QEMU并密切关注命令行,您会注意到我们定义了所有所需的设备,但从未真正指定了内存量或CPU内核数量。默认情况下,VM将具有大约100MB的内存和1个CPU核心。在实践中,这可能是不够的,并且您希望扩展命令行:

将更多CPU核心分配给VM而不是必需的不是真正的问题,因为VM只是在系统中的任何其他进程中运行。如果访客无法完全使用其所有指定的核心,则您的主机CPU仍然可以使用其他任务或VM的物理核心。因此,如果您不知道实际需要多少,则可以过度管理虚拟CPU内核的数量。

您可能会迟早注意到的另一个问题是Linux内核永远才能初始化随机数生成器。我们的MicroVM中根本没有真正的熵源,并且试图访问RNG的应用程序将冻结。您可以通过将主机RNG公开给来宾:使用:

我们必须每次启动VM时重新创建磁盘映像,如果我们要确保未通过上一个运行修改图像,或者我们要并行执行多个实例。这减慢了启动过程,并且还会消耗比必要的更多磁盘空间。一个选项是将磁盘映像标记为只读,但这可能会破坏大多数容器,除非我们将额外的技巧应用于TMPFS覆盖物。更好的方法是使用qemu的差分/增量磁盘映像特征。这允许我们在原始图像文件的顶部添加另一层。所有写入指令只修改顶层顶层,而我们的原始图像文件被视为只读。这种方法与Docker的OverlayFS存储驱动程序非常相似。

只需替换QEMU命令行选项中的图像文件名,您就完成了。现在可以并行运行具有相同基础图像的多个VM。在VM终止之后,您可以再次删除差异图像。您可能希望将这些步骤包装到脚本中,以便您不必手动执行它们。

如果要保持其中一个VM所做的更改,则可以使用以下方式将它们与基本图像合并:

概念证明只使用STDIO与外部世界进行沟通,并且没有其他方式进出数据。如果您想在主机和访客之间传输大量文件,这显然不够。有两种简单的方法,我们可以用来传输文件。

使用磁盘映像:如果只想在VM开始之前上传/下载文件,并且在终止之后,您可以简单地修改差分磁盘映像中的文件。像QEMU-NBD或Guestmount这样的工具可以简单地安装在主机系统中的图像,以便您可以根据需要读取和写入文件。这样的方法是有意义的(可重复的)构建。源代码在VM开始之前上载,并且在VM关闭后提取生成的软件包。

虽然VM正在运行:如果要在运行时与VM共享文件,则可以使用9PFS虚拟文件系统设备。此设备允许您从主机系统挂载目录,类似于码头器中的绑定安装。要将目录公开给客户端,我们需要向QEMU命令行添加两个选项:

使用ID文件注册MyFiles目录。如果需要,您可以将RabOnly添加为选项。

要访问guest虚拟机中的文件,您需要使用mount_tag指定的名称装入文件系统。如果使用前面提供的内核配置,则使用Virtio的9P网络文件系统支持。您可以使用命令行安装文件系统:

您可能需要调整MSIZE参数,如QEMU Wiki中所述,以获得最佳性能。

QEMU提供可用于在访客和主机之间进行通信的各种虚拟设备。 最明显的解决方案是添加网络设备。 在许多情况下,这可能是必要的,但它也具有您需要在主机上创建桥梁或虚拟以太网设备的缺点。 此步骤需要特殊的权限,也是默认情况下为root运行的原因之一。 如果您不需要实际的网络访问,则可能需要尝试其他选项之一。 串行设备:您可以将一个或多个串行设备添加到访客并以各种方式(管道,套接字,文件等)从主机访问它们。 这使得可以在主机和访客之间传输数据。 Virtio设备也比模拟硬件串行设备快得多。 我在过去使用这种方法将Live-Stream Build Logs到构建系统的网站。 如果你想 ......