关于容器、虚拟机以及 Docker 的一个入门教程

Yves yao · 2017-09-05翻译 · 1315阅读  原文链接 huangxiaolu审校
 
源地址:http://zcfy.cc/article/a-beginner-friendly-introduction-to-containers-vms-and-docker-4139.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
 
 
 

Source: https://flipboard.com/topic/container

如果你是一个开发者或者技术专家,你一定对 Docker 有所耳闻:一个在 “容器” 中打包、传输以及运行应用的实用工具。它这段时间得到的关注很不容易,不只包括开发者和系统管理员,甚至像 Google、VMware 以及 Amazon 这样的大公司也正在建立相应的服务去支持它。

不管你对于 Docker 是否有一个整体的概念,我认为最重要的基础概念包括:理解一个 “容器” 到底是什么,以及它与虚拟机(VM)的比较。尽管现在网上有很多出色的 Docker 使用指南,但是我找不到对于初学者友好的理论教程,特别是一个容器到底是由什么构成的。所以,希望本文可以解决这个问题:)

让我们看看虚拟机和容器到底是什么。

什么是“容器”和“虚拟机”?

容器和虚拟机的目标是一致的:通过一个独立单元将应用与其依赖隔离,这个独立单元可以在任何地方运行。

更进一步,容器和虚拟机消除了对物理硬件的依赖,因此可以在权衡能源消耗和成本效益的前提下,更加有效地利用计算资源。

容器和虚拟机最主要的区别在于它们的架构,让我们深入了解一下。

虚拟机(VM)

虚拟机基本用于模拟真实机器并像真实机器一样地运行程序。虚拟机通过虚拟机管理程序在物理机器之上运行。而虚拟机管理程序运行在主机或“硬件”上。

让我解释一下这段术语:

虚拟机管理程序 是供虚拟机运行的一段软件、固件或硬件。虚拟机管理程序本身运行于实体电脑(也被称为“主机”)上。主机为虚拟机提供包括 RAM 以及 CPU 在内的资源。这些资源在虚拟机之间隔离,并且可以按你认为合适的方式分配。所以如果一个虚拟机正在运行一个很占资源的大应用时,那么相较于同一主机上的其他虚拟机,你应该为它分配更多的资源。

综上所述,一个客户机既可以在 托管管理程序(hosted hypervisor) 上运行,也可以在 硬件管理程序(bare-metal hypervisor) 上运行。它们之间有几条很重要的差异:

首先,托管管理程序运行于主机的操作系统之上。举个例子,一个搭载 OSX 的电脑可以在这个操作系统之上安装一个虚拟机 (例如 VirtualBox 或 VMware Workstation 8)。这个虚拟机并没有直接访问硬件的权限,而需要通过操作系统(即我们例子中的 Mac OSX)去访问。

托管管理程序的好处在于它对下层硬件没有强依赖。硬件驱动工作是由主机的操作系统而不是管理程序完成,因此它拥有更高的“硬件兼容性”。另一方面,托管管理程序和这硬件之间的这一层增加了额外的资源开销,因此降低了虚拟机的性能。

硬件管理程序直接安装并运行在主机的硬件上,因此解决了性能问题。因为它直接与下层硬件对接,所以它不需要依赖操作系统,管理程序会直接作为操作系统被首先安装在主机上。与托管管理程序不同,硬件管理程序拥有自己的设备驱动并与电脑直接交互来完成 I/O、处理以及操作系统任务。这样做带来了更好的性能、可扩展性和稳定性。但这么做的代价是硬件兼容性因管理程序内置的设备驱动而受限。

看完上面关于虚拟机管理程序的讨论,你可能会疑惑为何虚拟机和主机之间需要这个额外的“管理层”。

由于虚拟机本身有一个虚拟操作系统,因此这个管理程序为虚拟机提供了一个管理和运行这个操作系统的平台。基于这个平台,主机可以在这个平台之上运行的虚拟机之间共享资源。

关于容器、虚拟机以及 Docker 的一个入门教程_第1张图片

