云原生:运用容器、函数计算和数据构建下一代应用
上QQ阅读APP看书,第一时间看更新

2.1 容器

最初,容器第一次站在聚光灯下是由初创公司和云计算公司推动的。但是在过去几年中,容器已经成为现代化应用程序的代名词。现如今已经很少有公司不在使用容器了,哪怕有,他们也一定是正处于评估使用容器的阶段。这也就意味着,架构师和开发者们需要了解容器能用来做什么,以及不能做什么。

如今人们在谈论容器时,大多数时候指的都是“Docker容器”。尽管在Linux操作系统(OS)中,容器的历史可以追溯到十多年以前,然而真正使得容器广为人知的,是因为Docker。容器的最初想法是把操作系统分割成几块,每一块都可以安全地运行应用程序,它们之间不会产生相互干扰。这种隔离的办法是用命名空间(namespace)和控制组(control group)来实现的。命名空间和控制组是Linux内核的功能。命名空间允许操作系统的各个组件被切分成若干块,从而创建隔离的工作区。然后,控制组可以用来对资源的使用进行细粒度的控制,从而有效防止所有系统资源被一个容器霸占。

对于开发者而言,直接调用系统内核的功能可并不是件令人愉快的事情。因此引入了Linux容器(LXC),它是现在人们所熟知的“容器”的底层技术,极大地简化了与系统内核交互的复杂性。正是Docker通过把这些复杂的内核功能封装成对开发者友好的组件,才使得容器流行起来。Docker对容器的定义是一个“标准化的软件单元”。“软件单元”更准确的意思是指运行在容器中的服务或者应用能够完全地、私有地访问操作系统视图(操作系统视图的含义是指它与其他容器实例所访问的操作系统是相互隔离的)。换句话说,你可以把容器看作是经过封装的,可以被独立部署的一个组件,这个组件通过系统级别的虚拟化技术使其可以作为一个独立的实例来运行并和其他实例共享同一个系统内核。

此外,容器使用写时复制(copy-on-write)的文件系统策略,这就允许多个容器实例可以共享数据,因为只有当容器需要修改或者写入新数据时,操作系统才会复制一个数据的副本。所以从内存和磁盘空间使用的角度来看容器是非常轻量级的,这也是为什么容器可以非常快地启动,快速启动是容器带来的巨大好处之一。容器还有其他好处,比如部署的一致性、在多环境下的可移植性、隔离性和更高的部署密度等。对于现代的云原生应用而言,容器镜像已经成为集应用服务代码的封装、运行环境、依赖项和系统库等于一体的部署单元。因为容器具有快速启动的特性,所以非常适合用在需要快速横向扩容的场景,比如云原生应用。图2-1显示了单个服务器上虚拟机(VM)和容器之间的差异。

图2-1:同一台服务器上的虚拟机和容器

2.1.1 容器隔离等级

由于容器基于操作系统虚拟化技术,因此它们在同一台机器上运行时是共享一个操作系统内核的。尽管与虚拟机提供的基于硬件虚拟化的隔离相比还有一定差距,但是这种系统级别的隔离在大部分情况下已经足够了。若以VM为基础来构建云原生应用则会有以下缺点:

· 虚拟机的启动需要整个操作系统,因此花费的启动时间会更长。

· 虚拟机镜像占用的空间也是一个问题。一台虚拟机包含了整个操作系统,通常有几个GB大小。而镜像通常存储在中央镜像仓库中,通过网络来传输它们则需要花不少时间。

· 虚拟机的扩容也是一个挑战。纵向扩容(增加更多资源)意味创建然后启动一个新的、更大的虚拟机(更多CPU、内存、存储空间等)实例。而横向扩容意味着创建新的实例,因为虚拟机的启动很慢,所以往往无法达到快速响应的目的。

· 虚拟机还会带来一些额外的开销,会消耗更多的内存、CPU和存储资源。所以它部署的密度(同一台机器上虚拟机实例的数量)就会受到制约。

最常见的需要硬件虚拟化级别隔离的场景是有潜在风险的多租户场景。你需要防止一个用户试图恶意突破隔离,尝试访问位于同一个基础架构或同一台服务器上的其他租户的资源。云服务提供商会在内部采用一些技术手段,在保持容器的启动速度和效率优势的情况下同时满足虚拟机级别的隔离性。诸如此类的技术有Hyper-V容器、沙盒容器(sandbox container)和微虚机(MicroVM)等。以下是最流行的一些微虚机技术(排名不分先后):


