虚拟化技术大观

按:2014 年 11 月 21 日,笔者在阿里巴巴技术俱乐部的虚拟化技术交流会暨 “科大云 3.0” 发布会上分享了一些对虚拟化技术的浅见,整理、补充后与诸位探讨。(长文慎入)

虚拟化技术大家都不陌生,我们大都使用过诸如 VMWare、VirtualBox 的虚拟机软件。一些人认为,虚拟化技术是近几年跟着云计算的潮流才火起来的,十年前只是桌面用户测试其他操作系统的玩具。非也。只要计算机上同时运行着多个任务,就会有任务隔离的需求,虚拟化就是让每个任务看起来独占整个计算机、隔离任务之间影响的技术。早在计算机还是庞然大物的 20 世纪 60 年代,虚拟化技术就开始发展了。

IBM 7044

黑历史:硬件虚拟化

1964 年的 IBM M44/44X 被认为是世界上第一个支持虚拟化的系统。它采用专门的硬件和软件,能够在一台物理机器上虚拟多个当时流行的 IBM 7044 大型机。它使用的虚拟化方法是非常原始的:像分时系统一样,在每个时间片,一个 IBM 7044 大型机独占所有硬件资源来运行。

值得一提的是,这个研究用的原型系统不仅开启了虚拟化技术的时代,也提出了 “分页” 这样一个重要的概念(因为每个虚拟机需要使用虚拟地址,这就需要一层虚拟地址到物理地址的映射)。

在那个 “进程” 概念尚未被发明的年代,多任务操作系统和虚拟化技术事实上是难以分开的,因为 “虚拟机” 就是一个任务,而且当时还没有 Intel x86 这种霸主地位的体系结构,各家的大型机各自为政,也谈不上兼容别家的体系结构。这种 “任务级” 或者说 “进程级” 虚拟化,从概念上延续到今天,就是以 LXC 和 OpenVZ 为代表的操作系统级虚拟化。

这种主要依赖定制的硬件来实现虚拟化的技术,史称 “硬件虚拟化”。在这个什么都要 “软件定义” 的时代,大型机已经日薄西山,大部分依赖硬件的虚拟化也进了历史博物馆。今天我们看到的虚拟化技术,大多是软件为主,硬件为辅。下图所示的划分没有严格的界限,而且一种虚拟化解决方案可能同时使用了下图中的多种技术,因此不要在意这些细节啦~

从模拟执行到二进制翻译

前面提到大型机时代各大厂商各有各的体系结构和指令集,为什么没有出现各种指令集之间的翻译软件呢?须知,思科就是靠兼容各家网络设备和协议起家的(这里面还有八卦,创办思科的那对小情侣希望使用计算机网络传递情书,但网络设备五花八门的,于是他们就发明了兼容多种协议的路由器)。

思科第一台路由器

在网络协议之间翻译和在指令集之间翻译,都是机械、冗长、繁琐的事情,能把它做对,考虑到各种边角情况,是需要无比天才和细心的事情。指令集比网络协议麻烦的地方是,指令的边界(哪里是第一条指令,哪里是最后一条指令)未知,可能执行特权操作(如重启),还能把动态生成的数据作为代码来执行(冯·诺依曼体系结构中,数据和代码共享线性内存空间)。因此,在二进制代码运行之前就做好指令集之间的静态翻译,是不可能的。

最简单粗暴的解决方法是 “模拟执行”。开一个大数组当 “虚拟机” 的内存,拿来指令集手册,写一个无数个 case 的 switch 语句,判断当前要执行的是什么指令,按照手册的说法模拟执行。这样做自然是可行的,但效率就不敢恭维了,虚拟机至少比物理机慢一个数量级。所谓的 “动态类型语言”,如 Python、PHP、Ruby,在编译成中间代码后,大多也是用这种 “模拟执行” 的方法,因此快不起来。著名的 x86 模拟器 Bochs 就是模拟执行的,虽然慢,但兼容性好,而且不容易有安全漏洞。

