Docker:用于一致开发和部署的轻量级 Linux 容器
使用 Docker 容器(VM 的轻量级和灵活的表亲)应对“依赖地狱”。了解 Docker 如何通过基于 LXC 技术将应用程序打包在容器中来使其可移植和隔离。
想象一下,能够轻松地将应用程序及其所有依赖项打包在一起,然后在不同的开发、测试和生产环境中顺利运行它。这就是开源 Docker 项目的目标。尽管它仍未正式准备好用于生产环境,但最新版本(在撰写本文时为 0.7.x)使 Docker 离实现这一雄心勃勃的目标又近了一步。
Docker 尝试解决“依赖地狱”的问题。现代应用程序通常由现有组件组装而成,并依赖于其他服务和应用程序。例如,您的 Python 应用程序可能使用 PostgreSQL 作为数据存储、Redis 用于缓存,以及 Apache 作为 Web 服务器。这些组件中的每一个都带有自己的一组依赖项,这些依赖项可能与其他组件的依赖项冲突。通过打包每个组件及其依赖项,Docker 解决了以下问题
-
依赖冲突:需要在 PHP 4.3 上运行一个网站,在 PHP 5.5 上运行另一个网站?如果您在单独的 Docker 容器中运行每个版本的 PHP,则没有问题。
-
缺少依赖项:使用 Docker 在新环境中安装应用程序非常简单,因为所有依赖项都与应用程序一起打包在容器中。
-
平台差异:从一个发行版移动到另一个发行版不再是问题。如果两个系统都运行 Docker,则相同的容器将毫无问题地执行。
Docker 最初是 dotCloud(一家以云为中心的平台即服务公司)在 2013 年初启动的一个开源项目。最初,Docker 是该公司为在其数千台服务器上运行云业务而开发的技术的自然延伸。它使用 Go 编写,Go 是一种由 Google 开发的静态类型编程语言,其语法大致基于 C。快进六到九个月,该公司聘请了一位新 CEO,加入了 Linux 基金会,将其名称更改为 Docker Inc.,并宣布它正在将其重点转移到 Docker 和 Docker 生态系统的开发上。作为 Docker 受欢迎程度的进一步体现,在撰写本文时,它在 GitHub 上获得了 8,985 次星标,并被 fork 了 1,304 次。图 1 说明了 Docker 在 Google 搜索中的受欢迎程度不断上升。我预测,过去 12 个月的增长幅度与未来 12 个月相比将相形见绌,因为 Docker Inc. 将发布第一个被批准用于生产环境容器部署的版本,并且更广泛的社区将意识到 Docker 的实用性。