Nabla容器

Nable容器通过使用Solo5项目中的unikernel技术来实现更好的隔离,这种技术限制了从容器对宿主机内核的系统调用。Nable容器的运行时环境(runc)是一个符合OCI(Open Container Initiative)规范的运行时环境。我们会在本章后面详细介绍OCI。


谷歌的gVisor

这是一个用Go语言编写的运行于用户空间的内核,它提供了容器的运行时环境。新内核是一个可以满足系统调用需求的非特权进程,它防止了容器内与用户空间的直接交互。gVisor的运行时环境(runSC)是符合OCI规范的,同样也支持Kubernetes编排工具。


微软的Hyper-V容器

微软的Hyper-V容器在几年前就推出了,它是基于虚拟机的工作进程(vmwp. exe)的。这种容器能够在兼容OCI标准的同时提供完整的虚拟机级别的隔离。但是如果你想在生产环境中使用Kubernetes来管理Hyper-V容器的话,你得先等到Kubernetes能够正式在Windows上运行。


Kata容器

Kata容器结合了Hyper.sh和Intel的clear container,并提供了经典的硬件辅助虚拟化。Kata容器兼容Docker的OCI规范和Kubernetes的容器运行时接口CRI(Container Runtime Interface)。


亚马逊的Firecracker

Firecracker是亚马逊的Lambada服务的底层技术基础,并且已经在Apache 2.0许可下开源。它是一种用户模式的VM解决方案,位于KVM API之上,旨在运行现代Linux内核。Firecracker的目标是以管理程序隔离(hypervisor-isolated)的方式来运行Linux容器,该隔离和Kata容器这样的容器隔离技术有些类似。需要注意的是,在撰写本书时,Firecracker还不支持与Kubernetes、Docker或Kata容器一起使用。


图2-2提供了对这些技术隔离等级的概览。

图2-2:虚拟机、容器和进程的隔离等级

2.1.2 容器编排

若想要动态地管理容器的生命周期,你就需要一个容器编排工具。容器编排主要包含以下内容:

· 在集群节点上创建和部署容器实例。

· 容器的资源管理,即把容器部署在有足够运行资源的节点上,或者当这个节点的资源超出限额时可以将容器转移到别的节点上。

· 监控容器以及集群节点的运行状况,以便在容器或者节点出现故障时进行重启或重新编排。

· 在集群内对容器进行扩容或收缩。

· 为容器提供网络映射服务。

· 在集群内为容器提供负载均衡服务。

尽管有好几种容器编排工具存在,但是毫无疑问,Kubernetes是目前最流行的一款集群管理及容器编排工具。

2.1.3 Kubernetes概述

Kubernetes(通常缩写为k8s)是一个用于运行和管理容器的开源项目。谷歌公司于2014年开源该项目,Kubernetes常常被视为容器平台、微服务平台以及为云计算提供可移植性保障的中间层。如今,几乎所有主流的云服务提供商都推出了托管Kubernetes服务。

Kubernetes集群包含三大类组件:主节点组件、节点组件以及插件。主组件提供了集群的控制管理功能,是集群的控制层,主要负责集群的全局任务,比如集群的任务调度、事件响应(当有服务故障时重启服务以及当某个服务的实例数量不够时启动新的实例等)。主组件理论上可以在集群中的任意一个节点上运行,但是通常都会把它们部署在专用主节点上。云服务商提供的托管Kubernetes服务一般会负责集群控制层的管理,包括按需升级和打补丁等工作。

Kubernetes主节点组件包括了以下组件:


API服务器(kube-apiserver)

该组件对外暴露了Kubernetes的API接口,是Kubernetes的前端控制层。


etcd

这是一个键值数据库,用来保存集群数据。


调度器(kube-scheduler)

调度器的主要作用是监控新创建的pod(这是Kubernetes集群内的一组容器的集合,这章后面我们会详细介绍),为这些pod找到最合适的节点,并将pod调度到这个节点上。


集群控制器管理组件(kube-controller-manager)