不能因为指令翻译难做就因噎废食,一个运行 1 亿次的 for 循环,里面如果都是加加减减一类的操作,翻译成机器码直接执行,肯定比模拟执行要快。在二进制翻译前面,要加上 “动态” 两个字,也就是在程序执行的过程中,能翻译的部分翻译成目标架构的机器码直接执行(并缓存起来以便重复利用),不能翻译的部分就陷入到模拟器里,模拟执行特权指令,再对后面的代码进行翻译。如今 Java、.NET、JavaScript 等使用的 JIT(Just-In-Time)技术也是类似的套路。

有人会问,如果我只是虚拟相同的体系结构,比如在 64 位 x86 系统上虚拟一个 64 位 x86 系统,需要做指令翻译吗?需要的。比如,虚拟机可能读取特权寄存器 GDT、LDT、IDT 和 TR,并不会触发处理器异常(即虚拟机管理器无法捕获到这样的行为),但这些特权寄存器的值又是需要对虚拟机 “伪造” 的。这就需要在虚拟机读取特权寄存器的指令执行之前,把它替换成调用虚拟机管理器的指令。

遗憾的是,在大型机年代,能把动态二进制翻译这件事做好的天才级人物 Fabrice Bellard 才刚刚出生(1972 年)。

Fabrice Bellard

Fabrice Bellard 的 QEMU(Quick EMUlator)是目前最流行的采用动态二进制翻译技术的虚拟化软件。它可以模拟 x86、x86_64、ARM、MIPS、SPARC、PowerPC 等多种处理器架构,无修改地运行这些架构上的操作系统。当我们享受视听的乐趣,当我们流畅地运行各种架构的虚拟机系统时,不应忘记 ffmpeg 和 qemu 的创造者 Fabrice Bellard 大神。

以我们最熟悉的 Intel x86 架构为例,分为四个特权级 0~3。一般情况下,操作系统内核(特权代码)运行在 ring 0(最高特权级),而用户进程(非特权代码)运行在 ring 3(最低特权级)。(即使你是 root,你的进程也在 ring 3!只有进了内核才是 ring 0,无上的权力意味着巨大的责任,内核编程的限制很多,吐槽一下)

使用了虚拟机之后,虚拟机(Guest OS)运行在 ring 1,主机操作系统(VMM,Virtual Machine Monitor)运行在 ring 0。比如在 Windows 上装个 Linux 虚拟机,Windows 内核运行在 ring 0,而被虚拟的 Linux 内核运行在 ring 1,Linux 系统里的应用程序则运行在 ring 3。

当虚拟机系统需要执行特权指令时,虚拟机管理器(VMM)就会立即捕获它(谁让 ring 0 比 ring 1 的特权级高呢!)并模拟执行这条特权指令,再返回到虚拟机系统。为了提高系统调用、中断处理的性能,有时会利用动态二进制翻译的技术,在运行前把这些特权指令替换成调用虚拟机管理器 API 的指令。如果所有特权指令都模拟得天衣无缝,虚拟机系统就像运行在物理机器上一样,完全不能发现自己运行在虚拟机里。(当然,事实上还是有一些破绽的)

请虚拟机系统和 CPU 来帮忙

动态二进制翻译虽然比模拟执行快了很多,但由于每条特权指令都要到虚拟机管理器里绕一圈(模拟执行),离物理机的性能仍然有不小的差距。要让虚拟机快起来,人们想到两种方法:

  1. 让虚拟机操作系统帮忙,所谓 “半虚拟化”(paravirtualization)或 OS-assisted virtualization
  2. 让 CPU 帮忙,所谓 “硬件辅助虚拟化”(hardware-assisted virtualization)

这两种方法不是互斥的。现代的很多虚拟化解决方案,如 Xen、VMware,都同时使用了两种方法。

半虚拟化(paravirtualization)