图 1. 过去 12 个月“Docker 软件”的 Google 趋势图
底层原理Docker 利用了一些强大的内核级技术,并将它们触手可及。虚拟化中容器的概念已经存在多年,但通过为管理某些内核级技术(例如 LXC(LinuX 容器)、cgroups 和写时复制文件系统)提供一套简单的工具和统一的 API,Docker 创建了一个大于其各部分之和的工具。结果是 DevOps、系统管理员和开发人员的潜在游戏规则改变者。
Docker 提供了使创建和使用容器尽可能简单的工具。容器将进程彼此隔离。目前,您可以将容器视为虚拟机的轻量级等效物。
Linux 容器和 LXC(Linux 容器的用户空间控制包)构成了 Docker 的核心。LXC 使用内核级命名空间将容器与主机隔离。用户命名空间分隔了容器和主机的用户数据库,从而确保容器的 root 用户在主机上没有 root 权限。进程命名空间负责仅显示和管理容器中运行的进程,而不是主机中的进程。并且,网络命名空间为容器提供了自己的网络设备和虚拟 IP 地址。
LXC 提供的 Docker 的另一个组件是控制组 (cgroups)。虽然命名空间负责主机和容器之间的隔离,但控制组实现资源核算和限制。在允许 Docker 限制容器消耗的资源(例如内存、磁盘空间和 I/O)的同时,cgroups 还输出有关这些资源的大量指标。这些指标允许 Docker 监视容器内各种进程的资源消耗,并确保每个进程仅获得其应得的可用资源份额。
除了上述组件外,Docker 一直使用 AuFS(高级多层统一文件系统)作为容器的文件系统。AuFS 是一种分层文件系统,可以透明地覆盖一个或多个现有文件系统。当进程需要修改文件时,AuFS 会创建该文件的副本。AuFS 能够将多个层合并为文件系统的单个表示形式。此过程称为写时复制。
真正酷的是,AuFS 允许 Docker 使用某些镜像作为容器的基础。例如,您可能有一个 CentOS Linux 镜像,可以用作许多不同容器的基础。感谢 AuFS,只需要一个 CentOS 镜像副本,这节省了存储和内存,并加快了容器的部署速度。
使用 AuFS 的另一个好处是 Docker 能够对容器镜像进行版本控制。每个新版本都只是与先前版本相比的更改差异,从而有效地将镜像文件保持在最低限度。但是,这也意味着您始终拥有从一个容器版本到另一个容器版本所发生更改的完整审计跟踪。
传统上,Docker 一直依赖 AuFS 来提供写时复制存储机制。但是,最近添加的存储驱动程序 API 可能会减少这种依赖性。最初,有三个存储驱动程序可用:AuFS、VFS 和 Device-Mapper,这是与 Red Hat 合作的结果。
从 0.7 版本开始,Docker 可以与所有 Linux 发行版一起使用。但是,它不适用于大多数非 Linux 操作系统,例如 Windows 和 OS X。在这些操作系统上使用 Docker 的推荐方法是在 VirtualBox 上使用 Vagrant 配置虚拟机。
容器与其他类型的虚拟化那么,容器到底是什么?它与基于 hypervisor 的虚拟化有何不同?简单来说,容器在操作系统级别进行虚拟化,而基于 hypervisor 的解决方案在硬件级别进行虚拟化。虽然效果相似,但差异是重要且显着的,这就是为什么我将花一些时间探讨这些差异以及由此产生的差异和权衡。
虚拟化
容器和 VM 都是虚拟化工具。在 VM 方面,hypervisor 使硬件的隔离切片可用。hypervisor 通常有两种类型:“Type 1”直接在硬件的裸机上运行,而“Type 2”作为 guest 操作系统中的附加软件层运行。虽然开源 Xen 和 VMware 的 ESX 是 Type 1 hypervisor 的示例,但 Type 2 的示例包括 Oracle 的开源 VirtualBox 和 VMware Server。尽管 Type 1 更适合与 Docker 容器进行比较,但在本文的其余部分中,我不会区分这两种类型。
相比之下,容器使操作系统的受保护部分可用——它们有效地虚拟化了操作系统。在同一操作系统上运行的两个容器不知道它们正在共享资源,因为每个容器都有自己抽象的网络层、进程等等。
操作系统和资源
由于基于 hypervisor 的虚拟化仅提供对硬件的访问,因此您仍然需要安装操作系统。因此,有多个成熟的操作系统在运行,每个 VM 中一个,这会迅速消耗服务器上的资源,例如 RAM、CPU 和带宽。
容器在其主机环境中使用已在运行的操作系统。它们仅在彼此隔离且与主机操作系统的某些部分隔离的空间中执行。这有两个显着的好处。首先,资源利用率更高。如果容器未执行任何操作,则它不会占用资源,并且容器可以调用其主机操作系统来满足其部分或全部依赖项。其次,容器便宜,因此创建和销毁速度快。无需启动和关闭整个操作系统。相反,容器仅需终止在其隔离空间中运行的进程。因此,启动和停止容器更类似于启动和退出应用程序,并且速度一样快。
图 2 说明了两种类型的虚拟化和容器。