管理一系列的控制器,这些控制器负责节点故障的恢复,维持服务实例的数量等工作。云控制器管理组件(cloud-controller-manager


负责管理与底层云服务提供商交互的控制器。

群集中的每个节点上都会运行节点组件,它们也被称之为数据层。主要作用是负责维护运行中的pod以及它们自身所在节点的环境。

Kubernetes节点组件包括了以下组件:


kubelet

运行在集群中每个节点上的节点代理,负责根据pod规格来创建pod并运行其包含的容器。


kube-proxy

kube-proxy组件负责维护节点上的网络规则并执行连接转发。


容器运行时

负责运行容器的软件(参见2.1.4节)。


图2-3展示了Kubernetes主节点组件和工作节点组件的关系。

图2-3:Kubernetes主节点组件与工作节点组件

Kubernetes通常还会部署由主节点组件和工作节点组件管理的插件。这些插件包括了域名系统(DNS)和用户管理界面(UI)等服务。

这本书不会带你去深入了解Kubernetes。但是,有一些基本概念必须要掌握:


pod

pod基本上可以认为是一个管理容器整个生命周期的一个封装。它里面包含了一个或者多个容器、存储资源、唯一的网络IP。尽管Kubernetes支持一个pod可以包含多个容器,但在大多数情况下每个pod只有一个运行真正应用程序的容器,其他都是边车容器边车容器又称辅助容器,是指与应用容器运行在同一个pod中,起到辅助作用的容器。(sidecar container)。换句话说,通过边车容器来扩展或增强应用容器功能的这种模式非常受欢迎。像Istio这样的服务网格(service mesh)很大程度上依赖于边车容器,在第3章我们会介绍这个。


服务(service)

在Kubernetes中,一个服务是对集群中运行的一组pod的抽象,它向外提供一个可以访问这组pod的稳定接入点。Kubernetes使用标签选择器(label selector)来识别服务所指向的那些pod。


副本集(replicaSet)

最简单的理解就是把副本集当作是一组服务的实例。你可以简单地定义一个pod需要多少个实例运行,Kubernetes会负责保证在任何情况下都有指定数量的实例在运行。


部署(deployment)

一个“部署”指的是Kubernetes中的一个部署描述文件,你可以在该文件中描述你所期望的部署结果,部署控制器(deployment controller)会按指定的节奏来改变实际状态,直至达到期望状态。换句话说,你可以用部署文件来部署或监控服务的副本集、调整副本集的数量、更新服务、把服务回滚到之前的版本、清理旧的服务副本集,等等。


图2-4展现了Kubernetes的基础概念以及它们相互之间的逻辑关系。

图2-4:Kubernetes的基础概念

2.1.4 Kubernetes和容器

Kubernetes其实只是一个容器的编排平台,所以它需要一个容器运行时来管理容器的生命周期。Kubernetes从诞生开始就支持Docker运行时,但它并不是Kubernetes唯一支持的容器运行时。事实上,Kubernetes社区已经推出了一个将不同容器运行时集成到Kubernetes的通用方法。事实证明,接口是一种很好的软件模式,可以协调两个不同的系统,因此,Kubernetes社区创建了容器运行时接口(Container Runtime Interface, CRI)。CRI的存在避免了把特定的容器运行时“硬编码”进Kubernetes的代码中,这样也就避免了一旦容器运行时发生变化时就需要修改Kubernetes代码的麻烦。CRI的做法是定义了一组规范,这组规范描述了一个容器运行时应该实现哪些功能才能与CRI兼容。CRI所描述的这些功能可以满足pod中容器的整个生命周期(启动、停止、暂停、终止、删除)的管理以及容器镜像的管理(例如,从容器注册服务器下载镜像),同时还包括了一些辅助功能,如日志和指标的收集以及网络通信。图2-5高度概括了Docker和Kata容器在CRI中的体系结构。

图2-5:Kubernetes中Docker容器和Kata容器的对比示意图

下面列出的是一些其他跟容器相关的技术,可能会对你的学习有帮助:


OCI

OCI是一个Linux基金会下的项目,它旨在为容器的镜像和运行时设计一个开放标准。许多容器技术都会遵循OCI标准来实现其容器运行时和镜像规范。


containerd

如果要说一个最受欢迎的容器运行时的话,那非containerd莫属。它是Docker和Kubernetes CRI所使用的一个行业标准级的容器运行时。它作为一个Linux和Windows系统中的守护进程出现,负责管理它所在的这个主机上所有容器的整个生命周期(包括容器镜像的管理、容器的运行、底层存储、网络配置,等等)。


Moby

Moby是Docker创建的一组开源工具,用于方便和快速地将软件容器化。这组工具包含了容器的构建工具、容器注册服务器、编排工具、运行时,等等。你可以将这些工具与其他工具和项目结合使用。Moby使用containerd作为默认的容器运行时。