虚拟机图

如图所示,虚拟机囊括了虚拟硬件、内核(即操作系统)及用户空间。

容器(Container)

与提供硬件虚拟的虚拟机不同,容器通过抽象“用户空间”提供了一个操作系统级别的虚拟。我们接下来通过拆开容器这个术语来理解这句话的意思。

容器的意图和目标与虚拟机完全相同。例如,它们都拥有 process 所需的私有空间,都可以以 root 的身份运行命令,拥有私有的网络接口和 IP 地址,提供自定义路由和 iptable 规则,都可以挂载文件系统等等。

容器和虚拟机最大的区别在于容器与其他容器共享系统内核。

关于容器、虚拟机以及 Docker 的一个入门教程_第2张图片

容器图

如图所示,容器只包含了用户空间,而没有像虚拟机那样还包括了内核和虚拟硬件。每一个容器有自己独立的用户空间以保证多个容器可以同时运行在同一主机上。我们可以看到所有的操作系统级的架构都被容器共享。真正需要创建的部分只有 bins 和 libs,因此容器相当轻量。

Docker 是哪儿来的?

Docker 是一个基于 Linux 容器的开源项目。它使用了 Linux 内核的特性如命名空间(namespaces)和控制单元(control groups)来在操作系统上创建容器。

容器早就不是什么新概念了 —— Google 早在多年前就开始使用他们自己的容器技术。其他的 Linux 容器技术包括 Solaris Zones,BSD jails 和 LXC 也都出现很多年了。

所以为什么 Docker 突然获得了如此高的人气?

  1. 易用: 对于所有的开发者、系统管理员、架构师以及其他人,Docker 为他们提供了最便捷的方式去快速搭建和测试便携式应用。任何人都可以利用 Docker 在他们的笔记本电脑上打包一个应用程序,而这个程序可以在任何公共云、私有云甚至硬件上无需任何修改地运行。也就是所谓的:“一处构建,多端运行”。

  2. 速度: Docker 容器非常轻量和迅捷。因为容器只是内核之上的沙盒环境,因此它们占用更少的资源。你可以秒秒钟创建并运行一个 Docker 容器,而虚拟机需要花费更多的时间,因为它每次都需要启动整个操作系统。

  3. Docker Hub: Docker 用户还可以享受到 Docker Hub 越来越丰富的生态系统,你可以把它当做“Docker 镜像的 app store”。Docker Hub 拥有数万个社区创建的公开镜像可供随时使用。搜索你需要的镜像相当容易,下载之后只需做很少的修改甚至不用修改就可以使用。

  4. 模块化和可扩展性: Docker 帮助你特别容易地将你的应用按照功能划分到多个独立的容器中。比如,你可以在一个容器中运行你的 Postgres 数据库,在另一个上运行 Redis 服务器,在另一个上搭载 Node.js 应用。利用 Docker 可以很轻松地把这些容器连接起来创建你的应用,而且以后可以更加容易地单独扩展和更新这些组件。

最后但同样重要的,谁会不喜欢 Docker 的大鲸鱼呢?;)

关于容器、虚拟机以及 Docker 的一个入门教程_第3张图片

来源: https://www.docker.com/docker-birthday

Docker 的基础概念

现在我们心里已经有了一个总体概念了,让我们一起过一遍 Docker 的基础部分。

关于容器、虚拟机以及 Docker 的一个入门教程_第4张图片

Docker 引擎(Docker Engine)

Docker 引擎是 Docker 所运行的层。它是一个轻量的管理容器、镜像、构建等的运行环境和工具。它在 Linux 系统本地运行,由以下部分组成:

  1. 一个跑在主机上的 Docker 守护进程。

  2. 一个 Docker 客户端用于与 Docker 守护进程沟通以执行命令。

  3. 一个 REST API 用于与 Docker 守护进程远程交互。

Docker 客户端(Docker Client)