既然动态二进制翻译的难点和性能瓶颈在于模拟执行那些杂七杂八的特权指令,我们能不能修改虚拟机系统的内核,把那些特权指令改得好看些?毕竟在多数情况下,我们并不需要对虚拟机刻意 “隐瞒” 虚拟化层的存在,而是要在虚拟机之间提供必要的隔离,同时又不造成太多性能开销。

Paravirtualization 这个单词的前缀是 para-,即 “with” “alongside” 之意。也就是虚拟机系统与虚拟化层(主机系统)不再是严格的上下级关系,而是互信合作的关系,虚拟化层要在一定程度上信任虚拟机系统。在 x86 架构中,虚拟化层(Virtualization Layer)和虚拟机系统的内核(Guest OS)都运行在 ring 0。

虚拟机系统的内核需要经过特殊修改,把特权指令改成对虚拟化层 API 的调用。在现代操作系统中,由于这些体系结构相关的特权操作都被封装起来了(例如 Linux 内核源码中的 arch/ 目录),比起二进制翻译需要考虑各种边角情况,这种对虚拟机内核源码的修改就简单一些了。

相比使用二进制翻译的全虚拟化(full virtualization),半虚拟化是牺牲了通用性来换取性能,因为任何操作系统都可以无修改地运行在全虚拟化平台上,而每个半虚拟化的操作系统内核都要经过人肉修改。

硬件辅助的虚拟化

同样的功能,专用硬件实现几乎总是比软件实现更快,几乎是一条金科玉律了。在虚拟化这件事上,比尔搞不定的事情,自然也要请安迪来帮忙。(比尔是微软公司创始人,安迪是 Intel 公司创始人)

让硬件帮助软件实现虚拟化的概念也不是新东西了。早在 1974 年,著名论文 Formal requirements for virtualizable third generation architectures 就提出了可虚拟化体系结构的三个基本条件:

  1. 虚拟机管理器提供了与真实机器一模一样的虚拟环境;
  2. 运行在虚拟机里的程序在最坏情况下也比物理机慢得不多;
  3. 虚拟机管理器能够完全控制所有的系统资源。
    1. 虚拟机里的程序不能访问未分配给它的资源
    2. 在某些情况下,虚拟机管理器能够收回已经分配给虚拟机的资源

早期的 x86 指令集不满足上述条件,因此需要虚拟机管理器做复杂的动态二进制翻译。既然二进制翻译的主要开销在 “捕获” 虚拟机系统的特权指令上,CPU 能做的事就是帮忙把 “捕获” 的事情给做了。Intel 家的解决方案叫做 Virtual Machine Control Structures (VT-x),于 2005 年冬天推出;AMD 也不甘人后,随后推出了类似的 Virtual Machine Control Blocks (AMD-V)。

Intel 在原有的四个特权级基础上,增加了一个专供虚拟机管理器(VMM)使用的 “Root Mode”。这就好像是天不怕地不怕的孙悟空(ring 0)逃不出如来佛(Root Mode)的手掌心。这样,虚拟机系统尽管运行在 ring 0,其执行的特权指令仍然会被 CPU 自动捕获(触发异常),陷入到 Root Mode 的虚拟机管理器里,处理后再使用 VMLAUNCH 或 VMRESUME 指令返回到虚拟机系统。

为了方便半虚拟化(paravirtualization)中用 API 调用替代 CPU 异常捕获,Intel 还提供了从虚拟机(Non-root Mode)到虚拟机管理器(Root Mode)的 “系统调用”:VMCALL 指令。

看起来很美好,不是吗?事实上,对刚刚开始支持硬件辅助虚拟化的 CPU,使用这个方式未必比动态二进制翻译性能高。因为硬件辅助虚拟化模式对每条特权指令,需要切换到 Root Mode,处理完后再返回,这个模式切换就像实模式和保护模式切换一样,需要初始化很多寄存器,还要保存和恢复现场,更不用说对 TLB、高速缓存的影响了。不过,Intel 和 AMD 的工程师也不是吃白饭的,在较新的 CPU 里,硬件辅助虚拟化的性能开销已经比动态二进制翻译做得更好了。

