共计 8239 个字符,预计需要花费 21 分钟才能阅读完成。
导读 | Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。本文立足于新手,从容器和虚拟机两个大的概念入手,由浅入深,由宏转微,为我们解析了 Docker 的方方面面。来吧朋友们,理解它,热爱它,然后更好地使用它。 |
作为程序员或者技术人员,大家肯定听说过 Docker 的鼎鼎大名——这款工具能够帮助我们高效打包、发布及运行承载着应用程序的“容器”系统。其发展如火如荼——从开发者到运维人员,每个人都在关注着这位技术新贵。即使是像谷歌、VMware 与 Amazon 这样的技术巨擘也在构建相关服务为其提供支持。
无论大家是否有意立即使用 Docker,我们都应当对其基础概念加以了解,并明确区分容器与虚拟机之间的差异所在。目前网络上已经存在大量对二者关联与区别的阐述性文章,不过在今天的综述中,我们将立足于新手对其加以剖析。
下面首先聊聊虚拟机与容器究竟是什么?
容器与虚拟机拥有着类似的使命:对应用程序及其关联性进行隔离,从而构建起一套能够随处运行的自容纳单元。
此外,容器与虚拟机还摆脱了对物理硬件的需求,允许我们更为高效地使用计算资源,从而提升能源效率与成本效益。
容器与虚拟机之间的核心差异在于其架构方法。下面一起进行深入了解。
虚拟机在本质上就是在模拟一台真实的计算机设备,同时遵循同样的程序执行方式。虚拟机能够利用“虚拟机管理程序”运行在物理设备之上。反过来,虚拟机管理程序则可运行在主机设备或者“裸机”之上。
下面用更直白的表达来说明:
虚拟机管理程序可表现为软件、固件或者硬件,并作为虚拟机的运行基础。虚拟机管理程序本身运行在物理计算机之上,我们也将这种底层硬件称为“主机设备”。主机设备为虚拟机提供资源,包括内存与 CPU。这些资源由不同虚拟机共享,并根据需要进行随意分配。因此如果一套虚拟机运行有需求大量资源的高强度应用程序,那么我们可以在同一主机设备上为其分配远高于其它虚拟机的资源配额。
运行在主机设备上的虚拟机(当然,需要配合虚拟机管理程序)通常被称为一套“客户机”。这套客户机容纳有应用程序及其运行所必需的各类组件(例如系统二进制文件及库)。它同时还包含有完整的虚拟硬件堆栈,其中包括虚拟网络适配器、存储以及 CPU——这意味着它也拥有自己的完整访客操作系统。着眼于内部,这套客户机自成体系并拥有专用资源。而从外部来看,这套虚拟机使用的则是由主机设备提供的共享资源。
如上所述,客户机可以运行主机虚拟机管理程序或者裸机虚拟机管理程序。二者之间存在着多种重要区别。
首先,主机虚拟机管理程序运行在主机设备的操作系统之上。举例来说,一台运行有 OS X 的计算机可以在操作系统之上安装虚拟机(例如 VirtualBox 或者 VMware Workstation 8)。该虚拟机并不会直接访问硬件,因此其需要经由主机操作系统(也就是 Mac OS X)实现资源获取。
主机虚拟机管理程序的优势在于,其基本摆脱了对底层硬件的要求。该主机操作系统负责提供硬件驱动程序,而非由虚拟机管理程序自身提供,因此我们认为其具备更理想的“硬件兼容性”。在另一方面,这种介于硬件与虚拟机管理程序之间的额外层会带来更多资源消耗,进而降低虚拟机性能表现。
裸机虚拟机管理程序则将虚拟机直接安装并运行在主机设备硬件之上以改善性能表现。由于其直接接入底层硬件,因此我们不再需要主机操作系统作为辅助。在这种情况下,我们可以直接在硬件上安装虚拟机管理程序并将其作为操作系统。与主机虚拟机管理程序不同,裸机虚拟机管理程序拥有自己的设备驱动程序及接口,从而直接支持各类 I /O、处理或者操作系统特定任务相关组件。这种方式能够带来更理想的性能水平、可扩展性以及稳定性。但代价是其硬件兼容性比较有限,因为虚拟机管理程序只能包含一部分设备驱动程序。
说到这里,大家可能提出疑问:为什么我们非得在虚拟机与主机设备之间添加“虚拟机管理程序”呢?
这个嘛,因为虚拟机本身拥有一套虚拟操作系统,而虚拟机管理程序则负责为虚拟机提供平台以管理并运行这套访客操作系统。如此一来,主机计算机就能够为运行于其上的各虚拟机分配共享资源了。
虚拟机原理示意图
如大家所见,虚拟机会将虚拟硬件、内核(即操作系统)以及用户空间打包在新虚拟机当中。
与提供硬件虚拟化机制的虚拟机不同,容器通过对“用户空间”的抽象化处理提供操作系统层级的虚拟化机制。通过对容器进行分解,大家将可以非常清晰地理解其中含义。
出于各种考量与需求,容器在外观上与虚拟机非常相似。举例来说,二者皆拥有专有处理空间、能够作为 root 执行命令、提供专有网络接口与 IP 地址、允许定制化路由及 iptable 规则,且可启动文件系统等等。
容器与虚拟机间的最大区别在于,各容器系统 共享 主机系统的内核。
容器原理示意图
以上示意图显示了容器如何对用户空间进行打包,而不像虚拟机那样同样对内核或者虚拟硬件进行打包。每套容器都拥有自己的隔离化用户空间,从而使得多套容器能够运行在同一主机系统之上。我们可以看到全部操作系统层级的架构都可实现跨容器共享。惟一需要独立构建的就是二进制文件与库。正因为如此,容器才拥有极为出色的轻量化特性。
Docker 为基于 Linux 容器的开源项目,其利用 Linux 内核中的各项功能——例如命名空间与控制组——以在操作系统之上创建容器。
容器概念并不是什么新鲜事物; 谷歌公司多年来一直在使用自己开发的容器技术。其它 Linux 容器技术方案还包括 Solaris Zones、BSD jails 以及 LXC,且其都已经拥有多年的发展历史。
那么为什么 Docker 的出现会快速吸引到技术业界的注意?
- 易用性: Docker 能够为潜在受众带来出色的易用性——开发者、系统管理员以及架构师等等——从而帮助其充分利用容器技术优势以快速构建并测试可移植应用程序。每个人都可以在自己的笔记本上打包应用程序,并将其直接运行在任何公有云、私有云甚至是裸机之上。其座右铭是:一次构建,随处运行。
- 速度: Docker 容器具备轻量化与高速特性。由于容器本身属于运行在内核之上的沙箱环境,因为其对资源的需求量极低。大家可以在数秒钟内完成容器的创建与运行,而虚拟机则由于需要引导完整的虚拟操作系统而耗费更多时间。
- Docker Hub: Docker 用户还能够享受由 Docker Hub 带来的丰富生态系统支持,我们可以将其理解成“Docker 镜像的应用商店”。Docker Hub 提供成千上万由社区开发的公共镜像,且可立即加以使用。我们可以轻松根据需要搜索到合适的镜像,将其提取并稍加修改即加以使用。
- 模块性与可扩展性: Docker 允许我们轻松将应用程序的功能拆分成多个独立容器。举例来说,我们可以将自己的 Postgres 数据库运行在一套容器当中,并将 Redis 服务器运行在另一容器内,而 Node.js 也拥有自己的容器系统。在 Docker 的帮助上,大家能够轻松将这些容器对接起来以创建完整的应用程序,这就让未来的规模伸缩或者组件更新得以通过相互独立的方式完成。
最后但同样重要的是,Docker 鲸鱼实在是太惹人喜爱了~
现在我们对 Docker 有了宏观印象,下面具体对其组件进行解读:
Docker Engine 属于 Docker 的运行层。这是一套轻量化运行时及工具组合,负责管理容器、镜像、构建 等等。它以原生方式运行在 Linux 系统之上,并由以下元素构成:
Docker Daemon,运行在主机计算机之上。
Docker Client,负责与 Docker Daemon 通信以执行命令。
REST API,用于同 Docker Daemon 远程交互。
Docker Client 正是我们作为最终用户的通信对象。我们可以将其视为 Docker 的 UI。
我们进行的一切操作都将直接接入 Docker Client,再由其将指令传递至 Docker Daemon。
Docker Daemon 直接将执行命令发送至 Docker Client——例如构建、运行以及分发等等。Docker Daemon 运行在主机设备之上,但作为用户,我们永远不会直接与该 Daemon 进行通信。Docker Client 也可以运行在主机设备上,但并非必需。它亦能够运行在另一台设备上,并与运行在目标主机上的 Docker Daemon 进行远程通信。
Dockerfile 是我们编写指令以构建 Docker 镜像的载体。这些指令包括:
RUN apt-get y install some-package: 安装某软件包
EXPOSE 8000: 开放端口
ENV ANT_HOME /usr/local/apache-ant:传递环境变量
类似的指令还有很多。一旦设置了 Dockerfile,大家就可以利用 docker build 命令来构建镜像了。下面来看 Dockerfile 示例:
# Start with ubuntu 14.04
FROM ubuntu:14.04MAINTAINER preethi kasireddy iam.preethi.k@gmail.com# For SSH access and port redirection
ENV ROOTPASSWORD sample# Turn off prompts during installations
ENV DEBIAN_FRONTEND noninteractive
RUN echo "debconf shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections
RUN echo "debconf shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections# Update packages
RUN apt-get -y update# Install system tools / libraries
RUN apt-get -y install python3-software-properties \
software-properties-common \
bzip2 \
ssh \
net-tools \
vim \
curl \
expect \
git \
nano \
wget \
build-essential \
dialog \
make \
build-essential \
checkinstall \
bridge-utils \
virt-viewer \
python-pip \
python-setuptools \
python-dev
# Install Node, npm
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
RUN apt-get install -y nodejs
# Add oracle-jdk7 to repositories
RUN add-apt-repository ppa:webupd8team/java
# Make sure the package repository is up to date
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
# Update apt
RUN apt-get -y update
# Install oracle-jdk7
RUN apt-get -y install oracle-java7-installer
# Export JAVA_HOME variable
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle
# Run sshd
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo "root:$ROOTPASSWORD" | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
# Expose Node.js app port
EXPOSE 8000
# Create tap-to-android app directory
RUN mkdir -p /usr/src/my-app
WORKDIR /usr/src/my-app
# Install app dependencies
COPY . /usr/src/my-app
RUN npm install
# Add entrypoint
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["npm", "start"]
示例 Dockerfile
镜像属于只读模板,大家可以借此配合 Dockerfile 中的编写指令集进行容器构建。镜像定义了打包的应用程序以及其相关依赖。这些依赖就好像是其启动时需要运行的进程。
Docker 镜像利用 Dockerfile 实现构建。Dockerfile 中的每条指令都会在镜像中添加一个新的“层”,这些层则表现为镜像文件系统中的一个分区——我们可以对其进行添加或者替换。层概念正是 Docker 轻量化的基础。Docker 利用一套 Union File System 建立起这套强大的结构:
Docker 利用 Union File System 以构建镜像。大家可以将 Union File System 视为一套可堆叠文件系统,这意味着各文件系统中的文件与目录(在 Docker 中被称为分支)可以透明方式覆盖并构成单一文件系统。
覆盖分支内的各目录内容拥有同样的路径,并将被视为单一合并目录,这就避免了需要为每个层分别创建副本的麻烦。相反,这些目录可调用特定指针以指向同样的资源; 当特定层需要进行修改时,其会分别创建修改前与修改后的本地副本。通过这种方式,文件系统 表现 出可写入特性,但其实际上并未接受任何写入操作。(换言之,这是一套‘写入即复制’系统。)
分层系统拥有两大核心优势:
无重复数据: 分层机制使得我们在使用镜像创建并运行新容器时无需复制完整的文件集,从而保证 Docker 容器实例拥有良好的运行速度与低廉的资源成本。
层隔离: 变更操作速度很快——当我们对镜像进行变更时,Docker 只需要对进行了变更的层进行广播。
Volumes 属于容器中的“数据”部分,会在容器创建时进行初始化。Volumes 机制允许我们持久保留并共享容器数据。数据卷 独立于默认 Union File System 之外,且作为普通目录及文件存在于主机文件系统当中。因此即使是在对容器进行销毁、更新或者重构时,数据卷仍然不受影响。当我们需要对某一数据卷进行更新时,直接加以变更即可。(作为另一项优势,数据卷还能够在不同容器之间进行共享与复用。)
如上所述,Docker 容器将一款应用程序的软件打包在单一环境当中,同时包含全部运行必需的要素。其中包括操作系统、应用程序代码、运行时、系统工具、系统库等等。Docker 容器由 Docker 镜像构建而成。由于镜像存在只读属性,因此 Docker 会在镜像在只读文件系统之上添加一套读取 - 写入文件系统以实现容器创建。
另外,在创建容器时,Docker 还会创建一套网络接口以帮助容器同本地主机通信、对接可用 IP 地址并执行用户在定义镜像时所执行的进程以运行应用程序。
在成功创建了一套容器之后,我们随后可以将其运行在任何环境当中,而不必再做任何变更。
打开容器,我们会发现其中包含大量活动组件。容器的实现机制一直令我感到惊讶而好奇,特别是考虑到容器不存在任何抽象的基础设施边界。在阅读了大量材料之后,我将结合自己的理解为大家进行讲解:)
“容器”这一术语其实只是个抽象概念,用于表述多种不同的相关特性,而其与原词“container”的另一含义“集装箱”有着千丝万缕的联系。下面就来一起了解:
命名空间为容器提供对应的底层 Linux 系统视图,即限制容器的查看与访问范畴。当我们运行一套容器时,Docker 会创建多个命名空间供特定容器使用。
Docker 会在内核中使用多种不同类型的命名空间,例如:
- NET: 为容器提供独特的系统网络堆栈视图(例如自有网络设备、IP 地址、IP 路由表、/proc/net 目录、端口编号等等)。
- PID: PID 代表进程 ID。如果大家曾经在命令行中运行过 ps aux 以检查当前系统正在运行的进程,就会发现其中一栏名为“PID”。PID 命名空间为容器提供其能够查看与交互的进程范围,其中包括独立的 init(PID 1),其属于“所有进程的元祖”。
- MNT: 为容器提供独特的系统“mounts”视图。这样不同 mount 命名空间内的进程就将拥有彼此不同的文件系统结构。
- UTS: UTS 代表 UNIX 分时系统。它允许某一进程识别系统身份(例如主机名称或者域名等)。UTS 允许容器拥有不同于其它容器以及主机系统的主机名称与 NIS 域名。
- IPC: IPC 代表进程间通信。IPC 命名空间负责对运行在每套容器内的进程进行 IPC 资源隔离。
- USER: 此命名空间用于对每套容器内的用户进行隔离。其允许各容器拥有不同的 uid(即用户 ID)与 gid(组 ID)视图区间,并将其与主机系统进行比对。这样一来,某一进程的 uid 与 gid 在用户命名空间之内与之外即有所不同,这也使得该进程能够在不影响容器内 root 权限的情况下,撤销同一用户在容器外的权限。
Docker 将这些命名空间结合起来以隔离并创建容器。下面要讲的则是控制组。
控制组(也被称为 cgroups)属于 Linux 内核中的一项功能,用于对一组进程的资源使用量(包括 CPU、内存、磁盘 I / O 以及网络等)进行隔离、排序与计数。这意味着 cgroup 能够确保 Docker 容器只使用其必需的资源——并在必要情况下设置其所能使用的资源上限。另外,cgroups 还能够确保单一容器不至于占用太多资源并导致整体系统陷入瘫痪。
最后要说明的是 Union 文件系统:
我们在之前的 Docker 镜像章节中已经解释过了:)
这就是关于 Docker 容器的全部内容了(当然,实现细节才是最麻烦的环节——例如如何管理不同组件间的交互)。
尽管 Docker 已经开始逐步成为主流,但我认为它不太可能对虚拟机造成真正的威胁。容器将继续发展壮大,但虚拟机也仍然拥有适合自己的生存空间。
举例来说,如果我们需要在多台服务器上运行多款应用程序,那么最理想的办法就是使用虚拟机。在另一方面,如果我们需要运行同一应用程序的多套副本,那么 Docker 则拥有更多具体优势。
另外,尽管容器允许我们将应用程序拆分成多个功能组件,但这种分散化趋势意味着我们需要管理更多功能部件,即带来更为复杂的控制与协调任务。
安全同时也是 Docker 容器需要解决的一大难题——由于各容器共享同一套内核,因此不同容器之间的屏障非常薄弱。与只需要调用主机虚拟机管理程序的虚拟机方案不同,Docker 容器需要向主机内核发出系统调用,而这会带来更庞大的攻击面。出于安全性的考量,很多开发人员可能更倾向于选择虚拟机——其由抽象化硬件进行隔离,从而显著提升了彼此交互的难度。
当然,安全性与管理等难题必将随着容器在生产环境下的进一步普及而得到解决。就目前来讲,关于容器与虚拟机孰优孰劣的议题已经成为开发人员与运维人员间的日常争论素材。
希望通过此文能让大家掌握 Docker、虚拟机的基本概念和区别。容器时代即将到来!
![post-qrcode](https://www.xgss.net/wp-content/uploads/2023/07/二维码-微信公众号256x256.jpg)