图 2. VM 与容器
用于性能和安全性的隔离
在 Docker 容器中执行的进程与在主机操作系统或其他 Docker 容器中运行的进程隔离。然而,所有进程都在同一个内核中执行。Docker 利用 LXC 为容器提供单独的命名空间,这项技术已经在 Linux 内核中存在了 5 年以上,并且被认为相当成熟。它还使用控制组(Control Groups),该技术在 Linux 内核中存在的时间更长,用于实现资源审计和限制。
Docker 守护程序本身也构成潜在的攻击媒介,因为它当前以 root 权限运行。对 LXC 和 Docker 的改进应允许容器在没有 root 权限的情况下运行,并在不同的系统用户下执行 Docker 守护程序。
尽管所提供的隔离类型总体上非常强大,但可以说不如虚拟机在 hypervisor 级别强制执行的隔离类型强大。如果内核崩溃,所有容器也会崩溃。VM 具有优势的另一个领域是它们的成熟度和在生产环境中的广泛采用。VM 已经过强化,并在许多不同的高可用性环境中证明了自己。相比之下,Docker 及其支持技术还没有经历过那么多实践。特别是 Docker 每天都在经历巨大的变化,我们都知道变化是安全性的敌人。
Docker 和 VM — 亦敌亦友
既然我已经花费了所有时间比较 Docker 和 VM,现在是时候承认这两种技术实际上可以互补了。Docker 在已虚拟化的环境中也能很好地运行。您显然不希望承担将每个应用程序或组件封装在单独的 VM 中的成本,但是给定一个 Linux VM,您可以轻松地在其上部署 Docker 容器。这就是为什么在非 Linux 系统(例如 OS X 和 Windows)上使用 Docker 的官方支持方式是借助 Vagrant 安装 Precise64 基础 Ubuntu 虚拟机的原因,这不足为奇。在 http://www.docker.io 站点上提供了简单的详细说明。
最重要的是,虚拟化和容器表现出一些相似之处。最初,将容器视为非常轻量级的虚拟化是有帮助的。但是,随着您在容器上花费更多时间,您将开始理解细微但重要的差异。Docker 在利用容器化的优势方面做得很好,其重点明确,即应用程序的轻量级打包和部署。
Docker 仓库Docker 的一项杀手级功能是能够快速查找、下载和启动其他开发人员创建的容器镜像。存储镜像的位置称为仓库,Docker Inc. 提供了一个公共仓库,也称为中央索引。您可以将仓库以及 Docker 客户端视为 Node 的 NPM、Perl 的 CPAN 或 Ruby 的 RubyGems 的等效物。
除了各种基本镜像(您可以使用它们来创建自己的 Docker 容器)外,公共 Docker 仓库还提供现成可用的软件镜像,包括数据库、内容管理系统、开发环境、Web 服务器等等。虽然 Docker 命令行客户端默认搜索公共仓库,但也可以维护私有仓库。这对于在公司内部发布具有专有代码或组件的镜像来说是一个很好的选择。将镜像推送到仓库与下载一样容易。它需要您创建一个帐户,但这也是免费的。最后,Docker Inc. 的仓库具有基于 Web 的界面,用于搜索、阅读、评论和推荐(又名“星标”)镜像。它非常易于使用,我鼓励您点击本文“资源”部分中的链接并开始探索。
Docker 实践Docker 由一个二进制文件组成,该二进制文件可以通过三种不同的方式运行。首先,它可以作为守护程序运行以管理容器。守护程序公开了一个基于 REST 的 API,可以从本地或远程访问。越来越多的客户端库可用于与守护程序的 API 交互,包括 Ruby、Python、JavaScript(Angular 和 Node)、Erlang、Go 和 PHP。
客户端库非常适合以编程方式访问守护程序,但更常见的用例是从命令行发出指令,这是 Docker 二进制文件的第二种使用方式,即作为基于 REST 的守护程序的命令行客户端。
第三,Docker 二进制文件充当远程镜像仓库的客户端。构成容器文件系统的标记镜像称为仓库。用户可以拉取其他人提供的镜像,并通过将自己的镜像推送到仓库来共享它们。仓库用于收集、列出和组织仓库。
让我们看看运行 docker 可执行文件的所有三种方式的实际操作。在此示例中,您将在 Docker 仓库中搜索 MySQL 镜像。找到您喜欢的镜像后,您将下载它,并告诉 Docker 守护程序运行命令 (MySQL)。您将从命令行完成所有这些操作。