最后说点实用的:在大多数 BIOS 里,硬件辅助虚拟化都是有开关选项的。只有使能了 Intel Virtualization Technology 这样的选项,虚拟机管理器才能用上硬件加速。因此在使用虚拟机的时候,不要忘了进 BIOS 检查一下。

CPU 虚拟化不是一切

前面说了这么多,给大家一种假象:只要虚拟了 CPU 指令就万事大吉了。CPU 固然是计算机的大脑,但计算机的其他组件也不容忽略。这些组件包括与 CPU 朝夕共处的内存和硬盘、显卡、网卡等形形色色的外设。

内存虚拟化

前面讲虚拟化的鼻祖 IBM M44/44X 的时候,提到它提出了 “分页” 的概念。也就是每个任务(虚拟机)似乎独占所有内存空间,分页机制负责把不同任务的内存地址映射到物理内存。如果物理内存不够了,操作系统就会把不常用的任务的内存交换到磁盘之类的外部存储,等那个不常用任务需要执行时再加载回来(当然,这种机制是后来才发明的)。这样,程序的开发者就不需要考虑物理内存空间有多大,也不需要考虑不同任务的内存地址是否会冲突。

现在我们用的计算机都有分页机制,应用程序(用户态进程)看到的是一片广阔无涯的虚拟内存(Virtual Memory),似乎整台机器都被自己独占;操作系统负责设置用户态进程的虚拟内存到物理内存的映射关系(如下图 VM1 框内所示);CPU 中的 MMU(Memory Management Unit)负责在用户态程序运行时,通过查询映射关系(所谓的页表),把指令中的虚拟地址翻译成物理地址。

有了虚拟机,事情就麻烦了一层。虚拟机之间要隔离,虚拟机的操作系统也就不能直接看到物理内存。上图中的红色部分即 “机器内存”(MA)由虚拟化层负责管理,而虚拟机看到的 “物理内存”(PA)事实上是被虚拟化的,也就是形成了两级地址映射关系。

在 Intel 的 Nehalem 架构之前(感谢 jonathan 指正),内存管理器(MMU)只知道按照经典的分段、分页机制来进行内存寻址,不知道虚拟化层的存在。虚拟化层需要负责把两级地址映射 “压缩” 成一级映射,如上图红色箭头所示。虚拟化层的做法是:

  • 当切换到一台虚拟机时,就用这台虚拟机的内存映射关系(页表)作为物理机的页表;
  • 如果虚拟机操作系统试图修改指向页表的 CR3 寄存器(如在进程间切换页表),会被替换成对 “影子页表” 内存地址的访问(通过动态二进制翻译,或者在硬件辅助虚拟化方案中,触发异常后陷入虚拟化层);
  • 如果虚拟机操作系统试图修改页表的内容,例如将一块新的物理内存(PA)映射到一个进程的虚拟内存(VA),虚拟化层需要将其截获,为物理地址(PA)在机器内存(MA)中分配空间,并将 “影子页表” 中的虚拟内存(VA)映射到机器内存(MA);
  • 如果虚拟机访问一个页面时它被换出到外部存储了,就会触发缺页异常,虚拟化层要负责把缺页异常分发到该虚拟机的操作系统。

每个对页表的操作都从虚拟化层那里绕一圈是不小的开销。作为硬件辅助虚拟化的一部分,Intel 从 Nehalem 架构开始引入了 EPT (Extent Page Table) 技术,AMD 也引入了 NPT (Nest Page Table),让内存管理单元(MMU)支持二级内存翻译(Second Level Address Translation,SLAT):给虚拟化层提供另一套页表,首先根据虚拟地址(VA)查原来的页表得到物理地址(PA),再查新的页表得到机器地址(MA)。这样就不再需要 “影子页表” 了。

有人也许会担心增加的一级映射关系会减慢内存访问速度,事实上不论是否启用二级内存翻译(SLAT),页表高速缓存(Translation Lookaside Buffer,TLB)都会存储虚拟地址(VA)到机器地址(MA)的映射。如果 TLB 的命中率较高,则增加的一级内存翻译不会显著影响内存访问性能。

