近几年,随着微服务概念和容器化思想的风(chao)靡(zuo),Docker 技术成了各大厂和各种吹牛大会上的香饽饽,一提到 Docker,就是各种优势,大有在云计算领域一统江湖的趋势。但是 Docker 真的是万精油吗?本文简单介绍下 Docker 的一些底层技术,以及和传统虚拟机的一些差异。

开局一张图

我们先来看一张图 图1

这里先不解释上图的含义,等介绍完下面内容,再回过头看。

为什么需要 Docker 这类技术

对于应用程序开发和测试工作者来说,经常会遇到这样的场景:在开发环境下运行得好好的应用,部署到测试环境时就出问题,一会儿缺个目录,一会儿少个包,比如运行 Java,得装对应版本的 JDK,设置环境变量等。而有了 Docker,一切就简单了,首先开发机和测试机都安装好 Docker,开发把要测试的 App 和需要的库、依赖等,打成一个 Docker image,交付给测试,测试拿到这个 image,只需一个命令docker run,一个容器就运行起来,应用也就在测试环境跑起来了,如果测得没有问题,就可以拿这个 image 部署到生产环境了。整个过程中,不管有多少测试环境,只要大家拿的是相同的 image,就不会出现因为环境不同而导致应用无法运行。而升级也非常简单,停掉老容器,运行新容器。这就是 Docker 最大的优点,你只需要关注应用本身,而不需要关注应用之外的依赖和环境。 将应用程序打包成一个镜像,然后以容器的方式运行,Docker 的这个思想,大大提高了应用的开发效率,降低了运维成本。

运行 Docker 需要的内核技术

上面我们说,使用 Docker 后,就不需要关心执行环境的问题了,也就是表明,容器中的 Java 程序,用的是打包在容器中的 JDK1.8,而不会是宿主机上装的 JDK1.5,容器的执行环境和宿主机是隔离开的。那么,Docker 是怎么实现宿主机和容器的环境隔离呢?又有哪些东西是需要隔离的呢?

Docker 通过命名空间(namespace) 机制来实现隔离,而 Linux 内核提供了 6 种 namespace 隔离的系统调用,我们知道,通过fork()系统调用可以创建新进程,而clone()则是创建新进程的一种更通用的方式,其原形如下:

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

注意到其中有个 flags 的参数,调用clone()在创建新进程时,flags 支持传入类似型如 CLONE_* 的参数,使创建的进程有不同的独立的 namespace,达到隔离的目的。有 6 种 namespace 的名称、使用的参数、以及隔离效果如下:

命名空间 系统调用参数 隔离内容
UTS CLONE_NEWUTS 隔离主机名和域名,传入这个参数,新进程有自己的主机名了
IPC CLONE_NEWIPC 隔离信号量,消息队列和共享内容,新进程无法通过信号量的方式感知命名空间外的进程
PID CLONE_NEWPID 隔离进程编号,新进程的 pid 和 宿主机 pid 是独立开了
Network CLONE_NEWNET 隔离网络设备,网络栈,端口等,进程有了自己的网络资源、端口
Mount CLONE_NEWNS 挂载点,进程可以有自己的文件系统
User CLONE_NEWUSER 隔离用户和用户组

上表中的多个参数支持操作,这样我们创建出的新进程就可以隔离多项内容。 上述这 6 种命名空间,将 Linux 命名空间中的进程和命名空间外的进程区分开来,位于同一个命名空间中的进程可以感知各自存在,而且会认为这个命名空间就是整个世界环境,对命名空间外的一切是一无所知的,这样就达到了隔离目的。Docker 本质上就是一个使用 Linux 命名空间技术,来达到“虚拟化”的效果。

既然 Docker 大法这么好,那为何还要有 KVM 这一类虚拟机呢?

KVM 虚拟机

大多数朋友可能都有过在自己 Windows 电脑上装 VMWare 或者 VirtualBox 跑虚拟机的经历,运行虚拟机前,需要准备一个 ISO 镜像,然后让你选择给虚拟机分配多大内存,多大磁盘等,一切准备好后,启动虚拟机,就可以看到熟悉的装机界面了,接下来就和在物理机上装系统一样。

在 Linux 环境下,我们也有相应的虚拟化解决方案——QEMU/KVM,同样,我们创建虚拟机前,也要准备好镜像,以及定义好虚拟机的各种规格等。注意,这里说的镜像和上文 Docker 中的镜像是两个完全不同的概念,这里的镜像就是我们平时装操作系统用的镜像,里面只包括了引导程序和操作系统,而且装好后,这个镜像就不用了;而上文说的 Docker 镜像,是一个运行 App 和依赖库的最小包,或者把它理解成一个 zip 包。每次运行容器,都需要这个镜像。从创建 KVM 虚拟机的过程来看,显然它对虚拟机的各种资源有更详细的描述,基本等同于创建了一个“物理机”了,毕竟物理机有的它都有。