图 3. 拉取 Docker 镜像并启动容器
首先发出 docker search mysql
命令,然后显示公共 Docker 仓库中与关键字“mysql”匹配的镜像列表。没有特别的原因,只是因为我知道它有效,所以让我们下载“brice/mysql”镜像,您可以使用 docker pull brice/mysql
命令执行此操作。您可以看到 Docker 不仅下载了指定的镜像,还下载了构建在其上的镜像。使用 docker images
命令,您可以列出当前本地可用的镜像,其中包括“brice/mysql”镜像。使用 -d
选项启动容器以从当前正在运行的容器中分离,您现在已在容器中运行 MySQL。您可以使用 docker ps
命令进行验证,该命令列出容器,而不是镜像。在输出中,您还可以看到 MySQL 正在侦听的端口,这是默认端口 3306。
但是,您如何连接到 MySQL,因为它在容器内运行?请记住,Docker 容器有自己的网络接口。您需要找到 mysqld 服务器进程正在侦听的 IP 地址和端口。docker inspect <imageId>
提供了很多信息,但由于您只需要 IP 地址,因此您可以在检查容器时通过提供其哈希值 docker inspect 5a9005441bb5 | grep IPAddress
来 grep 它。现在,您可以通过指定主机和端口选项来使用标准 MySQL CLI 客户端进行连接。完成 MySQL 服务器后,您可以使用 docker stop 5a9005441bb5
将其关闭。
找到、下载和启动 Docker 容器以运行 MySQL 服务器,并在完成后将其关闭,总共花费了七个命令。在此过程中,您不必担心与已安装软件、MySQL 的不同版本或依赖项的冲突。您使用了七个不同的 Docker 命令:search、pull、images、run、ps、inspect 和 stop,但 Docker 客户端实际上提供了 33 个不同的命令。您可以通过从命令行运行 docker help
或查阅在线手册来查看完整列表。
在上面的示例中练习 Docker 之前,我提到客户端通过基于 REST 的 Web 服务与守护程序和 Docker 仓库进行通信。这意味着您可以使用本地 Docker 客户端与远程守护程序交互,从而有效地管理远程计算机上的容器。Docker 守护程序、仓库和索引的 API 都经过良好记录,并附有示例,可在 Docker 站点上找到(请参阅“资源”)。
Docker 工作流程Docker 可以通过多种方式集成到开发和部署过程中。让我们看一下图 4 中说明的示例工作流程。我们假设公司的开发人员可能正在运行安装了 Docker 的 Ubuntu。他可能会将 Docker 镜像推送到公共仓库或从中拉取镜像,以用作安装自己的代码和公司专有软件的基础,并生成他推送到公司私有仓库的镜像。
在此示例中,公司的 QA 环境正在运行 CentOS 和 Docker。每当环境更新时,它都会从公共和私有仓库拉取镜像并启动各种容器。
最后,该公司将其生产环境托管在云中,即 Amazon Web Services 上,以实现可伸缩性和弹性。Amazon Linux 也正在运行 Docker,它正在管理各种容器。
请注意,所有三个环境都在运行不同版本的 Linux,所有这些版本都与 Docker 兼容。此外,这些环境正在运行各种容器组合。但是,由于每个容器都对其自身的依赖项进行隔离,因此没有冲突,并且所有容器都愉快地共存。

