关于“虚拟化”这个话题,前面讲的都是基于“虚拟机”的,这是我们接触得比较早的,但是在虚拟化领域,还有存在被称为“容器虚拟化”的技术,这种技术自 2013 年出现以来,就备受推崇,短短几年就占据了虚拟化技术的半壁江山,今天,我们就来聊一聊这方面的内容。
1. 概述
1.1 什么是容器
容器技术是通过虚拟化操作系统的方式来管理代码和应用程序,是在2013年才开始出现并迅速兴起的一种虚拟化技术,容器技术主要是由一些技术型创新企业设计开发的,比如: BlueData、CoreOS、Docker、Kismatic、PortWorx 等等,其中最为出名的要属 Docker 技术,时至今日,Docker 几乎成为了容器技术的代表,我们今天也仅讨论 Docker 容器。
容器技术是让你可以将更多的计算工作负载加载到一台服务器上,并且可以在一瞬间为新的计算任务提高增加容量,而且不必消耗运行 Hypervisor 所需要的额外负载。
每个容器内都包含一个独享的完整用户环境空间,并且一个容器内的变动不会影响其他容器的运行环境。为了能达到这种效果,容器技术使用了一系列的系统级别的机制,诸如利用 Linux namespaces 来进行空间隔离,通过文件系统的挂载点来决定容器可以访问哪些文件,通过 cgroups 来确定每个容器可以利用多少资源。此外容器之间共享同一个系统内核,这样当同一个库被多个容器使用时,内存的使用效率会得到提升。
1.2 虚拟机和容器的区别
虚拟机和容器两大技术,从描述上看目的都是虚拟化,但是技术热点和实现方式则完全不同,简单来说,虚拟机技术虚拟出机器,让每个实例看到一个单独的机器;而 Docker 是虚拟出操作系统(或者说是应用的运行环境),实现应用之间的隔离,让各个应用觉得自己有一个自己的操作系统,而且彼此之间隔离。但 Docker 又没有虚拟机的开销,它虚拟的层次更加高。接下来,我们看看二者之间的主要区别:
(1)实现方式不同
使用虚拟机运行多个相互隔离的应用时,如下图所示:
从下到上理解上图:
— 基础设施(Infrastructure)。它可以是你的个人电脑,数据中心的服务器,或者是云主机。
— 主操作系统(Host Operating System,即 Host OS)。你的个人电脑之上,运行的可能是 MacOS,Windows 或者某个 Linux 发行版。
— 虚拟机管理系统(Hypervisor)。利用 Hypervisor,可以在主操作系统之上运行多个不同的从操作系统。类型 1(裸金属)的 Hypervisor 有支持 MacOS 的 HyperKit,支持 Windows 的 Hyper-V 以及支持 Linux 的 KVM。类型 2(宿主型) 的 Hypervisor 有 VirtualBox 和 VMWare。
— 从操作系统(Guest Operating System,即 Guest OS)。假设你需要运行 3 个相互隔离的应用,则需要使用 Hypervisor 启动 3 个从操作系统,也就是 3 个虚拟机。这些虚拟机都非常大,也许有 700MB,这就意味着它们将占用 2.1GB 的磁盘空间。更糟糕的是,它们还会消耗很多 CPU 和内存。
— 各种依赖。每一个从操作系统都需要安装许多依赖。如果你的的应用需要连接 PostgreSQL 的话,则需要安装 libpq-dev;如果你使用 Ruby 的话,应该需要安装 gems;如果使用其他编程语言,比如 Python 或者 Node.js,都会需要安装对应的依赖库。
— 应用。安装依赖之后,就可以在各个从操作系统分别运行应用了,这样各个应用就是相互隔离的。
而使用 Docker 容器运行多个相互隔离的应用时,如下图所示:
不难发现,相比于虚拟机,Docker 要简洁很多。因为我们不需要运行一个臃肿的 Guest OS 从操作系统了。
还是让我们从下到上来理解上图:
— 基础设施(Infrastructure)。
— 主操作系统(Host Operating System,即 Host OS)。所有主流的 Linux 发行版都可以运行 Docker。对于 MacOS 和 Windows,也有一些办法”运行” Docker。
— Docker 守护进程(Docker Daemon)。Docker 守护进程取代了 Hypervisor,它是运行在操作系统之上的后台进程,负责管理 Docker 容器。
— 各种依赖。对于 Docker,应用的所有依赖都打包在 Docker 镜像中,Docker 容器是基于 Docker 镜像创建的。
— 应用。应用的源代码与它的依赖都打包在 Docker 镜像中,不同的应用需要不同的Docker 镜像。不同的应用运行在不同的 Docker 容器中,它们是相互隔离的。
(2)性能特征不同
虚拟机提供了专用操作系统的安全性和更牢固的逻辑边界。如果是虚拟机,虚拟机管理程序与硬件对话,就如同虚拟机的操作系统和应用程序构成了一个单独的物理机。虚拟机中的操作系统可以完全不同于主机的操作系统。
而容器具有轻量级特性,所需的内存空间较少,提供非常快的启动速度,创建容器的速度比虚拟机要快得多,那是由于虚拟机必须从存储系统检索 10GB 至 20GB 的操作系统。容器中的工作负载使用主机服务器的操作系统内核,避免了这一步,容器可以在二十分之一秒内启动完毕。拥有这么快的速度让开发团队可以激活项目代码,以不同的方式测试代码,或者在其网站上推出额外的电子商务容量――这一切都非常快。
(3)移植方式不同
虚拟机和容器都具有高度可移植性,但方式不一样。就虚拟机而言,可以在运行同一虚拟机管理程序(通常是 VMware 的 ESX、微软的 Hyper-V 或者开源 Xen 或 KVM)的多个系统之间进行移植。
而容器不需要虚拟机管理程序,因为它与某个版本的操作系统绑定在一起。但是容器中的应用程序可以移到任何地方,只要那里有一份该操作系统的副本。
容器技术使得应用程序以标准方式进行了格式化,之后才放到容器中。开发人员可以使用同样的工具和工作流程,不管目标操作系统是什么。一旦在容器中,每种类型的应用程序都以同样的方式在网络上移动。
最后我们用一张表来对比一下虚拟机和 Docker 的特性差别:
需要注意的是,虽然 Docker 等容器技术在特性上优于虚拟机技术,但是虚拟机和容器两者有不同的使用场景,所以不能简单的对比孰优孰劣。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker 通常用于隔离不同的应用,例如前端,后端以及数据库。
2. 关于 Docker
2.1 Docker 简介
Docker 是一个开源项目,诞生于 2013 年初, 最初是 dotCloud 公司后改名为 Docker lnc 内部的一个业余项目。 它基于 Google 公司推出的 Go 语言实现。项目后来加入了 Linux 基金会, 遵从了 Apache 2.0 协议, 项目代码在 GitHub 上进行维护。
Docker 作为一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化;可以方便的开发微服务,也可以弹性的实现配置资源;还可以方便的实现模板模式。
需要注意的是 Docker 容器是完全使用沙箱机制,相互之间不会有任何接口,而且其只能运行在 64 位系统上。
如下图所示,Docker 的初衷就是将各种应用程序和他们所依赖的运行环境打包成标准的container/image,进而发布到不同的平台上运行。
从理论上说这一概念并不新鲜, 各种虚拟机 Image (镜像)也起着类似的作用。Docker container 和普通的虚拟机 Image 相比, 最大的区别是它并不包含操作系统内核,如下图所示:
普通虚拟机将整个操作系统运行在虚拟的硬件平台上, 进而提供完整的运行环境供应用程序运行, 而 Docker 则直接在宿主平台上加载运行应用程序。
本质上他在底层使用 LXC(Linux Container)启动一个 Linux Container,通过 cgroup(cgroups 子系统是 Linux 内核提供的一个基于进程组的资源管理的框架,可以为特定的进程组限定可以使用的资源) 等机制对不同的 container 内运行的应用程序进行隔离,权限管理和 quota 分配等。
而 LXC 在隔离控制方面依赖于 Linux 内核的 namespace 特性,具体而言就是在 clone 时加入相应的 flag(NEWNS NEWPID 等等)。也就是说 Docker 的每个 container 拥有自己独立的各种命名空间(namespace,亦即资源)包括PID(Process ID)进程,MNT(Mount)文件系统,NET 网络,IPC(InterProcess Communication),UTS(Unix Timesharing System)主机名等。
基本上咱们可以认为目前的 Docker 是 LXC 的一个高级封装,提供了各种辅助工具和标准接口方便你使用 LXC,在LXC的基础上,Docker额外提供的Feature包括:标准统一的打包部署运行方案,历史版本控制,Image的重用,Image共享发布等等。你可以依靠 LXC 和各种脚本实现与 Docker 类似的功能,就像我们不使用 APT/yum 等工具也可以自己搞定软件包安装一样,方便易用!
实际使用中,我们一般不用关心底层 LXC 的细节,同时也不排除将来 Docker 实现基于非LXC 方案的可能性。
2.2 Docker 的核心概念
Docker 包括两个主要部件,分别是:
Docker:开源的容器虚拟化平台。
Docker Hub:用于分享、管理 Docker 容器的 Docker SaaS 平台。
Docker 使用客户端-服务器 (C/S) 架构模式。Docker 客户端会与 Docker 守护进程进行通信。Docker 守护进程会处理复杂繁重的任务,例如建立、运行、发布 Docker 容器。Docker 客户端和守护进程可以运行在同一个系统上,当然你也可以使用 Docker 客户端去连接一个远程的 Docker 守护进程。Docker 客户端和守护进程之间通过 socket 或者 RESTful API 进行通信。
(1)Docker Deamon 守护进程
Docker 守护进程运行在一台主机上。用户并不直接和守护进程进行交互,而是通过 Docker 客户端间接和其通信。
(2)Docker Client 客户端
Docker 客户端,实际上是 docker 的二进制程序,是主要的用户与 Docker 交互方式。它接收用户指令并且与背后的 Docker 守护进程通信,如此来回往复。
(3)Namespace 命名空间(让资源“看起来”被隔离开)
以下 6 种 namespace 从进程、网络、IPC、文件系统、UTS 和用户角度的隔离,一个 container 就可以对外展现出一个独立计算机的能力,并且不同 container 从 OS 层面实现了隔离。 然而不同 namespace 之间资源还是相互竞争的,仍然需要类似 ulimit 来管理每个 container 所能使用的资源 - cgroup。
— PID namespace
不同用户的进程就是通过 PID namespace 隔离开的,且不同 namespace 中可以有相同 PID。具有以下特征:
1)每个 namespace 中的 pid 是有自己的 pid=1 的进程(类似 /sbin/init 进程);
2)每个 namespace 中的进程只能影响自己的同一个 namespace 或子 namespace 中的进程;
3)因为 /proc 包含正在运行的进程,因此在 container 中的 pseudo-filesystem 的 /proc 目录只能看到自己 namespace 中的进程;
4)因为 namespace 允许嵌套,父 namespace 可以影响子 namespace 的进程,所以子 namespace 的进程可以在父 namespace 中看到,但是具有不同的 pid。
— MNT namespace
类似 chroot,将一个进程放到一个特定的目录执行。mnt namespace 允许不同 namespace 的进程看到的文件结构不同,这样每个 namespace 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个 namespace 中的 container 在 /proc/mounts 的信息只包含所在 namespace 的 mount point。
— NET namespace
网络隔离是通过 net namespace 实现的, 每个 net namespace 有独立的 network devices, IP addresses, IP routing tables, /proc/net 目录。这样每个 container 的网络就能隔离开来。 docker 默认采用 veth 的方式将 container 中的虚拟网卡同 host 上的一个 docker bridge 连接在一起。
— UTS namespace
UTS(UNIX Time-sharing System)namespace 允许每个 container 拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非 Host 上的一个进程。
— IPC namespace
container 中进程交互还是采用 Linux 常见的进程间交互方法(interprocess communication - IPC),包括常见的信号量、消息队列和共享内存。然而同 VM 不同,container 的进程间交互实际上还是 host 上具有相同 pid namespace 中的进程间交互,因此需要在IPC资源申请时加入 namespace 信息 - 每个 IPC 资源有一个唯一的 32bit ID。
— USER namespace
每个 container 可以有不同的 user 和 group id, 也就是说可以以 container 内部的用户在 container 内部执行程序而非 Host 上的用户。
(4)cgroups 配额资源或者叫控制组(control group,让资源“用起来”是隔离开)
cgroups 实现了对资源的配额和度量。 cgroups 的使用非常简单,提供类似文件的接口,在 /cgroup 目录下新建一个文件夹即可新建一个 group,在此文件夹中新建 task 文件,并将 pid 写入该文件,即可实现对该进程的资源控制。具体的资源配置选项可以在该文件夹中新建子 subsystem ,. 是典型的配置方法。
例如 memory.usageinbytes 就定义了该 group 在 subsystem memory 中的一个内存限制选项。 另外,cgroups 中的 subsystem 可以随意组合,一个 subsystem 可以在不同的 group 中,也可以一个 group 包含多个 subsystem - 也就是说一个 subsystem。
— memory:内存相关的限制。
— cpu:在 cgroup 中,并不能像硬件虚拟化方案一样能够定义 CPU 能力,但是能够定义 CPU 轮转的优先级,因此具有较高 CPU 优先级的进程会更可能得到 CPU 运算。 通过将参数写入 cpu.shares ,即可定义改 cgroup 的 CPU 优先级 - 这里是一个相对权重,而非绝对值。
— blkio:block I\O 相关的统计和限制,byte/operation 统计和限制 (IOPS 等),读写速度限制等,但是这里主要统计的都是同步 I\O。
— devices:设备权限限制
(5)UnionFS(联合文件系统)
Union 文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对 文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。
Docker 中使用的 AUFS(AnotherUnionFS)就是一种 Union FS。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。
Docker 目前支持的 Union 文件系统种类包括 AUFS, btrfs, vfs 和 DeviceMapper。
关于容器和 Docker 的基本概念,咱们先聊到这里,下一篇,咱们就聊一下 Docker 容器的架构。
图片授权基于:CC0协议
领取专属 10元无门槛券
私享最新 技术干货