1.2 介绍容器技术
在1.1节中,罗列了一个不全面的开发和运维团队如今所面临的问题列表,尽管你有很多解决这些问题的方式,但本书将关注如何用Kubernetes解决。
Kubernetes使用Linux容器技术来提供应用的隔离,所以在钻研Kubernetes之前,需要通过熟悉容器的基本知识来更加深入地理解Kubernetes,包括认识到存在的容器技术分支,诸如Docker或者rkt。
1.2.1 什么是容器
在1.1.1节中,我们看到在同一台机器上运行的不同组件需要不同的、可能存在冲突的依赖库版本,或者是其他的不同环境需求。
当一个应用程序仅由较少数量的大组件构成时,完全可以接受给每个组件分配专用的虚拟机,以及通过给每个组件提供自己的操作系统实例来隔离它们的环境。但是当这些组件开始变小且数量开始增长时,如果你不想浪费硬件资源,又想持续压低硬件成本,那就不能给每个组件配置一个虚拟机了。但是这还不仅仅是浪费硬件资源,因为每个虚拟机都需要被单独配置和管理,所以增加虚拟机的数量也就导致了人力资源的浪费,因为这增加了系统管理员的工作负担。
用Linux容器技术隔离组件
开发者不是使用虚拟机来隔离每个微服务环境(或者通常说的软件进程),而是正在转向Linux容器技术。容器允许你在同一台机器上运行多个服务,不仅提供不同的环境给每个服务,而且将它们互相隔离。容器类似虚拟机,但开销小很多。
一个容器里运行的进程实际上运行在宿主机的操作系统上,就像所有其他进程一样(不像虚拟机,进程是运行在不同的操作系统上的)。但在容器里的进程仍然是和其他进程隔离的。对于容器内进程本身而言,就好像是在机器和操作系统上运行的唯一一个进程。
比较虚拟机和容器
和虚拟机比较,容器更加轻量级,它允许在相同的硬件上运行更多数量的组件。主要是因为每个虚拟机需要运行自己的一组系统进程,这就产生了除组件进程消耗以外的额外计算资源损耗。从另一方面说,一个容器仅仅是运行在宿主机上被隔离的单个进程,仅消耗应用容器消耗的资源,不会有其他进程的开销。
因为虚拟机的额外开销,导致没有足够的资源给每个应用开一个专用的虚拟机,最终会将多个应用程序分组塞进每个虚拟机。当使用容器时,正如图1.4所示,能够(也应该)让每个应用有一个容器。最终结果就是可以在同一台裸机上运行更多的应用程序。
图1.4 使用虚拟机来隔离一组应用程序与使用容器隔离单个应用程序
当你在一台主机上运行三个虚拟机的时候,你拥有了三个完全分离的操作系统,它们运行并共享一台裸机。在那些虚拟机之下是宿主机的操作系统与一个管理程序,它将物理硬件资源分成较小部分的虚拟硬件资源,从而被每个虚拟机里的操作系统使用。运行在那些虚拟机里的应用程序会执行虚拟机操作系统的系统调用,然后虚拟机内核会通过管理程序在宿主机上的物理来CPU执行x86指令。
注意 存在两种类型的管理程序。第一种类型的管理程序不会使用宿主机OS,而第二种类型的会。
多个容器则会完全执行运行在宿主机上的同一个内核的系统调用,此内核是唯一一个在宿主机操作系统上执行x86指令的内核。CPU也不需要做任何对虚拟机能做那样的虚拟化(如图 1.5所示)。
图1.5 虚拟机和容器中的应用程序对CPU的不同使用方式
虚拟机的主要好处是它们提供完全隔离的环境,因为每个虚拟机运行在它自己的Linux内核上,而容器都是调用同一个内核,这自然会有安全隐患。如果你的硬件资源有限,那当你有少量进程需要隔离的时候,虚拟机就可以成为一个选项。为了在同一台机器上运行大量被隔离的进程,容器因它的低消耗而成为一个更好的选择。记住,每个虚拟机运行它自己的一组系统服务,而容器则不会,因为它们都运行在同一个操作系统上。那也就意味着运行一个容器不用像虚拟机那样要开机,它的进程可以很快被启动。
容器实现隔离机制介绍
你可能会好奇,如果多个进程运行在同一个操作系统上,那容器到底是怎样隔离它们的。有两个机制可用:第一个是Linux命名空间,它使每个进程只看到它自己的系统视图(文件、进程、网络接口、主机名等);第二个是Linux控制组(cgroups),它限制了进程能使用的资源量(CPU、内存、网络带宽等)。
用Linux命名空间隔离进程
默认情况下,每个Linux系统最初仅有一个命名空间。所有系统资源(诸如文件系统、用户ID、网络接口等)属于这一个命名空间。但是你能创建额外的命名空间,以及在它们之间组织资源。对于一个进程,可以在其中一个命名空间中运行它。进程将只能看到同一个命名空间下的资源。当然,会存在多种类型的多个命名空间,所以一个进程不单单只属于某一个命名空间,而属于每个类型的一个命名空间。
存在以下类型的命名空间:
Mount(mnt)
Process ID(pid)
Network(net)
Inter-process communicaion(ipd)
UTS
User ID(user)
每种命名空间被用来隔离一组特定的资源。例如,UTS命名空间决定了运行在命名空间里的进程能看见哪些主机名和域名。通过分派两个不同的UTS命名空间给一对进程,能使它们看见不同的本地主机名。换句话说,这两个进程就好像正在两个不同的机器上运行一样(至少就主机名而言是这样的)。
同样地,一个进程属于什么Network命名空间决定了运行在进程里的应用程序能看见什么网络接口。每个网络接口属于一个命名空间,但是可以从一个命名空间转移到另一个。每个容器都使用它自己的网络命名空间,因此每个容器仅能看见它自己的一组网络接口。
现在你应该已经了解命名空间是如何隔离容器中运行的应用的。
限制进程的可用资源
另外的隔离性就是限制容器能使用的系统资源。这通过cgroups来实现。cgroups是一个Linux内核功能,它被用来限制一个进程或者一组进程的资源使用。一个进程的资源(CPU、内存、网络带宽等)使用量不能超出被分配的量。这种方式下,进程不能过分使用为其他进程保留的资源,这和进程运行在不同的机器上是类似的。
1.2.2 Docker容器平台介绍
尽管容器技术已经出现很久,却是随着Docker容器平台的出现而变得广为人知。Docker是第一个使容器能在不同机器之间移植的系统。它不仅简化了打包应用的流程,也简化了打包应用的库和依赖,甚至整个操作系统的文件系统能被打包成一个简单的可移植的包,这个包可以被用来在任何其他运行Docker的机器上使用。
当你用Docker运行一个被打包的应用程序时,它能看见你捆绑的文件系统的内容,不管运行在开发机器还是生产机器上,它都能看见相同的文件,即使生产机器运行的是完全不同的操作系统。应用程序不会关心它所在服务器上的任何东西,所以生产服务器上是否安装了和你开发机完全相同的一组库是不需要关心的。
例如,如果你用整个红帽企业版Linux(RHEL)的文件打包了你的应用程序,不管在装有Fedora的开发机上运行它,还是在装有Debian或者其他Linux发行版的服务器上运行它,应用程序都认为它运行在RHEL中。只是内核可能不同。
与在虚拟机中安装操作系统得到一个虚拟机镜像,再将应用程序打包到镜像里,通过分发整个虚拟机镜像到主机,使应用程序能够运行起来类似,Docker也能够达到相同的效果,但不是使用虚拟机来实现应用隔离,而是使用之前几节中提到的Linux容器技术来达到和虚拟机相同级别的隔离。容器也不使用庞大的单个虚拟机镜像,它使用较小的容器镜像。
基于Docker容器的镜像和虚拟机镜像的一个很大的不同是容器镜像是由多层构成,它能在多个镜像之间共享和征用。如果某个已经被下载的容器镜像已经包含了后面下载镜像的某些层,那么后面下载的镜像就无须再下载这些层。
Docker的概念
Docker是一个打包、分发和运行应用程序的平台。正如我们所说,它允许将你的应用程序和应用程序所依赖的整个环境打包在一起。这既可以是一些应用程序需要的库,也可以是一个被安装的操作系统所有可用的文件。Docker使得传输这个包到一个中央仓库成为可能,然后这个包就能被分发到任何运行Docker的机器上,在那儿被执行(大部分情况是这样的,但并不尽然,后面将做出解释)。
三个主要概念组成了这种情形:
镜像 — Docker镜像里包含了你打包的应用程序及其所依赖的环境。它包含应用程序可用的文件系统和其他元数据,如镜像运行时的可执行文件路径。
镜像仓库 — Docker镜像仓库用于存放Docker镜像,以及促进不同人和不同电脑之间共享这些镜像。当你编译你的镜像时,要么可以在编译它的电脑上运行,要么可以先上传镜像到一个镜像仓库,然后下载到另外一台电脑上并运行它。某些仓库是公开的,允许所有人从中拉取镜像,同时也有一些是私有的,仅部分人和机器可接入。
容器 — Docker容器通常是一个Linux容器,它基于Docker镜像被创建。一个运行中的容器是一个运行在Docker主机上的进程,但它和主机,以及所有运行在主机上的其他进程都是隔离的。这个进程也是资源受限的,意味着它只能访问和使用分配给它的资源(CPU、内存等)
构建、分发和运行Dcoker镜像
图1.6显示了这三个概念以及它们之间的关系。开发人员首先构建一个镜像,然后把镜像推到镜像仓库中。因此,任何可以访问镜像仓库的人都可以使用该镜像。然后,他们可以将镜像拉取到任何运行着Docker的机器上并运行镜像。Docker会基于镜像创建一个独立的容器,并运行二进制可执行文件指定其作为镜像的一部分。
图1.6 Docker镜像、镜像仓库和容器
对比虚拟机与Docker容器
由上文可知,Linux容器和虚拟机的确有相像之处,但容器更轻量级。现在让我们看一下Docker容器和虚拟机的具体比较(以及Docker镜像和虚拟机镜像的比较)。如图例1.7所示,相同的6个应用程序分别运行在虚拟机上和用Docker容器运行。
图1.7 在3个虚拟机上运行6个应用及用Docker容器运行它们
你会注意到应用A和应用B无论是运行在虚拟机上还是作为两个分离容器运行时都可以访问相同的二进制和库。在虚拟机里,这是显然的,因为两个应用都看到相同的文件系统。但是我们知道每个容器有它自己隔离的文件系统,那应用A和应用B如何共享同样的文件?
镜像层
前面已经说过Docker镜像由多层构成。不同镜像可能包含完全相同的层,因为这些Docker镜像都是基于另一个镜像之上构建的,不同的镜像都能使用相同的父镜像作为它们的基础镜像。这提升了镜像在网络上的分发效率,当传输某个镜像时,因为相同的层已被之前的镜像传输,那么这些层就不需要再被传输。
层不仅使分发更高效,也有助于减少镜像的存储空间。每一层仅被存一次,当基于相同基础层的镜像被创建成两个容器时,它们就能够读相同的文件。但是如果其中一个容器写入某些文件,另外一个是无法看见文件变更的。因此,即使它们共享文件,仍然彼此隔离。这是因为容器镜像层是只读的。容器运行时,一个新的可写层在镜像层之上被创建。容器中进程写入位于底层的一个文件时,此文件的一个拷贝在顶层被创建,进程写的是此拷贝。
容器镜像可移植性的限制
理论上,一个容器镜像能运行在任何一个运行Docker的机器上。但有一个小警告——一个关于运行在一台机器上的所有容器共享主机Linux内核的警告。如果一个容器化的应用需要一个特定的内核版本,那它可能不能在每台机器上都工作。如果一台机器上运行了一个不匹配的Linux内核版本,或者没有相同内核模块可用,那么此应用就不能在其上运行。
虽然容器相比虚拟机轻量许多,但也给运行于其中的应用带来了一些局限性。虚拟机没有这些局限性,因为每个虚拟机都运行自己的内核。
还不仅是内核的问题。一个在特定硬件架构之上编译的容器化应用,只能在有相同硬件架构的机器上运行。不能将一个x86架构编译的应用容器化后,又期望它能运行在ARM架构的机器上。你仍然需要一台虚拟机来做这件事情。
1.2.3 rkt——一个Docker的替代方案
Docker是第一个使容器成为主流的容器平台。Docker本身并不提供进程隔离,实际上容器隔离是在Linux内核之上使用诸如Linux命名空间和cgroups之类的内核特性完成的,Docker仅简化了这些特性的使用。
在Docker成功后,开放容器计划(OCI)就开始围绕容器格式和运行时创建了开放工业标准。Docker是计划的一部分,rkt(发音为“rock-it”)则是另外一个Linux容器引擎。
和Docker一样,rkt也是一个运行容器的平台,它强调安全性、可构建性并遵从开放标准。它使用OCI容器镜像,甚至可以运行常规的Docker容器镜像。
这本书只集中于使用Docker作为Kubernetes的容器,因为它是Kubernetes最初唯一支持的容器类型。最近Kubernetes也开始支持rkt及其他的容器类型。
在这里提到rkt的原因是,不应该错误地认为Kubernetes是一个专为Docker容器设计的容器编排系统。实际上,在阅读这本书的过程中,你将会认识到Kubernetes的核心远不止是编排容器。容器恰好是在不同集群节点上运行应用的最佳方式。有了这些意识,终于可以深入探讨本书所讲的核心内容——Kubernetes了。