图 4. 使用 Docker 的示例软件开发工作流程
至关重要的是要理解,Docker 提倡以应用程序为中心的容器模型。也就是说,容器应运行单个应用程序或服务,而不是运行一大堆应用程序或服务。请记住,容器的创建和运行速度快且资源消耗低。遵循单一职责原则并在每个容器中运行一个主要进程会导致系统组件的松耦合。考虑到这一点,让我们创建您自己的镜像,从中启动容器。
创建新的 Docker 镜像在前面的示例中,您从命令行与 Docker 进行了交互。但是,在创建镜像时,更常见的是创建“Dockerfile”以自动化构建过程。Dockerfile 是描述构建过程的简单文本文件。您可以将 Dockerfile 置于版本控制之下,并拥有完全可重复的方式来创建镜像。
对于下一个示例,请参考“PHP Box”Dockerfile(清单 1)。
清单 1. PHP Box
# PHP Box
#
# VERSION 1.0
# use centos base image
FROM centos:6.4
# specify the maintainer
MAINTAINER Dirk Merkel, dmerkel@vivantech.com
# update available repos
RUN wget http://dl.fedoraproject.org/pub/epel/6/x86_64/
↪epel-release-6-8.noarch.rpm; rpm -Uvh epel-release-6-8.noarch.rpm
# install some dependencies
RUN yum install -y curl git wget unzip
# install Apache httpd and dependencies
RUN yum install -y httpd
# install PHP and dependencies
RUN yum install -y php php-mysql
# general yum cleanup
RUN yum install -y yum-utils
RUN package-cleanup --dupes; package-cleanup --cleandupes;
↪yum clean -y all
# expose mysqld port
EXPOSE 80
# the command to run
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"]
让我们仔细看看此 Dockerfile 中发生了什么。Dockerfile 的语法是命令关键字,后跟该命令的参数。按照惯例,命令关键字大写。注释以井号字符开头。
FROM 关键字指示要用作基础的镜像。这必须是文件中的第一个指令。在本例中,您将在最新的 CentOS 基础镜像之上构建。MAINTAINER
指令显然列出了 Dockerfile 的维护者。RUN
指令执行命令并提交生成的镜像,从而创建一个新层。Dockerfile 中的 RUN
命令获取其他仓库的配置文件,然后使用 Yum 安装 curl、git、wget、unzip、httpd、php-mysql 和 yum-utils。我可以将 yum install
命令组合成单个 RUN
指令,以避免连续提交。
然后,EXPOSE
指令公开端口 80,这是您启动容器时 Apache 将侦听的端口。
最后,CMD
指令将提供在启动容器时要运行的默认命令。将单个进程与容器的启动相关联,使您可以将容器视为命令。
在命令行上键入 docker build -t php_box .
现在将告诉 Docker 使用当前工作目录中的 Dockerfile 启动构建过程。生成的镜像将被标记为“php_box”,这将使以后更容易引用和识别该镜像。
构建过程下载基础镜像,然后安装 Apache httpd 以及所有依赖项。完成后,它将返回一个哈希值,用于标识新创建的镜像。与您之前启动的 MySQL 容器类似,您可以使用“php_box”标记和以下命令行运行 Apache 和 PHP 镜像:docker run -d -t php_box
。
让我们用一个快速示例来结束,该示例说明了在现有镜像之上分层以创建新镜像有多么容易
# MyApp
#
# VERSION 1.0
# use php_box base image
FROM php_box
# specify the maintainer
MAINTAINER Dirk Merkel, dmerkel@vivantech.com
# put my local web site in myApp folder to /var/www
ADD myApp /var/www
第二个 Dockerfile 比第一个 Dockerfile 短,实际上只包含两个有趣的指令。首先,您使用 FROM
指令将“php_box”镜像指定为起点。其次,您使用 ADD
指令将本地目录复制到镜像。在本例中,它是正在复制到镜像上 Apache 的 DOCUMENT_ROOT 文件夹的 PHP 项目。结果是,当您启动镜像时,默认情况下将提供该站点。
Docker 轻量级打包和部署应用程序及依赖项的前景令人兴奋,它正在被 Linux 社区快速采用,并正在进入生产环境。例如,Red Hat 在 12 月宣布在即将推出的 Red Hat Enterprise Linux 7 中支持 Docker。但是,Docker 仍然是一个年轻的项目,并且正在以惊人的速度增长。观看该项目接近其 1.0 版本(预计是第一个正式批准用于生产环境的版本)将令人兴奋。Docker 依赖于成熟的技术,其中一些技术已经存在了十多年,但这并不会使其变得不那么具有革命性。希望本文为您提供了足够的信息和启发,让您下载 Docker 并亲自进行实验。
Docker 更新在本文发布时,Docker 团队宣布发布 0.8 版本。最新版本增加了对 Mac OS X 的支持,包括两个组件。虽然客户端在 OS X 上原生运行,但 Docker 守护程序在基于 VirtualBox 的轻量级 VM 中运行,该 VM 可以使用随附的命令行客户端 boot2docker 轻松管理。这种方法是必要的,因为底层技术(例如 LXC 和命名空间)OS X 根本不支持。我认为我们可以预期其他平台(包括 Windows)也会有类似的解决方案。
0.8 版本还引入了几个新的构建器功能和对 BTRFS(B 树文件系统)的实验性支持。BTRFS 是另一个写时复制文件系统,BTRFS 存储驱动程序被定位为 AuFS 驱动程序的替代方案。
最值得注意的是,Docker 0.8 带来了许多错误修复和性能增强。这种对质量的总体承诺标志着 Docker 团队努力生产可在生产环境中使用的 1.0 版本。随着团队致力于每月发布周期,我们可以期待 1.0 版本在四月到五月的时间范围内发布。
资源Docker 主站点:https://www.docker.io
Docker 仓库:https://index.docker.io
Docker 仓库 API:http://docs.docker.io/en/latest/api/registry_api
Docker 索引 API:http://docs.docker.io/en/latest/api/index_api
Docker 远程 API:http://docs.docker.io/en/latest/api/docker_remote_api