设备虚拟化

除了 CPU 和内存以外的组件,被统称为外设。CPU 与外设之间的通信就是 I/O 了。每台虚拟机需要有硬盘、网络,甚至显卡、鼠标、键盘、光驱等。如果大家使用过虚拟机,应该对这些配置不陌生。

Hyper-V 中的虚拟设备配置

设备虚拟化一般有三种方式:

  1. 虚拟设备,共享使用
  2. 直接分配,独占访问
  3. 在物理设备的辅助下,虚拟出多个 “小设备”

我们以网卡为例,看看上述三种设备虚拟化方式是怎样的:

  1. 最经典的做法:对每个虚拟机虚拟出一个与物理设备无关的网卡,虚拟机访问网卡时,就会陷入到虚拟机管理器(VMM)。这个虚拟网卡仿佛有 A/B 两面,A 面在虚拟机内,B 面在虚拟机管理器(主机)里。从 A 面发出的数据包会被送到 B 面,而从 B 面的发出的数据包会被送到 A 面。虚拟机管理器里有一个虚拟交换机,在各个虚拟机的 B 面以及物理网卡间进行转发。显然,纯软件实现的虚拟交换机是系统的性能瓶颈。
  2. 最土豪的做法:给每个虚拟机分配一个真实的物理网卡,把网卡的 PCI-E 地址空间映射到虚拟机内。当虚拟机访问该网卡的 PCI-E 地址空间时,如果 CPU 支持 I/O 虚拟化,CPU 中的 I/O MMU 就会把物理地址(PA)映射到机器地址(MA);如果 CPU 不支持 I/O 虚拟化,就会触发异常并陷入虚拟机管理器,由虚拟机管理器软件完成从物理地址到机器地址的翻译,并发出真正的 PCI-E 请求。当 CPU 支持 I/O 虚拟化时,虚拟机访问网卡不需要经过虚拟化层,性能是比较高的,但这需要每个虚拟机独占一块物理网卡,土豪才能玩得起。
  3. 最时髦的做法:物理网卡支持虚拟化,可以虚拟出多个 Virtual Function(VF),虚拟化管理器可以把每个 Virtual Function 分配给一个虚拟机,虚拟机里使用定制的驱动程序,就能看到一个独立的 PCI-E 设备。虚拟机访问网卡时也是通过 CPU 的 I/O MMU 直接进行地址翻译,不需要经过虚拟化层。物理网卡内部则有一个简单的交换机,能够在 Virtual Functions 和外面连接的网线之间做转发。这种方法可以用单块网卡实现高性能,但需要比较高端的网卡,需要 CPU 支持 I/O 虚拟化,还需要虚拟机系统内使用定制的驱动程序。

I/O MMU 是让外设和虚拟机能够直接通信的硬件组件,绕过了虚拟机管理器(VMM),提高了 I/O 性能。Intel 的名字叫做 VT-d,而 AMD 的名字叫做 AMD-Vi。它可以把设备寄存器和 DMA 请求中的物理地址(PA)翻译成机器地址(MA),还可以把设备产生的中断分发到虚拟机里。

又到了实用时间:跟硬件辅助虚拟化一样,I/O MMU 在 BIOS 里也是有开关的,别忘了打开哦~

设备虚拟化也有 “全虚拟化” 和 “半虚拟化” 之分。如果虚拟机系统里的驱动程序能够修改,就可以直接调用虚拟机管理器(VMM)的 API,减少模拟硬件设备的开销,这种修改虚拟机内的驱动程序来提高性能的行为就属于半虚拟化。

通过在虚拟机系统里安装额外的驱动程序,还可以实现一些主机与虚拟机之间的交互(如共享剪贴板、拖动传文件)。这些驱动程序会在虚拟机系统中加载钩子,调用虚拟化管理器提供的 API 完成与主机系统的交互。

在虚拟机里安装额外的驱动程序

