随着计算机的兴起,应用从单体架构、分离式架构逐渐演变为现在的微服务架构。而在应用的部署也从单物理机部署、VM(Virtual Machine)部署演变为 Docker 部署,逐步云原生化。
物理机、VM、Docker 的演进
物理机到 VM 的演进
在物理机上部署计算机应用时,往往会将多个应用部署到同一台物理机上。这种部署方式所暴露的最大问题就是应用集中部署,没有资源隔离。一旦某个应用导致服务器崩溃,该主机的所有服务全部不能正常使用,这是大多数系统所不能接受的。
虚拟机(VM,Virtual Machine)解决了资源调配的问题。物理资源的虚拟化技术通过 Hypervisor 层抽象底层基础设施资源,提供相互隔离的虚拟机,通过统一配置、统一管理、计算资源的可运维性以及资源利用率,可以在一台物理机上同时运行多台虚拟主机,让硬件资源的利用效率得到有效的提升。同时,虚拟机提供客户机操作系统,客户机的变化不会影响宿主机,这样能够提供可控的测试环境,更能够屏蔽底层硬件甚至基础软件的差异性,使应用广泛兼容。然而,虚拟化技术不可避免地会出现计算、I/O、 网络性能损失,究其本质是因为多了一层软件,毕竟要运行一个完整的客户机操作系统。
VM 到 Docker 的演进
容器(Container)解决的核心问题是应用开发、测试和部署的问题。传统 VM 需要在宿主机操作系统上通过 Hypervisor 对硬件资源进行虚拟化并在其上是多个完整的 OS,而 Docker 直接使用宿主机操作系统调度硬件资源,所以在资源利用率上 Docker 远超传统 VM。另外,传统 VM 的创建速度在容器面前不值一提,因为容器是利用宿主机的系统内核创建的,可以在几秒内大量创建,两者具有数量级上的差距。然而凡事都具有两面性,容器巨大性能优势的对立面是对安全和隔离问题的一系列妥协。
Docker 通过容器虚拟化、共享内核,能够把应用需要的运行环境、缓存环境、数据库环境等封装起来,以最简单的方式支持应用运行,轻装上阵,性能更佳。Docker 镜像特性则让这种方式更加简单易行。当然,因为共享内核,容器隔离性没有虚拟机那么好。通过 Docker 的特性,以容器化封装为基础,企业就可以很好地实现容器云(向云而生的架构)平台,包括但不限于微服务架构、DevOps,让开发团队可以从运维工作中解脱,集中精力在应用的快速上线、快速迭代方面。
VM 与 Docker 的虚拟化区别
Hypervisor 虚拟化内存的方法是,创建一个 Shadow Page Table。正常的情况下,一个 Page Table 可以用来实现从虚拟内存到物理内存的翻译。在虚拟化的情况下,由于所谓的物理内存仍然是虚拟的,因此 Shadow Page Table 就要做到:虚拟内存->虚拟的物理内存->真正的物理内存。
Hypervisor 虚拟化 I/O 设备的方法是,当 Hypervisor 接到 Page Fault,并发现实际上虚拟的物理内存地址对应的是一个 I/O 设备,Hypervisor 就用软件模拟这个设备的工作情况,并返回。比如当 CPU 想要写磁盘时,Hypervisor 就把相应的数据写到一个 Host OS 的文件上,这个文件实际上就模拟了虚拟的磁盘。
对比虚拟机实现资源和环境隔离的方案,Docker 就显得简练很多。根据 Docker 布道师 Jerome Petazzoni 的说法,Docker=LXC+AUFS,其中 LXC 负责资源管理,AUFS 负责镜像管理。而 LXC 又包括 CGroup、Namespace、chroot 等组件,并通过 CGroup 进行资源管理。Docker 并没有和虚拟机一样利用一个完全独立的 Guest OS 实现环境隔离,它利用的是目前 Linux 内核本身支持的容器方式实现资源和环境隔离。与虚拟化技术相比,这样既不需要指令级模拟,也不需要即时编译。容器可以在核心 CPU 本地运行指令,而不需要任何专门的解释机制,也避免了准虛拟化(paravirtualization)和系统调用替换中的复杂性。容器在提供隔离的同时,还通过共享资源节省开销,这意味着容器比传统虚拟化技术的开销要小得多。
Docker 与虚拟机计算效率比较
从原理的角度推测,Docker 应当在 CPU 和内存的利用效率上比虚拟机高。根据 IBM Research 《An Updated Performance Comparison of Virtual Machines and Linux Containers》给出的数据进行分析。
图中从左往右分别是物理机、Docker 和虚拟机的计算能力数据。可见 Docker 相对于物理机其计算能力几乎没有损耗,而虚拟机对比物理机则有着非常明显的损耗。虚拟机的计算能力损耗在 50% 左右。
为什么会有这么大的性能损耗呢?一方面是因为虚拟机增加了一层虚拟硬件层,运行在虚拟机上的应用程序在进行数值计算时是运行在 Hypervisor 虚拟的 CPU 上的;另外一方面是由于计算程序本身的特性导致的差异。虚拟机虚拟的 CPU 架构不同于实际 CPU 架构,数值计算程序一般针对特定的 CPU 架构有一定的优化措施,虚拟化使这些措施作废,甚至起到反效果。比如对于本次实验的平台,实际的 CPU 架构是 2 块物理 CPU,每块 CPU 拥有 16 个核,共 32 个核,采用的是 NUMA 架构;而虚拟机则将 CPU 虚拟化成一块拥有 32 个核的 CPU。这就导致了计算程序在进行计算时无法根据实际的 CPU 架构进行优化,大大减低了计算效率。
Docker 与虚拟机内存访问效率比较
内存访问效率的比较相对比较复杂一点,主要是内存访问有多种场景:
- 大批量的,连续地址块的内存数据读写。这种测试环境下得到的性能数据是内存带宽,性能瓶颈主要在内存芯片的性能上;
- 随机内存访问性能。这种测试环境下的性能数据主要与内存带宽、Cache 的命中率和虚拟地址与物理地址转换的效率等因素有关。
以下将主要针对这两种内存访问场景进行分析。在分析之前先概要说明一下 Docker 和虚拟机的内存访问模型差异。下图是 Docker 与虚拟机内存访问模型:
可见在应用程序内存访问上,虚拟机的应用程序要进行 2 次的虚拟内存到物理内存的映射,读写内存的代价比 Docker 的应用程序高。
下图是大批量、连续地址块内存读取场景下测试数据,即内存带宽数据。左图是程序运行在一块 CPU(即 8 核)上的数据,右图是程序运行在 2 块 CPU(即 16 核)上的数据。单位均为 GB/s。
从图中数据可以看出,在内存带宽性能上 Docker 与虚拟机的性能差异并不大。这是因为在内存带宽测试中,读写的内存地址是连续的,大批量的,内核对这种操作会进行优化(数据预存取)。因此虚拟内存到物理内存的映射次数比较少,性能瓶颈主要在物理内存的读写速度上,因此这种情况 Docker 和虚拟机的测试性能差别不大。
内存带宽测试中 Docker 与虚拟机内存访问性能差异不大的原因是由于内存带宽测试中需要进行虚拟地址到物理地址的映射次数比较少。根据这个假设,我们推测,当进行随机内存访问测试时这两者的性能差距将会变大,因为随机内存访问测试中需要进行虚拟内存地址到物理内存地址的映射次数将会变多。结果如下图所示。
左图是程序运行在一个 CPU上的数据,右图是程序运行在 2 块 CPU 上的数据。单位均是 Giga Updates per Second。从左图可以看出,确实如我们所预测的,在随机内存访问性能上容器与虚拟机的性能差距变得比较明显,容器的内存访问性能明显比虚拟机优秀;但出乎我们意料的是在 2 块 CPU 上运行测试程序时容器与虚拟机的随机内存访问性能的差距却又变的不明显。
针对这个现象,IBM 的论文给出了一个合理解释。这是因为当有 2 块 CPU 同时对内存进行访问时,内存读写的控制将会变得比较复杂,因为两块 CPU 可能同时读写同一个地址的数据,需要对内存数据进行一些同步操作,从而导致内存读写性能的损耗。这种损耗即使对于物理机也是存在的,可以看出右图的内存访问性能数据是低于左图的。2 块 CPU 对内存读写性能的损耗影响是非常大的,这个损耗占据的比例远大于虚拟机和 Docker 由于内存访问模型的不同产生的差异,因此在右图中 Docker 与虚拟机的随机内存访问性能上我们看不出明显差异。
Docker 与虚拟机启动时间及资源耗费比较
上面两个小节主要从运行在 Docker 里的程序和运行在虚拟机里的程序进行性能比较。事实上,Docker 之所以如此受到开发者关注的另外一个重要原因是启动 Docker 的系统代价比启动一台虚拟机的代价要低得多:无论从启动时间还是从启动资源耗费角度来说。Docker 直接利用宿主机的系统内核,避免了虚拟机启动时所需的系统引导时间和操作系统运行的资源消耗。利用 Docker 能在几秒钟之内启动大量的容器,这是虚拟机无法办到的。快速启动、低系统资源消耗的优点使 Docker 在弹性云平台和自动运维系统方面有着很好的应用前景。
Docker 的劣势
前面的内容主要论述 Docker 相对于虚拟机的优势,但 Docker 也不是完美的系统。相对于虚拟机,Docker 还存在着以下几个缺点:
- 资源隔离方面不如虚拟机。Docker 是利用 CGroup 实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源。
- 安全性问题。Docker 目前并不能分辨具体执行指令的用户,只要一个用户拥有执行 docker 的权限,那么他就可以对 Docker 的容器进行所有操作,不管该容器是否是由该用户创建。比如 A 和 B 都拥有执行 Docker 的权限,由于 Docker 的 Server 端并不会具体判断 Docker Client 是由哪个用户发起的,A 可以删除 B 创建的容器,存在一定的安全风险。
- Docker 目前还在版本的快速更新中,细节功能调整比较大。一些核心模块依赖于高版本内核,存在版本兼容问题。