Docker 客户端是作为 Docker 终端用户的你与 Docker 交互的地方。你可以把它当做 Docker 的 UI。当你执行docker build iampeekay/someImage . 这个命令时,你是在与 Docker 客户端交互,它随后会将你的指令传给 Docker 守护进程。

Docker 守护进程(Docker Deamon)

Docker 守护进程是真正执行发送到 Docker 客户端的指令的地方,这些指令包括构建、运行以及分发你的容器。Docker 守护进程运行在主机上,但是作为用户,我们永远无法直接与守护进程交互。Docker 客户端也可以运行在主机上,但不强制,你可以把它放在另一台与 Docker 所在的主机相连接的机器上。

Dockerfile

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.04 |
 MAINTAINER preethi kasireddy [email protected] |
 # 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 示例

Docker 镜像(Docker Image)

镜像是你从 Dockerfile 中的指令构建出来的只读模板。镜像定义了你想让你的应用包及其依赖长什么样子以及当它启动后你想运行的进程。

Docker 镜像是由 Dockerfile 构建而来的。Dockerfile 中的每一个指令都为镜像创建了一个新的“层”,用这些层表示镜像文件系统的一部分,它们可以添加或替换它们之下的层。层是 Docker 轻量但强大的结构的关键。Docker 使用了一种联合文件系统来做到这点:

联合文件系统(Union File Systems)

Docker 使用联合文件系统来创建一个镜像。你可以把联合文件系统当做是一个“可堆叠的文件系统”,即文件和单独文件系统(也叫分支)的文件夹都可以被无感地覆盖来形成单个文件系统。

覆盖的分支中拥有相同路径的文件夹的内容都会被看做是一个合并文件夹,这样避免了将每一层都拷贝一次。相反,它们都会被赋予指向相同资源的指针;当某层需要变更时,它将创建一份拷贝并在本地的拷贝上修改,而原始的层并不会被改变。这就是为什么文件系统看起来可写实则不允许写入(换句话说,叫“先拷贝再写”系统)。

分层系统有两个主要优势:

1. 可自由复制: 每当你使用一个镜像去创建并运行一个新的容器时,层有助于避免每次都要复制一组完整的文件。这让 Docker 容器的实例化又快又轻。 2. 层隔离: 做出改变要快得多 —— 当你改变一个镜像时,Docker 只需将变更应用到你改变了的层上即可。

卷(Volumes)

卷是一个容器的“数据”部分,在创建容器时被初始化。数据卷独立于默认的联合文件系统,并以常规目录和文件的存在于主机文件系统中。所以,即使你销毁、更新或重新构建了一个容器,数据卷都保持不变。如果你想更新一个数据卷,你需要直接去修改它。(额外的好处是,数据卷可以被多个容器共享和重用,这种做法很整洁。)

Docker 容器(Docker Containers)

如上面所述,Docker 容器将应用程序的软件及其运行所需的所有东西封装到一个不可见的盒子中。包括操作系统、应用源码、运行环境、系统工具、系统库等等。Docker 容器由 Docker 镜像构建而来。由于镜像是只读的,因此 Docker 在镜像的只读文件系统之上新增了一个读-写文件系统以创建一个容器。

关于容器、虚拟机以及 Docker 的一个入门教程_第5张图片

来源: Docker

接下来是创建容器,Docker 创建了一个网络结构来让容器与本地服务器沟通,为容器附加了一个可用的 IP 地址,并执行你在定义镜像时指定的运行你的应用所需的命令。

一旦你成功创建了一个容器,你可以在任何环境下去运行它而不需要做任何修改。

双击 “容器”

哦呦!这里有很多活动部件。最让我好奇的是一个容器是如何被真正实例化的,因为容器周围没有任何抽象的基础边界。通过大量的阅读我总算理解了,让我试着解释给你!:)

“容器”这个术语实际上只是一个抽象的概念,用于描述如何让一些特性像一个“容器”一样地在一起工作。让我们快速地过一遍这些特性:

1) 命名空间(Namespaces)

