转载本文需注明出处:微信公众号EAWorld,违者必究。
前言:
容器,容器编排,微服务,云原生,这些无疑都是当下软件开发领域里面最热门的术语。容器技术的出现并迅速的广泛应用于软件开发的各个领域里,主要的原因是容器技术革命性的改变了软件开发和部署的基本方式。作为一个架构师,了解容器技术是非常重要的一个话题,我们今天就来聊聊它。
目录:
1、Chroot
2、容器 VS. 虚机
3、命名空间 Namespace
4、控制组 CGroup
5、Docker
6、ContainerD
7、RunC
8、CRI-O
9、Podman
10、LXC/LXD
11、rkt
12、Kata Container
13、其他
14、总结
容器技术是一种虚拟化技术,也就是我们常常说的“软件定义XXX”。在容器之前被广泛应用的是虚拟机技术,也就是软件定义的硬件。代表产品就是VMWare。我早年在EMC工作的时候,公司常常提起收购VMWare是多么多么英明的一项决策,那个时候虚拟机技术真的很火。例如软件模拟硬件,用户可以很方便的在自己的主机上运行不用的硬件和操作系统,并且可以方便的把整个系统的快照作为文件迁移,真的非常方便。
但是虚拟机需要模拟整个的硬件,它的开销是非常大的。内存,CPU和磁盘空间都是独占的。在系统架构的层面,虚拟机技术仍然非常有用,但是对日常的开发工作来说,虚拟机技术就太重了。
从软件开发和部署的角度来看,我们希望有一个这样的虚拟化技术:
在Unix/linux世界,我们第一个想到的类似的东西是Chroot。
Chroot是在Unix系统的一个操作,即 change root directory (更改 root 目录)。在 Linux 系统中,系统默认的目录结构都是以 `/`,即是以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 `/` 位置。由Chroot创造出的那个根目录,叫做“chroot jail”(chroot jail,或chroot prison)。
只有root用户可以执行chroot。大多数Unix系统都没有以完全文件系统为导向,以及可能通过网络和过程控制,通过系统调用接口来提供一个破坏chroot的程序。
在经过chroot 之后,系统读取到的目录和文件将不在是旧系统根下的而是新根下(即被指定的新的位置)的目录结构和文件,因此它带来的好处大致有以下3个:
但是Chroot并不能满足我们之前提到的哪些需求,chroot的隔离功能非常有限,chroot的机制本身不是为限制资源的使用而设计,如I/O,带宽,磁盘空间或CPU时间。
为了实现我们之前提到的那些需求,我们需要一种更为强大的虚拟化技术,容器也就随之发展起来。
Linux的容器和虚拟机的差比我想大家都比较了解了,我这里再简单叙述一下:
对于使用虚拟机的传统虚拟化,每个虚拟机都有自己的完整操作系统,因此在运行内置于虚拟机的应用程序时,内存使用量可能会高于必要值,虚拟机可能会开始耗尽主机所需的资源。容器共享操作系统环境(内核),因此它们比完整虚拟机使用更少的资源,并减轻主机内存的压力。
传统虚拟机可占用大量磁盘空间:除了虚拟机托管的任何应用程序外,它们还包含完整的操作系统和相关工具。
容器相对较轻:它们仅包含使容器化应用程序运行所需的库和工具,因此它们比虚拟机更紧凑,并且启动速度更快。
在更新或修补操作系统时,必须逐个更新传统计算机:必须单独修补每个客户操作系统。对于容器,只需更新容器主机(托管容器的机器)的操作系统。这显著简化了维护。
利用这些优势容器在软件开发领域里迅速发展,我已经很习惯用容器去安装各种软件应用,因为它开销很小,而且隔离性很好,我可以很方便的使用同一个软件的多个版本而不用担心冲突问题。
Linux的容器技术是如何做到这些的呢?我们来看看构建容器技术的两个核心功能命名空间Namespace和控制组CGroup。
命名空间是Linux内核的一项功能,该功能对内核资源进行分区。控制进程可以访问的资源,以使一组进程看到一组资源,而另一组进程看到另一组资源。资源可能存在于多个空间中。Linux系统以每种类型的单个名称空间开始,供所有进程使用。进程可以创建其他名称空间,并加入不同的名称空间。
Linux下常见的命名空间有:
简单的讲,利用Namespace可以实现我们想要的资源隔离,控制哪些资源可以使用。
CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物理资源 (如 cpu memory i/o 等等) 的机制。2007 年进入 Linux 2.6.24 内核,CGroups 不是全新创造的,它将进程管理从 cpuset 中剥离出来,作者是 google 的 Paul Menage。CGroups 也是 LXC 为实现虚拟化所使用的资源管理手段。
CGroup 是将任意进程进行分组化管理的 Linux 内核功能。CGroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为 CGroup 子系统或控制器。CGroup 子系统有控制内存的 Memory 控制器、控制进程调度的 CPU 控制器等。运行中的内核可以使用的 Cgroup 子系统由/proc/cgroup 来确认。
CGroup的主要功能:
CGroup常见的子系统包括:
每个subsystem会关联到定义的cgroup上,并对这个cgroup中的进程做相应的限制和控制.
简单的讲,利用CGroup,可以控制能使用的资源的量。
有了Namespace和CGroup这两个特性,容器做到了控制资源隔离和访问的量。但是我们还是需要方便的管理功能和接口,Docker在容器的基本功能的基础上提供了出色的管理功能和接口,成为了容器领域里的事实标准,我们一般说容器,默认的是用Docker的技术。
Docker 是一个开放源代码软件,是一个开放平台,用于开发应用、交付应用、运行应用。Docker允许用户将基础设施中的应用单独分割出来,形成更小的颗粒,从而提高交付软件的速度。Docker容器与虚拟机类似,但原理上,容器是将操作系统层虚拟化,虚拟机则是虚拟化硬件,因此容器更具有便携性、高效地利用服务器。
Docker的主要特性有:
• 分层容器
Docker使用AUFS / devicemApper / btrfs使用文件系统的只读层来构建容器。容器由只读层组成,这些只读层在提交后将成为容器映像。镜像是一个包含用于构建应用程序的图层的容器。当docker容器运行时,只有顶层是可读写的,下面的所有层都是只读的,顶层是临时数据,直到将其提交到新层为止。使用只读文件系统的覆盖层会带来固有的复杂性和性能损失。
• 单一应用容器
Docker将容器限制为仅一个进程。默认的docker baseimage OS模板并非旨在支持多个应用程序,进程或服务,如init,cron,syslog,ssh等。您可以想象这会引入一定的复杂性,并且对日常使用场景具有巨大的影响。由于当前的体系结构,应用程序和服务旨在在正常的多进程OS环境中运行,因此您需要找到一种Docker方式来做事或使用支持Docker的工具。对于LAMP容器的应用程序,需要构建3个相互使用服务的容器,一个php容器,一个Apache容器和一个MySQL容器。能在一个容器中建造所有3个容器吗?可以,但无法在同一容器中运行php-fpm,apache和mysqld,也无法安装单独的进程管理器(如runit或supervisor)。
• 状态分离
Docker将容器存储与应用程序分开,可以在数据卷容器中将持久性数据安装在主机中的容器外部。除非用例只是具有非持久性数据的容器,否则有可能使Docker容器的可移植性降低。这也是容器编排更容易支持无状态应用的根本原因。
• 镜像注册
Docker提供了一个公共和私有镜像注册,用户可以在其中推送和提取镜像。镜像用于组成应用程序的只读层。这使用户可以轻松共享和分发应用程序。
上图是Docker的架构图,我们看到Docker是如何提供容器的管理功能的。
在Docker 1.11版之前,Docker Engine守护进程下载容器映像,启动容器进程,公开远程API并充当日志收集守护进程,所有这些都以集中化进程的身份以root身份运行。尽管这样的集中式体系结构便于部署,但是它没有遵循Unix进程和特权分离的最佳实践;此外,这使得Docker难以与Linux初始系统(如upstart和systemd)正确集成。
如下图所示,从1.11版开始,Docker守护程序不再处理容器本身的执行。而是现在由containerd处理。更准确地说,Docker守护程序将映像准备为开放容器镜像(OCI)捆绑包,并对容器进行API调用以启动OCI捆绑包。然后使用runC启动容器化容器。
那么ContainerD和RunC又分别是神马东东呢?我们继续探索。
Containerd是行业标准的容器运行时,重点是简单性,健壮性和可移植性。containerd可用作Linux和windows的守护程序。它管理着主机系统的容器的整个生命周期,从镜像传输和存储到容器执行和监督,再到低级存储再到网络附件等等。containerd旨在嵌入到更大的系统中,而不是由开发人员或最终用户直接使用。
containerD是用Go语言构建的,有兴趣的可以去看它的代码:
https://github.com/containerd/containerd
RunC是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器。事实上,runC 是标准化的产物,它根据 OCI 标准来创建和运行容器。而 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。
RunC支持一普通用户的身份运行容器。
RunC支持容器的热迁移操作,所谓热迁移就是将一个容器进行 checkpoint 操作,并获得一系列文件,使用这一系列文件可以在本机或者其他主机上进行容器的 restore 工作。这也是 checkpoint 和 restore 两个命令存在的原因。热迁移属于比较复杂的操作,目前 runC 使用了 CRIU 作为热迁移的工具。RunC 主要是调用 CRIU(Checkpoint and Restore in Userspace)来完成热迁移操作。CIRU 负责冻结进程,并将作为一系列文件存储在硬盘上。并负责使用这些文件还原这个被冻结的进程。
上图显示了不同的容器技术是如何使用RunC的,可以看到,Docker/Podman/CRI-O都使用了RunC。那么我们看看除了Docker,现在还有哪些容器的运行时呢?
CRI-O是Kubernetes的轻量级容器运行时,这就是CRI-O提供的。该名称源于CRI(Container Runtime Interface)加开放容器倡议(OCI open container initiative ),因为CRI-O严格关注符合OCI的运行时和容器映像。CRI-O的范围是与Kubernetes一起使用,以管理和运行OCI容器。尽管该项目确实具有一些用于故障排除的面向用户的工具,但它并不是面向开发人员的工具。
上图是CRI-O的架构。
简而言之,CRI-O是用于Kubernetes内部的容器运行时接口的标准。它的出现我的理解是K8s(google)为了摆脱docker的束缚,走向开放平台的一步棋。可以看到,Docker作为一个成功的技术产品,它在商业上却似乎不是那么成功,k8s如果完成对docker的解耦,docker的前景似乎不太妙。
守护进程是人们对Docker架构的主要诟病,它带来了很多管理和安全上的问题。
Podman是一个无守护进程的容器引擎,用于在Linux系统上开发,管理和运行OCI容器。容器可以以root用户或普通用户的模式运行。
Podman管理容器使用传统的fork / exec模型,因此容器进程是Podman进程的后代。Docker使用客户端/服务器模型。执行的docker命令是Docker客户端工具,它通过客户端/服务器操作与Docker守护进程通信。然后,Docker守护程序创建容器并处理stdin / stdout与Docker客户端工具的通信。Podman可以运行于非root用户模式下,而docker的守护进程必须用root用户启动。Podman的模型被认为是更为安全的模型。同时因为唯有守护进程,你的系统看上去也更为干净。
当然Podman的问题是它还很新,管理工具和功能都很弱,你可能需要buildah来构建镜像,社区和生态都还很小。如果你想用Podman取代Docker,请谨慎操作。
LXC是Linux内核容器功能的用户空间接口。通过功能强大的API和简单的工具,它使Linux用户可以轻松地创建和管理系统或应用程序容器。
LXC是一个系统容器运行时,旨在执行“完整的系统容器”,通常由完整的操作系统映像组成。在最常见的用例中,LXC进程将引导完整的Linux发行版,如Debian,Fedora,Arch等,并且用户将与虚拟机映像进行交互。LXC也可以用于运行(但不下载)应用程序容器,但是这种用法需要对底层操作系统的详细信息有更多的了解,并且这种做法不太常见。LXC可以从各种公共镜像下载“完整系统容器”映像,并以密码方式对其进行验证。LXC没有中央守护程序,可以与instart系统(例如upstart和systemd)集成。
LXD与LXC相似,但它是liblxc之上的REST API,它派生了一个监视器和容器进程。这样可以确保LXD守护程序不是故障的中心点,并且在LXD守护程序发生故障的情况下容器可以继续运行。所有其他细节与LXC几乎相同。简单的说LXD = LXC + RestAPI
LXC是一种容器技术,可为您提供轻量级Linux容器,而Docker是基于容器的单个应用程序虚拟化引擎。它们听起来可能相似,但完全不同。与LXC容器不同,Docker容器的行为不像轻量级VM,因此不能被视为轻量级VM。Docker容器在设计上仅限于单个应用程序。你可以登录到LXC容器,将其像OS一样对待,然后安装您的应用程序和服务,它将按预期运行。您无法在Docker容器中做到这一点。Docker基础OS模板被简化为单个应用程序环境,并且没有适当的初始化或支持诸如服务,守护程序,syslog,cron或运行多个应用程序之类的东西。
rkt是为现代生产云原生环境开发的应用程序容器引擎。它具有pod-native方法,可插入执行环境以及定义明确的表面积,使其非常适合与其他系统集成。
rkt的核心执行单元是Pod,它是在共享上下文中执行的一个或多个应用程序的集合(rkt的Pod与Kubernetes编排系统中的概念同义)。rkt允许用户在Pod级别和更细粒度的每个应用程序级别应用不同的配置(例如隔离参数)。rkt的体系结构意味着在一个独立的,独立的环境中,每个pod都可以直接在经典的Unix流程模型(即没有中央守护程序)中执行。rkt实施了现代,开放,标准的容器格式,即App Container(appc)规范,但还可以执行其他容器映像,例如使用Docker创建的那些。
自2014年12月由CoreOS引入以来,rkt项目已经非常成熟并得到了广泛使用。它可用于大多数主要的Linux发行版,并且每个rkt发行版都会构建供用户安装的独立rpm / deb软件包。这些软件包还可以作为Kubernetes存储库的一部分使用,以支持rkt + Kubernetes集成的测试。rkt在Google Container Image和CoreOS Container Linux如何运行Kubernetes方面也起着核心作用。RKT以其快速,可组合和安全的提供功能而闻名。许多用户已经注意到docker的安全问题,因此CoreOS必须在2014年发布RKT作为docker的竞争对手,并且由于其功能(如安全性,可互操作性等)而变得流行。
类似Podman,rkt没有集中的守护进程,而是直接从客户端命令启动容器,从而使其与系统初始化功能(例如systemd,upstart等)兼容。
Kata Containers 是由 OpenStack 基金会管理,但独立于 OpenStack 项目之外的容器项目。它是一个可以使用容器镜像以超轻量级虚机的形式创建容器的运行时工具,Kata Containers 创建的不同容器跑在一个个不同的虚拟机(kernel)上,比起传统容器提供了更好的隔离性和安全性。同时继承了容器快速启动和快速部署等优点。
我们之前提到了虚拟机技术因为其开销的原因,受到了一定的使用限制。Kata Container可以说是虚拟机技术的逆袭,可以多快好省的建设容器社会。
如上图的架构所示,Kata Containers 其实跟 RunC 类似,也是一个符合 OCI 运行时规范的一种实现。不同之处是它给每个 Docker 容器或每个 K8S Pod 增加了一个独立的 Linux 内核 (不共享宿主机的内核),使容器具有更好的隔离性、安全性。
除了我们之前提到的,还有其他一些容器技术,我们简单的看看。
随着微服务和云原生技术的大行其道,容器的生态系统的吸引了越来越多的玩家。Docker作为昔日王者,受到了诸多其它后起之秀的挑战,我们可以预期未来更多的组织会加入到下面的这张图里。
希望本文能够帮助你了解容器技术的基本知识,在面对诸多容器技术的术语和各种容器运行时的时候,不再手足无措。
关于作者:陶刚,Splunk资深软件工程师,架构师,毕业于北京邮电大学,现在在温哥华负责Splunk机器学习云平台的开发,曾经就职于SAP,EMC,Lucent等企业,拥有丰富的企业应用软件开发经验,熟悉软件开发的各种技术,平台和开发过程,在商务智能,机器学习,数据可视化,数据采集,网络管理等领域都有涉及。
关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。