做个小结,大家可以去喝杯茶,回味一下:

  • 根据虚拟化管理器的实现方式,分为硬件虚拟化和软件虚拟化,硬件虚拟化已经退出历史舞台;
  • 根据虚拟机操作系统是否需要修改,分为全虚拟化(Full Virtualization)和半虚拟化(Paravirtualization);
  • 根据如何处理特权指令,分为模拟执行(效率很低)、二进制翻译(QEMU)和硬件辅助的虚拟化(KVM 等)。

操作系统级虚拟化

很多时候,我们并不是想在虚拟机里运行任意的操作系统,而是希望在不同的任务间实现一定程度的隔离。前面提到的虚拟化技术,每个虚拟机都是一个独立的操作系统,有自己的任务调度、内存管理、文件系统、设备驱动程序等,还会运行一定数量的系统服务(如刷新磁盘缓冲区、日志记录器、定时任务、ssh 服务器、时间同步服务),这些东西都会消耗系统资源(主要是内存),而且虚拟机和虚拟机管理器的两层任务调度、设备驱动等也会增加时间开销。能不能让虚拟机共享操作系统内核,又保持一定的隔离性呢?

两河缘何入一渠。为了方便开发和测试,1979 年的 UNIX 第七版引入了 chroot 机制。chroot 就是让一个进程把指定的目录作为根目录,它的所有文件系统操作都只能在这个指定目录里进行,这样就不会危害到主机系统。尽管 chroot 存在经典的跳出漏洞,而且它没有对进程、网络等资源进行任何隔离,chroot 至今仍然被用作构建和测试软件的干净环境。

要成为一个真正的虚拟化解决方案,只有文件系统隔离是不够的。另外两个重要的方面是:

  1. 进程、网络、IPC(进程间通信)、用户等命名空间的隔离。使得虚拟机内部只能看到自己的进程,只能使用自己的虚拟网卡,进程间通信时不会干扰到虚拟机外面,虚拟机内的 UID/GID 与外面的独立。
  2. 资源的限制和审计。不能因为虚拟机内的程序 “跑飞了”,就占掉物理机器的所有 CPU、内存、硬盘等资源。必须要能统计虚拟机占了多少资源,并能够对资源进行限制。

上述两件事情就是 BSD 和 Linux 社区在进入 21 世纪以来逐步在做的。在 Linux 中,命名空间的隔离叫做用户命名空间,在创建进程时,通过指定 clone 系统调用的参数来创建新的命名空间;资源的限制和审计是 cgroups 做的,它的 API 位于 proc 虚拟文件系统中。

这种虚拟机里运行一个或多个进程、虚拟机与主机共享一个内核的虚拟化方案,被称为 “操作系统级虚拟化” 或 “任务级虚拟化”。由于 Linux Containers(LXC)从 Linux 3.8 版本开始被纳入内核主线,操作系统级虚拟化又被称为 “容器”(container)。为了与虚拟机是一个完整的操作系统的虚拟化方案相区分,被隔离执行的进程(进程组)往往不称为 “虚拟机”,而称为 “容器”。由于没有多余的一层操作系统内核,容器比虚拟机更加轻量,启动更快,内存开销、调度开销也更小,更重要的是访问磁盘等 I/O 设备不需要经过虚拟化层,没有性能损失。

Linux 上的操作系统级虚拟化并不是从 LXC 开始有的,恰恰相反,LXC 是 “长江后浪推前浪” 的典型。Linux-Vserver、OpenVZ、Parallels Containers 都是在 Linux 内核里实现操作系统级虚拟化的解决方案。尽管小字辈们赢得了更多的眼球,作为一个长者,OpenVZ 还是比 LXC 多一些 “企业级” 的功能:

  • 可以审计并限制每个容器的磁盘配额,通过目录级别的磁盘空间审计来实现(这也是 freeshell 使用 OpenVZ 而非 LXC 的主要原因);
  • 支持检查点(checkpoint)、热迁移(live migration);
  • free -m、df -lh 等内存和磁盘的容量,在 OpenVZ 容器里看到的是它的配额,而 LXC 里看到的是物理机的容量;
  • 支持 swap 空间,当发生 OOM(Out Of Memory)时杀死进程的行为与物理机器相同,而 LXC 会直接分配不出内存。

