在深入配置和安装Docker之前,我们需要先进行一个广泛的调查,来解释Docker是什么以及它的优势。从本质上讲,Docker是一种强大的技术,但并不是非常复杂的技术。在本章中,我们将介绍Docker和Linux容器的一般工作原理,以及它们的强大之处,以及您可能使用它们的一些原因。如果您正在阅读本章,您可能已经有了使用容器的原因,但在着手使用之前增加对它们的理解是非常有益的。 不用担心 - 本章不会占用太多时间。在下一章中,我们将立即开始安装和运行Docker在您的系统上。
流程简化
由于Docker是一款软件,如果能够采用和实施得当,它对公司和团队流程也可能产生巨大的积极影响,这一点可能并不明显。因此,让我们深入研究一下,看看Docker和Linux容器如何简化工作流程和沟通。这通常从部署开始。传统上,将一个应用程序部署到生产环境通常遵循以下步骤(如图2-1所示):
根据我们的经验,遵循传统流程,将全新的应用程序部署到生产环境可能需要花费大部分时间,尤其对于一个复杂的新系统而言,可能需要一个星期的时间。这并不是很高效,尽管DevOps实践致力于消除许多障碍,但通常仍需要团队间的大量努力和沟通。这个过程既可能在技术上具有挑战性和成本高昂,更糟糕的是,它可能限制开发团队将来进行的创新。如果部署新软件是困难、耗时和依赖于另一个团队的资源,那么开发人员可能会将所有内容都集成到现有应用程序中,以避免遭受新部署的惩罚,甚至更糟糕的是,他们可能会避免解决需要新的开发工作的问题。
像Heroku这样的推送到部署系统向开发人员展示了如果您能够控制您的应用程序和大多数依赖项,世界会是什么样子。与开发人员讨论部署问题时,经常会涉及到Heroku或类似系统的易用性。如果您是运维工程师,您可能听过对内部系统相对于像Heroku这样的“一键式”解决方案的速度较慢的抱怨,而这些解决方案是构建在Linux容器技术之上的。
Heroku是一个完整的环境,不仅仅是一个容器引擎。虽然Docker并不试图成为包含在Heroku中的所有内容,但它提供了责任的清晰分离和依赖项的封装,从而实现了类似的生产力提升。Docker甚至比Heroku更具细粒度的控制,因为它使开发人员对一切事物都有控制权,甚至包括与其应用程序一起运行的精确文件和软件包版本。在Docker之上构建的一些工具和编排器(如Kubernetes、Docker Swarm模式和Mesos)旨在复制类似Heroku的系统的简单性。但是,尽管这些平台在Docker的周围添加了更多功能来提供更强大和复杂的环境,但只使用Docker的简单平台仍然提供了所有核心的流程优势,而无需增加更大系统的复杂性。
作为一家公司,Docker采用“内置电池,可拆卸”的方法。这意味着其工具提供了大多数人完成工作所需的一切,同时又是由可互换的部件构建而成,可以轻松地更换以支持自定义解决方案。通过使用镜像仓库作为交接点,Docker允许将构建应用程序镜像的责任与容器的部署和运行分开。在实践中,开发团队可以将其应用程序及其所有依赖项构建成一个完整的镜像,运行在开发和测试环境中,然后只需将完全相同的应用程序和依赖项捆绑发送到生产环境。因为这些捆绑在外部看起来都是相同的,运维工程师可以构建或安装标准工具来部署和运行应用程序。接着,如图2-2所示的循环过程如下:
这是因为Docker允许在开发和测试周期中发现所有依赖问题。在应用程序准备进行首次部署时,这些工作已经完成。通常情况下,这不需要开发团队和运维团队之间进行太多的交接。在一个完善的流程中,这完全可以消除除开发团队之外的任何人参与新服务的创建和部署的需要。这样做更简单,节省了大量时间。更重要的是,在发布之前通过对部署环境进行测试,可以使软件更加健壮。
广泛的支持和采用
Docker受到了广泛的支持,大多数大型公有云都直接支持它。例如,Docker和Linux容器在亚马逊云服务(AWS)中通过多个产品使用,如亚马逊弹性容器服务(Amazon ECS)、亚马逊弹性Kubernetes服务(Amazon EKS)、亚马逊Fargate和亚马逊弹性Beanstalk。Linux容器还可以在Google App Engine(GAE)、Google Kubernetes Engine、Red Hat OpenShift、IBM Cloud、Microsoft Azure等平台上使用。在2014年的DockerCon大会上,谷歌的Eric Brewer宣布谷歌将支持Docker作为其主要的内部容器格式。这对Docker社区意味着大量的资金开始支持Docker平台的稳定性和成功,而不仅仅是这些公司的好公关。
随着进一步扩大其影响力,Docker的Linux容器镜像格式成为了云提供商之间通用的标准,为"一次编写,到处运行"的云应用程序提供了可能性。当Docker发布其libswarm开发库时,Orchard的一名工程师展示了将Linux容器同时部署到不同的云提供商的异构混合环境。在此之前,这种编排并不容易,因为每个云提供商都提供了不同的API或工具集来管理实例,而这些实例通常是可以通过API管理的最小单元。从2014年Docker所做的承诺已经完全成为主流,因为最大的公司继续投资于该平台、支持和工具。大多数提供商都提供了某种形式的Docker和Linux容器编排,以及容器运行时本身的支持,因此Docker在常见的生产环境中得到了很好的支持,适用于几乎任何类型的工作负载。如果您的所有工具都围绕着Docker和Linux容器构建,那么您的应用程序可以以与云提供商无关的方式部署,这为以前不可能的新灵活性提供了可能。
在2017年,Docker将其containerd运行时捐赠给了Cloud Native Computing Foundation(CNCF),并在2019年升级为毕业项目地位。
时至今日,Linux容器在开发、交付和生产中的使用比以往任何时候都更广泛。在2022年,我们看到Docker开始在服务器市场上失去份额,因为最新版本的Kubernetes不再需要Docker守护进程,但即使这些Kubernetes版本也非常依赖最初由Docker开发的containerd运行时。Docker在许多开发人员和CI/CD工作流程中仍然具有非常强大的存在。
那么,操作系统供应商的支持和采用情况如何呢?Docker客户端直接在大多数主要操作系统上运行,服务器可以在Linux或Windows Server上运行。绝大多数生态系统都是围绕Linux服务器构建的,但其他平台也越来越受到支持。大多数情况下,仍然围绕Linux服务器运行Linux容器。
为了支持在开发环境中对Docker工具的日益增长的需求,Docker推出了易于使用的macOS和Windows实现。这些实现看起来像是本地运行的,但实际上仍然使用一个小型的Linux虚拟机来提供Docker服务器和Linux内核。Docker在传统上是在Ubuntu Linux发行版上开发的,但现在大多数Linux发行版和其他主要操作系统都在可能的情况下得到支持。例如,Red Hat全面支持容器,其所有平台都对Docker提供了一流的支持。随着Linux领域中容器的普及,现在有像Red Hat的Fedora CoreOS这样专门为Linux容器工作负载构建的发行版。
在Docker发布后的头几年里,一些竞争对手和服务提供商对Docker的专有镜像格式表示担忧。Linux上的容器没有标准的镜像格式,因此Docker公司根据其业务需求自行创建了自己的镜像格式。服务提供商和商业供应商特别不愿意构建可能受到与他们自己利益重叠的公司决策影响的平台。作为公司,Docker在这一时期面临着一些公众挑战。为了赢得一些好感并在市场上获得更广泛的支持,Docker公司决定在2015年6月赞助Open Container Initiative(OCI)。该努力的第一个完整规范于2017年7月发布,其很大程度上基于Docker镜像格式的第二版。现在,可以为容器镜像和容器运行时申请OCI认证。
以下是主要的高级OCI认证运行时:
- containerd,在现代版本的Docker和Kubernetes中是默认的高级运行时。
以下是可以由containerd使用的低级OCI认证运行时,用于管理和创建容器:
- runc通常被用作containerd的默认低级运行时。
- crun是用C语言编写的,旨在快速且具有小的内存占用。
- 来自Intel、Hyper和OpenStack Foundation的Kata容器是一个虚拟化运行时,可以运行一组容器和虚拟机。
- 来自谷歌的gVisor是一个在用户空间完全实现的沙箱运行时。
- Nabla Containers提供了另一个沙箱运行时,旨在大大减少Linux容器的攻击面。
部署容器和编排整个容器系统的空间也在不断扩大。其中许多是开源的,并且可以在本地使用,也可以作为各种提供商的云或软件即服务(SaaS)提供。鉴于继续涌入Linux容器领域的大量投资,Docker很可能将继续在现代互联网中发挥重要作用。
架构
Docker是一项强大的技术,通常意味着工具和过程具有较高的复杂性。在底层,Docker确实是相当复杂的;然而,它的基本面向用户的结构确实是一个简单的客户端/服务器模型。Docker API后面有几个组件,包括containerd和runc,但基本的系统交互是一个客户端通过API与服务器通信。在这个简单的外表之下,Docker大量利用了内核机制,比如iptables、虚拟桥接、Linux控制组(cgroups)、Linux命名空间、Linux capabilities、安全计算模式、各种文件系统驱动等。我们将在第11章中讨论其中一些内容。现在,我们将介绍客户端和服务器的工作原理,并简要介绍Docker中Linux容器下面的网络层。
客户端/服务器模式
最简单的方式是将Docker看作由两部分组成:客户端和服务器/守护进程(见图2-3)。可选地,还有第三个组件称为注册表,用于存储Docker镜像及其元数据。服务器负责持续构建、运行和管理容器,而您使用客户端来告诉服务器应该做什么。Docker守护进程可以在基础设施中的任意数量的服务器上运行,而单个客户端可以连接任意数量的服务器。客户端驱动所有的通信,但是当客户端指示时,Docker服务器可以直接与镜像注册表进行通信。客户端负责告诉服务器该做什么,而服务器专注于托管和管理容器化应用程序。
Docker在结构上与其他一些客户端/服务器软件有些不同。它有一个Docker客户端和一个dockerd服务器,但与完全单块的结构不同,服务器在客户端的代表下幕后协调一些其他组件,包括containerd-shim-runc-v2,它用于与runc和containerd进行交互。然而,Docker将任何复杂性都干净地隐藏在简单的服务器API后面,因此在大多数情况下,您只需将其视为一个直接的客户端和服务器即可。每个Docker主机通常会运行一个Docker服务器,可以管理任意数量的容器。然后,您可以使用docker命令行工具与服务器通信,无论是从服务器本身还是(如果已经适当地进行了安全设置)从远程客户端。我们稍后会详细讨论这些内容。
网络端口和Unix套接字
docker命令行工具和dockerd守护进程可以通过Unix套接字和网络端口进行通信。Docker, Inc.已向Internet Assigned Numbers Authority (IANA)注册了三个端口供Docker守护进程和客户端使用:TCP端口2375用于未加密的流量,端口2376用于加密的SSL连接,端口2377用于Docker Swarm模式。您可以轻松配置不同的端口,以满足需要使用不同设置的场景。Docker安装程序的默认设置是仅使用Unix套接字与本地Docker守护进程进行通信。这确保系统默认使用最安全的安装方式。这也是可以轻松配置的,但强烈建议不要使用Docker的网络端口,因为Docker守护进程内部缺乏用户身份验证和基于角色的访问控制。Unix套接字可以在不同操作系统上的不同路径下找到,但在大多数情况下,它位于/var/run/docker.sock。如果您对不同的位置有特殊偏好,通常可以在安装时指定或稍后更改服务器配置并重新启动守护进程。如果没有特殊需求,默认设置可能适合您。与大多数软件一样,如果您不需要更改默认设置,则遵循默认设置将节省很多麻烦。
坚固的工具化
Docker之所以被广泛采用,其中之一就是其简单而强大的工具化。自其首次发布以来,得益于Docker社区的大力支持,其功能不断扩展。Docker附带的工具支持构建Docker镜像、基本部署到各个Docker守护程序,以及名为Swarm mode的分布式模式,以及管理远程Docker服务器所需的所有功能。除了包含的Swarm mode外,社区还努力管理整个Docker服务器群组(或集群)以及调度和编排容器部署。
Docker还推出了自己的编排工具集,包括Compose、Docker Desktop和Swarm mode,为开发人员提供了一个一体化的部署方案。然而,Docker在生产编排领域的产品大多被Google的Kubernetes所掩盖,尽管值得注意的是,直到2022年初发布的v1.24版本之前,Kubernetes还在很大程度上依赖于Docker。但Docker的编排工具仍然很有用,其中Compose对于本地开发特别方便。
由于Docker提供了命令行工具和远程REST API,因此可以很容易地使用任何编程语言添加更多的工具。命令行工具非常适合Shell脚本编程,而客户端可以通过REST API以编程方式实现任何命令行工具的功能。Docker CLI非常著名,许多其他Linux容器CLI工具(例如podman和nerdctl)都模仿其参数以实现兼容性和易于采用。
Docker命令行工具
命令行工具docker是大多数人与Docker交互的主要界面。Docker客户端是一个Go程序,可以编译并运行在所有常见的架构和操作系统上。命令行工具作为主要Docker发行版的一部分,可在各种平台上使用,也可以直接从Go源代码编译。使用Docker命令行工具,您通常可以执行以下操作,但不限于此:
- 构建容器镜像
- 从注册表拉取镜像到Docker守护进程,或将镜像从Docker守护进程推送到注册表
- 在Docker服务器上前台或后台启动容器
- 从远程服务器检索Docker日志
- 在远程服务器上交互式地在正在运行的容器内运行命令
- 监视有关容器的统计信息
- 获取容器中的进程列表
您可能可以看到这些操作如何组合成一个构建、部署和观察应用程序的工作流程。但是Docker命令行工具并不是与Docker交互的唯一方式,也不一定是最强大的方式。
Docker引擎API
和许多现代软件一样,Docker守护进程有一个API。实际上,这就是Docker命令行工具与守护进程通信所使用的方式。但由于API是文档化和公开的,外部工具通常直接使用API。这提供了一种方便的机制,允许任何工具创建、检查和管理Docker守护进程管理的所有镜像和容器。虽然初学者可能不太可能一开始就想直接与Docker API交互,但拥有这个工具是非常好的。随着您的组织逐渐采用Docker,您将越来越发现API是这种工具集成的良好接口。
Docker网站上有关于API的详尽文档。随着生态系统的成熟,出现了各种流行语言的稳健的Docker API库实现。Docker维护着Python和Go的SDK,还有其他由第三方维护的值得考虑的库。例如,多年来我们使用过这些Go和Ruby库,并发现它们在新版本的Docker发布时都得到了迅速更新,并且非常稳定。
使用Docker命令行工具几乎可以轻松支持大多数操作。但有两个显著的例外,即涉及流式传输或终端访问的终点:在远程主机上运行shell或以交互模式执行容器。在这些情况下,使用其中一个可靠的客户端库或命令行工具通常更加简单。
容器网络
尽管Linux容器在很大程度上由运行在主机系统上的进程组成,但它们在网络层通常与其他进程的行为有很大不同。Docker最初支持了单一的网络模型,但现在支持了一系列强大的配置,满足了大多数应用程序的需求。大多数人在默认配置下运行容器,称为桥接模式(bridge mode)。让我们来看看它是如何工作的。
要理解桥接模式,最简单的方法是将每个Linux容器视为在私有网络上行为像主机一样。Docker服务器充当虚拟桥接器,而容器则在其后面充当客户端。桥接器只是一个网络设备,它将来自一侧的流量转发到另一侧。因此,您可以将其视为一个小型虚拟网络,每个容器都像连接到该网络的主机一样运行。实际的实现方式是,每个容器都有一个连接到Docker桥接器的虚拟以太网接口,并分配给虚拟接口的IP地址。Docker允许您将主机上的单个端口或端口组绑定和暴露给容器,以便外部世界可以通过这些端口访问您的容器。流量主要由vpnkit库进行管理。
Docker从未使用的RFC 1918私有子网块中分配私有子网。它检测主机上未使用的网络块,并将其中之一分配给虚拟网络。这通过名为docker0的服务器接口桥接到主机的本地网络。这意味着,默认情况下,所有容器都位于同一个网络中,并且可以直接相互通信。但要访问主机或外部世界,它们会通过docker0虚拟桥接接口进行通信。
Docker的网络层有各种令人眼花缭乱的配置方式,可以从分配自己的网络块到配置自定义的桥接接口。人们通常使用默认机制运行,但在某些情况下,可能需要更复杂或特定于应用程序的配置。您可以在文档中找到有关Docker网络的更多详细信息,并且我们将在第11章中涵盖更多细节。
充分利用Docker的方法
就像大多数工具一样,Docker有许多很好的使用案例,也有一些不太适合的情况。例如,你可以用锤子打开一个玻璃罐,但这样做可能会有一些不利的影响。了解如何最好地使用这个工具,甚至确定它是否适合你的需求,可以让你更快地找到正确的路径。
首先,Docker的架构主要面向无状态的应用程序,或者状态被外部化到像数据库或缓存这样的数据存储中的应用程序。这类应用程序最容易容器化。Docker对于这类应用程序强制执行一些良好的开发原则,我们稍后将讨论它的强大之处。但这也意味着将数据库引擎放入Docker中有点像逆水行舟。并不是说你不能这么做,或者不应该这么做;只是这并不是Docker最明显的使用案例,所以如果你从这个方向开始,可能会很快感到失望。那些在Docker中运行良好的数据库通常是以复杂方式部署的。一些适合初学者的Docker使用案例包括Web前端、后端API以及短时间运行的维护脚本,这些脚本通常可以通过cron来处理。
如果你首先专注于在容器中运行无状态或外部化状态的应用程序,你将建立起一个基础,从中可以开始考虑其他使用案例。我们强烈建议先从无状态的应用程序入手,从这个经验中学习,然后再尝试其他用例。社区不断致力于更好地支持有状态的应用程序在Docker中的部署,这个领域可能会有许多发展。
容器不是虚拟机
开始塑造对Docker的理解的一个好方法是将Linux容器视为非常轻量级的封装,而不是虚拟机。在实际实现中,这个封装可能会产生其他进程,但另一方面,一个静态编译的二进制文件可能就是你容器中的全部内容(有关更多信息,请参阅“外部依赖”)。容器也是临时的:它们可能比传统的虚拟机更容易创建和销毁。
虚拟机的设计是为了替代你可能会放在机架上并在那里使用几年的真实硬件。因为它们是对真实服务器的抽象,虚拟机通常具有较长的生命周期。即使在云中,公司经常根据需求启动和关闭虚拟机,它们通常也会运行几天或更长时间。另一方面,特定的容器可能会存在几个月,或者可能被创建出来,执行一项任务一分钟,然后被销毁。所有这些都是可以接受的,但这与虚拟机通常用于的方式根本不同。
为了帮助理解这种差异,如果你在mac或Windows系统上运行Docker,你将利用Linux虚拟机来运行Docker服务器(dockerd)。然而,在Linux上,dockerd可以在本地运行,因此在系统上不需要运行虚拟机(参见图2-5)。
有限的隔离性
容器之间是相互隔离的,但这种隔离可能比您预期的要有限。虽然您可以限制它们的资源,但默认的容器配置是共享主机系统上的CPU和内存,就像共存的Unix进程一样。这意味着除非对它们进行限制,否则容器可能会在生产机器上竞争资源。对于您的使用情况可能没有问题,但它会影响您的设计决策。虽然Docker鼓励对CPU和内存使用进行限制,但在大多数情况下,它们不像虚拟机那样是默认的设置。
通常,许多容器共享一个或多个常见的文件系统层。这是Docker中更强大的设计决策之一,但这也意味着如果您更新共享的映像,则可能需要重新构建和部署仍在使用旧映像的容器。
容器化进程只是在Docker服务器本身上的进程。它们在与主机操作系统相同的Linux内核实例上运行。所有容器进程都显示在Docker服务器上的正常ps输出中。这与虚拟化管理程序完全不同,后者的进程隔离深度通常包括为每个虚拟机运行完全独立的操作系统内核实例。
这种轻量级的隔离性可能会引发一种诱人的选择,即从主机中暴露更多资源,例如共享文件系统,以允许存储状态。但是,在将主机资源进一步暴露到容器之前,您应该认真考虑,除非这些资源完全由容器独占。我们稍后将讨论容器的安全性,但通常情况下,您可能应该考虑通过应用安全增强型Linux(SELinux)或AppArmor策略来进一步强制隔离,而不是削弱现有的屏障。
容器是轻量级的
我们稍后会更详细地介绍这是如何实现的,但创建一个新的容器所需的磁盘空间非常小。一个快速的测试显示,从现有映像创建一个新容器只需占用12KB的磁盘空间。这相当轻量级。另一方面,从黄金映像创建一个新的虚拟机可能需要数百或数千兆字节,因为它至少需要一个完整的操作系统安装来存在于该磁盘上。而新容器之所以如此小,是因为它只是对分层文件系统映像的引用以及关于配置的一些元数据。默认情况下,并不为容器分配数据的副本。容器只是现有系统上的进程,可能只需要从磁盘读取信息,因此在容器的独占使用数据之前,可能不需要复制任何数据。
容器的轻量性意味着您可以在创建另一个虚拟机过于繁重的情况下使用它们,或者在需要真正短暂的情况下使用它们。例如,您可能不会为了从远程位置运行curl命令访问网站而启动整个虚拟机,但您可能会为此目的创建一个新的容器。
走向不可变基础设施(Immutable Infrastructure)
通过将大部分应用程序部署在容器中,您可以开始朝着不可变基础设施迈进,从而简化配置管理过程,使组件完全被替换而不是在原地进行修改。不可变基础设施的理念在现实中越来越受欢迎,因为真正维护幂等配置管理代码库是多么困难。随着配置管理代码库的增长,它可能变得像大型、笨重、难以维护的传统遗留应用程序一样难以处理。
通过Docker,您可以部署一个非常轻量级的Docker服务器,几乎不需要配置管理,或者在许多情况下根本不需要。您只需通过部署和重新部署容器来管理所有应用程序。当服务器需要对Docker守护程序或Linux内核等重要组件进行更新时,您只需启动一个新的服务器并进行更改,然后将容器部署到新服务器上,最后将旧服务器废弃或重新安装。
基于容器的Linux发行版(如Red Hat的Fedora CoreOS)就是围绕这一原则设计的。不过与其要求您废弃实例不同,Fedora CoreOS可以完全更新自身并切换到更新后的操作系统。您的配置和工作负载主要保留在容器中,因此几乎不需要对操作系统进行太多配置。
由于在服务器的部署和配置之间存在清晰的分离,许多基于容器的生产系统使用诸如HashiCorp的Packer等工具来构建云虚拟服务器镜像,然后利用Docker来几乎或完全避免配置管理系统。
无状态应用程序
一个很好的容器化的应用程序例子是将其状态保存在数据库中的Web应用程序。无状态应用程序通常设计为立即响应单个独立的请求,并且不需要在一个或多个客户端的请求之间跟踪信息。你也可以在容器中运行临时的Memcached实例。但是,如果考虑你的Web应用程序,它可能有一些本地状态,比如配置文件。这可能看起来不像很多状态,但如果你将该配置嵌入到你的镜像中,这意味着你限制了镜像的可重用性,并使其在不同环境中部署变得更具挑战性,因为需要为不同的部署目标维护多个镜像。
在许多情况下,将应用程序容器化意味着将配置状态移入环境变量,并在运行时将配置应用于容器。这使得您可以轻松地在生产环境或测试环境中使用相同的容器。在大多数公司中,这些环境需要许多不同的配置设置,例如应用程序使用的各种外部服务的连接URL。
使用容器,您可能会发现在优化时,您会不断减小容器化应用程序的大小,使其最小化到运行所需的最基本要素。我们发现,将任何需要分布式运行的东西视为容器可以导致一些有趣的设计决策。例如,如果你有一个收集数据,处理数据并返回结果的服务,你可以在许多服务器上配置容器来运行作业,然后在另一个容器上聚合响应。
状态外部化
如果Docker最适合无状态应用程序,那么在需要存储状态时应该如何处理呢?通常,配置是通过环境变量传递的。Docker原生支持环境变量,并将它们存储在构成容器配置的元数据中。这意味着每次重新启动容器时,都会确保将相同的配置传递给您的应用程序。同时,这也使得在容器运行时轻松地观察容器的配置,这可以使调试变得更加容易,尽管在使用环境变量时可能存在一些安全问题。此外,您还可以将应用程序配置存储在外部数据存储中,例如Consul或PostgreSQL。
数据库通常是规模化应用程序存储状态的地方,Docker不会干扰对容器化应用程序进行这种操作。然而,需要存储文件的应用程序面临一些挑战。将数据存储到容器的文件系统中性能不佳,受到空间限制,并且在重新创建容器时不会保留状态。如果您重新部署一个有状态服务而不使用容器外部存储,将会丢失所有状态。在将这些应用程序放入Docker之前,应仔细考虑需要存储文件系统状态的情况。如果您决定在这些情况下受益于Linux容器,最好设计一个解决方案,其中状态可以存储在集中位置,无论容器在哪个主机上运行,都可以访问。在某些情况下,这可能意味着使用Amazon Simple Storage Service(Amazon S3)、OpenStack Swift或本地块存储,甚至可以在容器内挂载EBS卷或iSCSI磁盘。Docker卷插件提供了一些额外的选项,并在第11章中简要讨论了这些内容。
Docker工作流
和许多工具一样,Docker强烈鼓励采用特定的工作流程。这是一种非常有益的工作流程,与许多公司的组织结构很好地契合,但它可能与您或您的团队目前采用的方式有所不同。通过将我们自己组织的工作流程调整到Docker的方法中,我们可以有信心地说,这种改变可以对您组织中的许多团队产生广泛的积极影响。如果工作流程得到良好实施,它可以帮助您实现减少团队之间沟通开销的承诺。
版本控制(Revision Control)
Docker提供的第一个开箱即用功能是两种形式的版本控制。其中一种用于跟踪每个Docker镜像所组成的文件系统层次结构,另一种是用于对这些镜像进行标记的系统。
文件系统层
Linux容器由一系列堆叠的文件系统层组成,每个层都由唯一的哈希标识,构建过程中的每组新更改都会叠加在之前的更改之上。这非常棒,因为这意味着当您进行新的构建时,只需重新构建跟随您部署的更改之后的层次。这样可以节省时间和带宽,因为容器是以层次结构形式进行传输,并且您不必传输服务器已经存储的层次。如果您使用过许多传统的部署工具进行部署,您会知道您可能会一遍又一遍地向服务器传输数百兆字节的相同数据。这非常低效,更糟糕的是,您无法确定在部署之间发生了什么变化。由于层次结构效应,而且Linux容器包含所有应用程序依赖项,使用Docker时,您可以更加确信正在向生产环境部署的更改内容。
为了简化这个概念,记住Docker镜像包含了运行应用程序所需的所有内容。如果您更改了一行代码,您肯定不想将每个代码所需的依赖重新构建成一个新的镜像。相反,通过利用构建缓存,Docker可以确保仅重新构建受代码更改影响的层次。
镜像层
Docker提供的第二种版本控制方式使回答一个重要问题变得很容易:上一个部署的应用程序版本是什么?这通常不是容易回答的问题。对于非容器化的应用程序,有很多解决方案,比如为每个发布设置Git标签、部署日志、带有标签的构建用于部署等等。例如,如果您正在使用Capistrano进行部署协调,它会为您在服务器上保存一组以前的发布版本,并使用符号链接将其中一个设置为当前发布版本。
但是在任何规模的生产环境中,每个应用程序都有一种独特的处理部署版本的方式。其中很多方法可能是相同的,但有些可能是不同的。更糟糕的是,在异构的语言环境中,部署工具通常在应用程序之间完全不同,并且很少有共享的部分。因此,问题“上一个版本是什么?”可能会有很多答案,取决于您向谁问以及指的是哪个应用程序。Docker具有内置的机制来处理这个问题:使用图像标记进行标准构建步骤。您可以轻松地在服务器上保留多个应用程序版本,以便执行回滚变得非常简单。这并不是高深莫测的技术,也不是在其他部署工具中难以找到的功能,但是对于容器镜像来说,可以很容易地在所有应用程序中建立标准,每个人都可以对所有应用程序的标记有相同的期望。这使得团队之间的沟通更加容易,同时也让工具更加简单,因为应用程序发布有一个统一的数据源。
构建
在许多组织中,构建应用程序是一门黑科技,只有少数人知道如何操纵所有杠杆和旋钮,才能得到一个形式良好、可交付的构建产物。部署新应用程序的重要成本之一就是确保构建正确。Docker并不能解决所有这些问题,但它提供了一套标准化的工具配置和工具集,用于构建应用程序。这使得其他人更容易学习如何构建您的应用程序,并且能够快速启动新的构建。
Docker命令行工具包含一个构建(build)标志,它将使用Dockerfile生成一个Docker镜像。Dockerfile中的每个命令都会在镜像中生成一个新的层,因此通过查看Dockerfile本身,很容易理解构建会做什么。所有这些标准化的好处在于,任何曾使用过Dockerfile的工程师都可以直接加入并修改任何其他应用程序的构建。由于Docker镜像是一个标准化的构建产物,不论使用的开发语言、基础镜像或所需层数,所有构建背后的工具都是相同的。Dockerfile通常会被检入版本控制系统,这也意味着跟踪构建的更改变得更加简单。现代的多阶段Docker构建还允许您将构建环境与最终产物镜像分开定义,这为您的构建环境提供了巨大的“可配置性”,就像为生产容器配置一样。
许多Docker构建是通过单次调用docker image build命令来生成单个产物——容器镜像。由于构建的大部分逻辑通常完全包含在Dockerfile中,所以很容易为任何团队创建标准的构建任务,并在诸如Jenkins等构建系统中使用。作为进一步的构建标准化,许多公司(例如eBay)已经将Linux容器标准化为从Dockerfile进行镜像构建。SaaS构建服务如Travis CI和CodeShip也对Docker构建提供了一流的支持。
利用Docker中的新BuildKit支持,还可以自动创建支持不同底层计算架构(如x86和ARM)的多个镜像。
测试
虽然Docker本身并不包含内置的测试框架,但容器构建的方式为使用Linux容器进行测试提供了一些优势。 测试生产应用程序可以采用多种形式,从单元测试到在半实时环境中进行完整的集成测试。Docker通过确保经过测试的构建产物将被部署到生产环境中,从而有助于实现更好的测试。这可以通过使用Docker SHA或自定义标签来保证我们始终部署相同版本的应用程序。
由于设计上容器包含了所有依赖项,因此在容器上运行的测试非常可靠。如果一个单元测试框架表示针对容器镜像的测试成功,您可以确信在部署时不会出现底层库版本问题。这在大多数其他技术中并不容易实现,即使是Java WAR(Java Web Application Archive)文件,也不包括对应用程序服务器本身的测试。而将相同的Java应用程序部署在Linux容器中通常也会包含应用程序服务器(如Tomcat),整个堆栈可以在部署到生产环境之前进行烟雾测试。
通过在Linux容器中部署应用程序的第二个好处是,在使用诸如API之类的远程方式相互通信的多个应用程序的场景中,一个应用程序的开发人员可以轻松地针对其所需环境(如生产或暂存环境)中当前标记的其他服务版本进行开发。每个团队的开发人员不需要成为其他服务工作或部署方式的专家,只需专注于自己的应用程序开发。如果将这扩展到具有无数微服务的服务导向架构,对于需要处理各种微服务之间的API调用的开发人员或质量保证工程师,Linux容器可以成为真正的救命稻草。
在生产环境中运行Linux容器的组织中,一个常见的做法是自动化集成测试会拉取一组经过版本化的不同服务的Linux容器,与当前部署的版本相匹配。然后,新服务可以针对将要与之一起部署的相同版本进行集成测试。在异构语言环境中执行这些操作以前需要大量的定制工具,但由于Linux容器提供了标准化,因此实现起来相对简单。
打包
Docker构建生成的镜像可以被视为单个构建产物,尽管在技术上它们可能由多个文件系统层组成。无论您的应用程序是用哪种语言编写的,或者在哪种Linux发行版上运行,您都会得到一个分层的Docker镜像作为构建的结果。而且这一切都是由Docker工具处理的。这个构建镜像就是Docker命名的“运输容器”隐喻:一个单一的、可传输的单位,通用的工具可以处理,无论它包含什么内容。就像把所有东西都打包到钢铁集装箱中的大洋货船一样,您的Docker工具只需要处理一种类型的包裹:Docker镜像。这是强大的,因为它是应用程序之间工具重用的巨大推动者,并且意味着其他人的现成容器工具将与您的构建镜像一起工作。
传统上在部署到新主机或开发系统时需要大量定制配置的应用程序,通过Docker变得非常便携。一旦容器构建完成,它可以轻松地部署在任何具有相同架构的运行Docker服务器的系统上。
发布
部署在不同场所由各种工具处理,这里不可能列出所有工具。其中一些工具包括Shell脚本、Capistrano、Fabric、Ansible和自定义工具。在我们与多团队组织的经验中,每个团队通常有一两个人知道让部署正常工作的神秘命令。当出现问题时,团队依赖他们使其再次运行。如您可能已经预料到的那样,Docker使大部分问题都变得不再重要。内置的工具支持简单的一行部署策略,将构建部署到主机并使其运行起来。标准Docker客户端只能在一个主机上部署,但有大量可用的工具可以轻松部署到Docker集群或其他兼容的Linux容器主机群。由于Docker提供的标准化,您的构建可以部署到这些系统中的任何一个,对开发团队来说复杂性很低。
Docker生态系统
多年来,围绕Docker已经形成了一个庞大的社区,其中既有开发人员又有系统管理员。就像DevOps运动一样,这个社区通过将代码应用于运维问题,促进了更好的工具的发展。在Docker提供的工具中存在空缺的地方,其他公司和个人也参与进来。其中许多工具也是开源的,这意味着它们可以被扩展和修改以适应其他公司的需求。
编排(Orchestration)
第一个重要的工具类别是为核心Docker发行版和Linux容器体验增加功能的编排和大规模部署工具。早期的大规模部署工具,如New Relic的Centurion、Spotify的Helios和Ansible的Docker工具套件,仍然基本上像传统的部署工具一样工作,但是利用容器作为分发工件。它们采用了相对简单、易于实现的方法。你可以获得Docker的许多优势,而不会增加太多复杂性,但是其中许多工具已经被更强大、更灵活的工具取代,例如Kubernetes。
完全自动化的调度器,如Kubernetes或Apache Mesos与Marathon调度器,是更强大的选项,它们几乎完全代表您控制了一个主机池。其他商业级别的解决方案也广泛可用,例如HashiCorp的Nomad、Mesosphere的DC/OS(数据中心操作系统)和Rancher。自由和商业选项的生态系统都在迅速增长。
不变的原子主机
不变的原子主机是另一个可以提升您 Docker 体验的想法。传统上,服务器和虚拟机是组织精心组装、配置和维护的系统,以提供广泛的功能支持各种使用模式。更新通常需要通过非原子操作应用,主机配置可能会发散,导致系统出现意外行为。在今天的世界里,大多数运行中的系统是通过就地修补和更新的方式进行维护。相比之下,在软件部署领域,大多数人会部署整个应用程序的副本,而不是尝试对运行中的系统应用补丁。容器的吸引之处在于它们有助于使应用程序比传统部署模型更加原子化。
如果您可以将这种核心容器模式扩展到操作系统呢?而不是依赖于配置管理来尝试更新、修补和合并 OS 组件的更改,您可以简单地获取一个新的、轻量级的 OS 镜像并重新启动服务器。然后,如果发生故障,可以轻松回滚到先前使用的精确镜像?
这是 Linux 基于原子主机发行版(例如 Red Hat 的 Fedora CoreOS、Bottlerocket OS 等)背后的核心思想之一。您不仅应该能够轻松地拆除和重新部署应用程序,而且同样的理念应该适用于整个软件堆栈。这种模式有助于为整个堆栈提供非常高的一致性和弹性。
不变的原子主机通常具有以下特点:最小的占用空间、专注于支持 Linux 容器和 Docker 的设计,以及可以通过多主机编排工具轻松控制的原子 OS 更新和回滚,适用于裸机和常见虚拟化平台。
在第三章中,我们将讨论如何在开发过程中轻松使用这些不变的主机。如果您还将这些主机用作部署目标,这个过程将为您的开发和生产环境之间创造前所未有的软件堆栈对称性。
其他工具
Docker不仅仅是一个独立的解决方案。它拥有丰富的功能集,但总有一些情况需要比它单独提供的更多功能。在广泛的生态系统中,有许多工具可以改进或增强Docker的功能。一些优秀的生产工具利用Docker API,如用于监控的Prometheus和用于简单编排的Ansible。还有一些利用Docker的插件架构。插件是符合规范的可执行程序,用于与Docker交换数据。
还有许多其他优秀的工具,它们要么与API进行通信,要么作为插件运行。其中许多工具的出现是为了在不同的云服务提供商上更轻松地使用Docker,它们有助于实现Docker与云的无缝集成。随着社区不断创新,生态系统不断壮大。在这个领域,每天都有新的解决方案和工具涌现。如果您在环境中遇到问题,可以去寻找生态系统中的解决方案!
总结
就是这样,我们快速地浏览了一下Docker。稍后我们将回到这个讨论,稍微深入地了解Docker的架构,更多地介绍如何使用社区工具,并探索一些设计稳健容器平台的思路。但是您可能迫不及待地想要尝试一切,所以下一章中我们将安装和运行Docker。