命名空间为容器提供了他们对下层 Linux 系统的视图,限制了容器可以看到和访问到的范围。当你运行一个容器的时候,Docker 会创建一个命名空间供它使用。

Docker 使用的内核中有很多种命名空间,比如:

a. NET: 为容器提供了它自己的系统网络堆栈视图(比如它自己的网络设备、IP 地址、IP 路由表、/proc/net 文件夹、端口号等)。 b. PID: PID 表示进程 ID。如果你在命令行中跑过 ps aux来查看你系统中的进程,你将会看到有一列叫“PID”。PID 命名空间为容器提供了它们自己的域中的进程,它们可以查看并与这些进程交互。这些进程包含一个独立的初始化进程(PID 1),这个进程是“所有进程的祖先”。 c. MNT: 为容器提供了一个对系统的“挂载”的视图。所以不同挂载命名空间中的进程拥有不同的文件系统层次结构视图。 d. UTS: UTS 表示UNIX分时系统(UNIX Timesharing System)。它提供了一个进程去识别系统标识符(即主机名、域名、等)。UTS 使容器有独立于其他容器和主机系统的主机名和 NIS 域名。 e. IPC: IPC 表示进程间通信。IPC 命名空间用于在隔离每个容器中运行的进程之间 IPC 资源。 f. USER: 这个命名空间用于隔离每个容器中的用户。它给每个容器提供了一个独立的视图来存放 uid (用户 ID) 和 gid (组 ID) 与主机系统的对照。因此,一个进程的 uid 和 gid 在一个用户命名空间的里面和外面是可以不同的,这么做也使得一个进程可以在不牺牲 root 权限的前提下,在容器之外拥有一个特权用户。

Docker 用这些命名空间隔离并开始容器的创建。下一个特性叫对照组。

2) 对照组(Control groups)

对照组(也叫 cgroups)是 Linux 的内核特性,用于隔离、优先考虑以及计算一组进程的资源占用情况(CPU、内存、磁盘I/O、网络等)。在这个意义上讲,一个 cgroup 保证了 Docker 容器只使用它们需要的资源,而且如果需要的话可以限制一个容器能够使用的资源。cgroup 也保证了单个容器不会耗尽其中的一个资源,从而导致整个系统崩溃。

最后,Docker 也使用了联合文件系统特性:

3) 独立联合文件系统(Isolated Union file system)

这个在上面的 Docker 镜像部分已经讲过了 :)

这就是一个 Docker 容器的全部了(当然,实现细节还是很恐怖的 —— 比如如何管理各个组件之间的交互)。

Docker 的未来:Docker 将与虚拟机共存

尽管 Docker 很火,但是我不认为它会对虚拟机造成威胁。容器将会继续取得进展,但是有很多场景还是更适合使用虚拟机。

例如,如果你需要在多个的服务器上跑多个应用,那么使用虚拟机可能会更合适。另一方面,如果你需要跑一个应用的多份拷贝,那么 Docker 更有优势。

更进一步,虽然容器使你可以将应用程序分解为功能更为独立的部分,从而分散了维护压力,但是这也意味着你需要管理更多部分,这有可能会比较繁重。

Docker 容器的安全性也是需要考虑的一个问题 —— 因为容器共用一个内核,因此容器之间的屏障比较脆弱。一个完整的虚拟机只能向管理程序发起超级调用(hypercall),而一个 Docker 容器只可以向主机内核发起系统调用(syscall),这带来了更大的攻击面。当对安全性要求特别高的时候,开发者倾向于使用虚拟机,虚拟机被抽象硬件隔离,因此它们之间更不容易互相干扰。

当然,随着容器在生产过程中得到更多的暴露加之用户的进一步审查,安全和管理等问题会得到一定的改善。但就现在而言,容器和虚拟机之间的高下之分最好还是由每天使用它们的开发和运营去决定吧!

结论

我希望你现在已经掌握了关于Docker所需的知识,甚至以后能在项目中使用它

 

 

你可能感兴趣的:(关于容器、虚拟机以及 Docker 的一个入门教程)