简介
无论你是学生,还是公司的开发人员,或是软件爱好者,相信你都听说过容器。 你可能还听说容器是轻量级虚拟机,但这究竟意味着什么,容器究竟是如何工作的,以及它们为什么如此重要呢?
本文将带你深入了解容器,它们的关键技术思想,以及它们的应用。 除了计算机科学的基本概念之外,你不需要该领域的其它任何先决知识。
内核和操作系统
你的笔记本电脑以及其它所有计算机都是基于CPU,持久存储(磁盘驱动器,SSD),内存,网卡等硬件构建的。
要与此硬件进行交互,操作系统中被称为内核的软件将充当硬件与系统其余部分之间的桥梁。内核负责调度要运行的进程(程序),管理设备(在磁盘和内存上读写地址)等等。
操作系统的其余部分用于引导和管理用户进程的运行用户空间,并将不断与内核进行交互。
内核是操作系统的一部分,并与硬件接口。 整个操作系统都位于“内核空间”中,而用户程序则位于“用户空间”中。 内核空间负责管理用户空间。
虚拟机
假定你有一台运行MacOS的计算机和一个可以在Ubuntu上运行的应用程序。一个常见的解决方案是在运行Ubuntu的MacOS计算机上启动虚拟机,然后在那里运行你的程序。
虚拟机由某些特定的硬件和内核虚拟化组成,运行客户操作系统。称为管理程序的软件创建虚拟化硬件,其可以包括虚拟磁盘,虚拟网络接口,虚拟CPU等。虚拟机还包括可以与此虚拟硬件通信的宾客内核。
管理程序可以托管,这意味着它是一些在主机操作系统(MacOS)上运行的软件,如示例中所示。它也可以是裸机,直接在机器硬件上运行(替换你的操作系统)。无论哪种方式,管理程序方法都被认为是重量级的,因为它需要虚拟化多个部分(如果不是全部硬件和内核)。
当同一台机器上需要有多个隔离组时,为每个组运行一个VM太繁重且浪费资源,不是一个好方法。
开销不按比例
VM需要硬件虚拟化才能实现机器级隔离,而容器则只需要在同一操作系统内进行隔离操作。 随着隔离空间数量的增加,开销差异变得非常明显。 普通的笔记本电脑可以运行数十个容器,但很难运行一台VM。
cgroups
2006年,Google的工程师发明了Linux“控制组”,缩写为cgroups。这是Linux内核的一项功能,可隔离和控制用户进程的资源使用情况。
这些进程可以放入命名空间,实质上是共享相同资源限制的进程集合。计算机可以有多个命名空间,每个命名空间都具有内核强制执行的资源属性。
我们可以管理每个命名空间的资源分配,以便限制一组进程可以使用的总CPU,RAM等的数量。例如,后台日志聚合应用程序可能需要限制其资源,以免意外地压倒它正在记录的实际服务器。
虽然不是原始功能,但Linux中的cgroup最终被重新设计为包含命名空间隔离的功能。命名空间隔离本身并不新鲜,Linux已经有多种命名空间隔离。一个常见的例子是进程隔离,它将每个单独的进程分开并防止诸如共享内存之类的事情。
cgroup隔离是一种更高级别的隔离,可确保cgroup命名空间中的进程独立于其他命名空间中的进程。下面概述了一些重要的命名空间隔离功能,为我们对容器的隔离奠定了基础。
- PID(进程标识符)命名空间:这可确保一个命名空间内的进程不知道其他命名空间中的进程。
- 网络命名空间:隔离网络接口控制器,iptables,路由表和其他低级网络工具。
- 挂载命名空间:已挂载文件系统,因此命名空间的文件系统范围仅限于已挂载的目录。
- 用户名空间:将命名空间内的用户限制为仅限该命名空间,并避免跨命名空间的用户ID冲突。
简单地说,每个命名空间看起来都是它自己的机器。
Linux 容器
Linux cgroups为一种名为Linux容器(LXC)的技术铺平了道路。 LXC实际上是我们今天所知的第一个实现容器的主要实现,利用cgroup和命名空间隔离来创建具有独立进程和网络空间的虚拟环境。
从某种意义上说,这允许独立和隔离的用户空间。 容器的概念直接来自LXC。 事实上,早期版本的Docker直接构建在LXC之上。
Docker
Docker是最广泛使用的容器技术,也是大多数人在引用容器时的意思。 虽然还有其他开源容器技术(如CoreOS的rkt)和大型公司构建自己的容器引擎(如谷歌的lmctfy),但Docker已成为容器化的行业标准。 它仍然建立在Linux内核和最近的Windows提供的cgroups和命名空间之上。
图片来源:Docker
Docker容器由多层镜像组成,二进制文件一起打包到一个包中。 基本镜像包含容器的操作系统,该操作系统可以与主机的操作系统不同。
容器的操作系统是镜像形式。 这不是主机上的完整操作系统,不同之处在于镜像只是操作系统的文件系统和二进制文件,而完整的操作系统包括文件系统,二进制文件和内核。
在基础镜像的顶部是多个镜像,每个镜像构建容器的一部分。 例如,在基本镜像的顶部可以是包含apt-get依赖性的镜像。 最重要的可能是包含应用程序二进制文件的镜像,依此类推。
很酷的部分是如果有两个带有镜像层a,b,c和a,b,d的容器,那么你只需要在本地和存储库中存储每个镜像层a,b,c,d的一个副本。 这是Docker的联合文件系统。
由散列标识的每个镜像只是构成容器的许多可能镜像层之一。但是,容器仅由其顶级镜像标识,该镜像具有对父镜像的引用。此处显示的两个顶级镜像(镜像1和镜像2)共享前三个图层。镜像2具有两个附加的配置相关层,但与镜像1共享相同的父镜像。
引导容器时,将从repo下载镜像及其父镜像,创建cgroup和命名空间,并使用该镜像创建虚拟环境。在容器内,镜像中指定的文件和二进制文件似乎是整个计算机中的唯一文件。然后启动容器的主进程,并将容器视为活动状态。
Docker还有其他一些非常酷的功能,例如写入时复制,卷(容器之间的共享文件系统),docker守护程序(管理机器上的容器),版本控制的存储库(如容器的Github)等等。要了解有关它们的更多信息并查看有关如何使用Docker的一些实际示例,这篇Medium上的文章非常有用。
命令行客户端(1)告诉计算机上的进程名为docker daemon(2)该做什么。 守护程序从注册表/存储库中提取图像(3)。 这些镜像在本地计算机上缓存(4),并且可以由守护程序启动以运行容器(5)。 图片来源:Docker。
为什么使用容器
除了工艺隔离外,容器还具有许多其他有益的特性。
容器可作为一个独立的单元,可以在任何支持它的地方运行。在每个实例中,容器本身都是完全相同的。如果主机操作系统是CentOS,Ubuntu,MacOS,甚至是像Windows这样的非UNIX系统都无关紧要——从容器内部看操作系统将是容器指定的任何操作系统。因此,你可以确定你在笔记本电脑上构建的容器也将在公司的服务器上运行。
容器还充当标准化的工作或计算单元。一个常见的范例是每个容器运行单个Web服务器,数据库的单个分片或单个Spark工作程序等。然后,为了扩展应用程序,你只需要扩展容器的数量。
在这个范例中,每个容器都有一个固定的资源配置(CPU,RAM,线程数等),并且扩展应用程序需要只扩展容器的数量而不是单个资源原语。当应用程序需要按比例放大或缩小时,这为工程师提供了更容易的抽象。
容器也是实现微服务架构的一个很好的工具,每个微服务只是一组协作容器。例如,可以使用单个主容器和多个从容器来实现Redis微服务。
这种(微)服务导向架构具有一些非常重要的属性,使工程团队可以轻松创建和部署应用程序(有关更多详细信息,请参阅我之前的文章)。
编排
自从Linux容器出现以来,用户一直试图在许多虚拟机上部署大型应用程序,其中每个进程都在自己的容器中运行。这样做需要能够在数百个虚拟机中有效地部署数十到数千个容器,并管理他们的网络,文件系统,资源等。今天Docker使它更容易,因为它公开了抽象来定义容器网络,文件卷系统,资源配置等。
但是仍然需要一个工具来:
- 真实地采取规范并将容器分配给机器(调度)
- 真实地通过Docker在机器上启动指定的容器
- 处理升级/回滚/系统不断变化的性质
- 应对容器崩溃等故障
- 创建集群资源,如服务发现,VM间网络,集群入口/出口等。
这组问题涉及在一组(可能是瞬态的或不断变化的)容器之上构建的分布式系统的编排,人们已经建立了一些非常奇妙的系统来解决这个问题。
在下一篇文章中,我将深入讨论Kubernetes(主流的开源编排器)的实现,以及两个同样重要但鲜为人知的Mesos和Borg。
原文链接:Demystifying containers 101: a deep dive into container technology for beginners (翻译:池剑锋)