但是,由于 OpenVZ 坚持走 RHEL 路线,RHEL 6 还是 2.6.32 的老内核,RHEL 7 刚发布 OpenVZ 还没跟进,OpenVZ 内核现在看来已经很老了,连新版本的 systemd 都运行不起来,更不用说 3.x 内核的各种酷炫新功能了。

OpenVZ 架构

容器的好管家 Docker

好马配好鞍,这么好用的 Linux 容器,自然也要有个好管家。从哪里获得系统镜像?如何对镜像进行版本控制?Docker 就是近来大红大紫的 Linux 容器好管家,据说连巨硬都向 Docker 示好,要为 Windows 开发容器支持了。

Docker 其实是为系统运维而生的,它大大降低了软件安装、部署的成本。软件的安装之所以是个麻烦事,是因为

  1. 软件之间存在依赖关系。比如,Linux 上依赖标准 C 库 glibc,依赖密码学库 OpenSSL,依赖 Java 运行环境;Windows 上依赖 .NET Framework,依赖 Flash 播放器。如果每个软件都带上它所有的依赖,那就太臃肿了,如何找到并安装软件的依赖,是一门大学问,也是各个 Linux 发行版的特色所在。
  2. 软件之间存在冲突。比如,程序 A 依赖 glibc 2.13,而程序 B 依赖 glibc 2.14;甲脚本需要 Python 3,乙脚本需要 Python 2;Apache 和 Nginx 两个 Web 服务器都想要监听 80 端口。互相冲突的软件安装在同一个系统里,总是容易带来一些混乱,比如 Windows 早期的 DLL Hell。解决软件冲突之道就是隔离,让多个版本在系统里共存,并提供方法来找到匹配的版本。

我们看看 Docker 如何解决这两个问题:

  1. 把软件的所有依赖关系和运行环境打包在一个镜像里,而不是使用复杂的脚本来在未知的环境里 “安装” 软件;
  2. 这个包含了所有依赖的包一定很大,因此 Docker 的镜像是层次化的,即应用程序的镜像一般是基于基本系统镜像,只需要传输和存储增量部分就行了;
  3. Docker 使用基于容器的虚拟化,把每个软件运行在独立的容器里,避免了不同软件的文件系统路径冲突和运行时的资源冲突。

其中,Docker 的层次化镜像结构依赖于 Linux 的 AUFS(Another Union File System),AUFS 可以把基础目录 A、增量目录 B 合并挂载为一个目录 C,C 中能够同时看到 A、B 中的文件(有冲突以 B 中的为准),对 C 的修改会被写入 B。当一个 Docker 容器被启动时,就会生成一个增量目录并与作为基础目录的 Docker 镜像合并挂载,Docker 容器的所有写操作都被写入增量目录,而不会修改基础目录。这样就以比较低的开销实现了文件系统的 “版本控制”,也减小了软件的分发体积(只需分发增量部分,基础镜像是大多数人已经有的)。

Docker、LXC 与内核组件的关系

Docker 的虚拟化则是基于 libcontainer(上图比较老了,那时还是基于 LXC)。libcontainer 和 LXC 事实上都是基于 Linux 内核提供的 cgroups 资源审计、chroot 文件系统隔离、命名空间隔离等机制。

插播:Freeshell 是如何启动的

我们知道 freeshell 采用的是 OpenVZ 虚拟化技术,经常有人问能不能换内核,还有人问 freeshell 系统是从哪里启动的。事实上,跟普通的 Linux 系统一样,freeshell 是从虚拟机里的 /sbin/init 开始运行的(看看是不是 1 号进程?),后面就是虚拟机系统自己的启动过程了。

Freeshell 控制面板一角