这里不打算讨论 Win 平台下的虚拟化,我们着重看下 Linux 下的虚拟化。最常见的虚拟化组合就是 QEMU/KVM 了。QEMU 是一款开源的模拟器和虚拟机监视器(Virtual Machine Monitor, VMM)。QEMU 主要提供两个功能给用户使用:

  • 作为用户态模拟器:通过动态代码翻译机制来执行不同架构的代码,例如在 X86 平台上模拟 ARM 平台下执行环境。
  • 作为虚拟机监视器:模拟全系统,利用其它 VMM(如 Xen,KVM等等)来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机。

**从上面描述我们可以看到,QEMU 其实是一个纯软件实现的虚拟机模拟器,但虚拟化效率很低,因此配合 KVM 等一类 VMM,利用 KVM 提供的硬件加速,使得在 QEMU 中运行的 CPU 指令,直接在宿主机的物理 CPU 上执行,使虚拟机的性能更高。**KVM 已经是 Linux 平台下的一个内核模块了,它本身不实现任何模拟,运行于内核空间,仅仅是暴露了一个 /dev/kvm 接口,由运行于用户空间的 QEMU 与之交互。当虚拟机有 CPU 操作时,QEMU 将指令转交给 KVM 模块,而 IO 仍然由 QEMU 来完成。因此,由 QEMU/KVM 组合创建的虚拟机,被称为 KVM 虚拟机。

KVM 的基本架构可以用下图来表示 图2

运行一个 KVM 虚拟机不像运行一个 Docker 那么简单,除了要安装好一系列包,还得检查你机器的 CPU 是否在硬件上支持虚拟化扩展特性参数,具体的可以参考 这篇文章

一个 KVM 虚拟机运行起来后,从宿主机的角度来看,它就是一个标准的 Linux 进程,具体来讲,是 QEMU 进程。可以通过命令行虚拟机管理工具 virsh 来对虚拟机进行开机关机等操作。

二者的比较

从图1和上面的讨论,我们看到,Docker 只是一种 Linux 容器(LXC,Linux Container),容器是什么?和虚拟机一样,本质上都是 Linux 系统中的一个进程。Docker 和虚拟机的生命周期,都是在宿主机上完成,但二者也有显著的差别。

首先是二者的实现方式,上面我们说 Docker 是 Linux 容器,这决定了,Docker 只能运行于 Linux 系统,看到这里,可能有人会说,我的 Windows 上,Mac 上现在也跑着 Docker 呢,但是请注意,这都是通过软件技术做的一些障眼法,如 Boot2Docker,它使 Docker 客户端运行在用户的操作系统,但仍然起了一个 Linux 虚拟机,上面跑着 Docker deamon 服务,还是脱离不了 Linux,这种方式比直接在 Linux 上运行 Docker, 无疑更加复杂,也丧失了 Docker 的方便性和灵活性。此外,LXC 决定了我们起一个容器,容器中只能是 Linux 平台的应用,不可能运行 Windows 平台下的程序,而 KVM 虚拟机是一种硬件全虚拟化的解决方案,不受此限制,在 Linux 机器上,我们仍然可以虚拟化出一个 Windows 系统,并运行对应平台的程序。

其次,从图1我们看到,一个宿主机上的所有的容器,都是共享宿主机的操作系统内核,这决定了,Docker 的隔离性要比 KVM 类的虚拟机弱。如果宿主机内核出了问题,这将影响其上的所有 Docker 容器。

在大规模部署和资源调度方面,Docker 和虚拟机都有成熟的方案,Docker 领域有比较流行的 K8S,Mesos 等框架,开源社区活跃;虚拟机领域著名的有 openstack,以及一些企业自研的调度平台,从热门话题来看,显然 Docker 要更胜一筹。

二者的定位

站在云计算的视角来看,KVM 虚拟机,属于 IaaS 层面产品,它给用户提供的是一套完整的基础设施,用户可以拿虚拟机做任何用途,跑运算,做 Web 服务等;而 Docker,更像是 PaaS 层的产品,它提供了一个特定的软件运行的环境,用户拿到一个 Redis 的 Docker image,那么只能跑数据库服务,而不可能跑 Web 服务。

总结

每个新技术的出现,都是为了解决特定的问题,Docker 和 KVM 虚拟机,在大的方面上,都是为了提高效率,最大化的利用物理资源。Docker 在构建、部署方面,有着虚拟机无法比拟的优势,虚拟机在云计算领域,有更广的发挥空间。

参考