事实上,不同类型的虚拟化技术是从不同的地方开始引导虚拟机系统的:

  • 从模拟的 BIOS 开始引导的,支持 MBR、EFI、PXE 等启动方式,如 QEMU、VMWare;
  • 从内核开始引导的,虚拟机镜像内不包含内核,如 KVM、Xen;
  • 从 init 进程开始引导的,虚拟机是一个与主机共享内核的容器,会按照操作系统的引导过程启动各种系统服务,如 LXC、OpenVZ;
  • 只运行一个特定的应用程序或服务的,也是基于容器,如 Docker。

云操作系统 OpenStack

前面讲的都是实现虚拟化的具体技术。但要让最终用户用上虚拟机,还需要有管理虚拟机的平台,或称 “云操作系统”。OpenStack 是目前最火的云操作系统。

OpenStack 云操作系统

OpenStack(或者任何靠谱的云操作系统)要对云上的各种资源进行虚拟化,它的管理组件称为 Nova。

  • 计算:本文所说的虚拟化技术就是计算的虚拟化。OpenStack 可以使用多种多样的虚拟化解决方案,如 Xen、KVM、QEMU、Docker。管理组件 Nova 根据各物理节点的负载决定把虚拟机调度到哪台物理机,再调用这些虚拟化解决方案的 API 来创建、删除、开机、关机等。
  • 存储:虚拟机镜像如果只能存储在计算节点本地,那么不仅不利于数据的冗余,也不利于虚拟机的迁移。因此在云中,一般采用逻辑上集中、物理上分布式的存储系统,独立于计算节点,也就是计算节点对数据磁盘的访问一般是通过网络访问。
  • 网络:每个客户要有自己的虚拟网络,如何让不同客户的虚拟网络在物理网络上互不干扰,就是网络虚拟化的事情,参见我的另一篇博客《网络虚拟化技术大观》。

除了最核心的虚拟化管理器 Nova,OpenStack 还有虚拟机镜像管理器 Glance、对象存储 Swift、块存储 Cinder、虚拟网络 Neutron、身份认证服务 Keystone、控制面板 Horizon 等众多组件。

OpenStack 控制面板(Horizon)

下面两张图展示了 Nova 分别使用 Docker 和 Xen 作为虚拟化解决方案的架构。

Docker 受 OpenStack Nova 计算组件的调用
使用 Xen 作为 OpenStack 的计算虚拟化方案

结语

任务隔离的需求催生了虚拟化,我们都既希望任务隔离得彻底一些,又不希望损失太多的性能。从隔离性最高但慢到不实用的模拟执行,到现代全虚拟化技术所采用的动态二进制翻译与硬件辅助虚拟化,再到修改虚拟机系统的半虚拟化,再到共享内核、基于容器的操作系统级虚拟化,性能始终是虚拟化技术浪潮的第一推动力。不过,当我们选择虚拟化技术时,还是要根据实际需求,在隔离性与性能的天平上找到平衡点。

古老的计算虚拟化技术,加上相对新生的存储、网络虚拟化技术,构成了云操作系统的基石。九层之台,起于垒土,当我们享受云端似乎取之不尽用之不竭的计算资源时,如果能剥开一层层的封装,洞悉计算的本质,也许我们会更加珍惜云端的计算资源,更加赞叹虚拟化技术大厦的宏伟与精致,也更加崇敬和膜拜那些引领人类走进云时代的计算机大师。

参考文献

  1. Understanding Full Virtualization, Paravirtualization, and Hardware Assist, VMWare, http://www.vmware.com/files/pdf/VMware_paravirtualization.pdf
  2. 虚拟化技术漫谈, IBM, http://www.ibm.com/developerworks/cn/linux/l-cn-vt/
  3. Formal requirements for virtualizable third generation architectures, CACM 1974
  4. 各种网络图片(恕不一一注明出处)

《虚拟化技术大观》有2个想法

  1. 内存的虚拟化,影子页表在实际中已经不使用了,应该简单讲讲硬件辅助内存虚拟化EPT/NPT技术

发表评论

电子邮件地址不会被公开。 必填项已用*标注