为Kubernetes构建优化容器

容器图像是在Kubernetes中定义应用程序的主要包装格式。作为pod和其他对象的基础,图像在平台上的应用程序的成功和Kubernetes可以提供的支持量方面起着重要作用。 ...

介绍

容器图像是在Kubernetes中定义应用程序的主要包装格式。 作为pod和其他对象的基础,图像在利用Kubernetes的功能有效地在平台上运行应用程序方面发挥着重要作用。 精心设计的图像是安全的,高性能和集中的。 他们能够对Kubernetes提供的配置数据或指令做出反应,并实现业务流程系统用于理解内部应用程序状态的端点。

在本文中,我们将介绍一些创建高质量图像的策略,并讨论一些通用目标,以帮助在容器化应用程序时指导您的决策。 我们将专注于构建要在Kubernetes上运行的映像,但许多建议同样适用于在其他业务流程平台或其他上下文中运行容器。

高效容器图像的特征

在我们讨论构建容器映像时要采取的具体操作之前,我们将讨论什么是一个好的容器映像。 在设计新图像时,您的目标应该是什么? 哪些特征和行为最重要?

目标的一些特质是:

一个明确定义的目的

容器图像应该具有单个离散焦点。 避免将容器映像视为虚拟机,将相关功能打包在一起是有意义的。 相反,将您的容器图像视为Unix实用程序,严格注重做好一件小事。 可以在容器范围之外协调应用程序以组成复杂的功能。

通用设计,能够在运行时注入配置

容器图像的设计应尽可能考虑重用。 例如,通常需要在运行时调整配置的能力,以满足基本要求,例如在部署到生产之前测试映像。 小型通用图像可以组合在不同的配置中,以修改行为而无需创建新图像。

图像尺寸小

较小的图像在像Kubernetes这样的集群环境中具有许多优点。 它们可以快速下载到新节点,并且通常具有较小的已安装软件包集,这可以提高安全性。 减少容器图像使得通过最小化所涉及的软件量来调试问题变得更加简单。

外部管理状态

集群环境中的容器经历了非常不稳定的生命周期,包括由于资源稀缺,扩展或节点故障导致的计划内和计划外停机。 为了保持一致性,帮助恢复和提供服务,并避免丢失数据,将应用程序状态存储在容器外的稳定位置至关重要。

容易明白

尝试保持容器图像尽可能简单易懂是很重要的。 进行故障排除时,通过查看容器映像配置或测试容器行为来轻松解决问题可以帮助您更快地达到分辨率。 将容器图像视为应用程序的打包格式而不是机器配置可以帮助您实现正确的平衡。

遵循容器化软件的最佳实践

图像的目标应该是在容器模型中工作而不是对其进行操作。 避免实施传统的系统管理实践,例如包括完整的初始化系统和守护进程应用程序。 记录到标准输出,以便Kubernetes可以将数据公开给管理员,而不是使用内部日志记录守护程序。 这些都与完整操作系统的最佳实践不同。

充分利用Kubernetes功能

除了符合容器模型之外,理解和协调Kubernetes提供的环境和工具也很重要。 例如,根据配置或环境的变化提供活动和准备就绪检查或调整操作的端点可以帮助您的应用程序使用Kubernetes的动态部署环境。

现在我们已经建立了定义高功能容器图像的一些特性,我们可以深入研究帮助您实现这些目标的策略。

重用最小的共享基础层

我们可以从检查构建容器映像的资源开始:基本映像。 每个容器图像都是从父图像 ,用作起点的图像,或从抽象的scratch图层scratch ,是没有文件系统的空图像层。 基本映像是容器映像,通过定义基本操作系统和提供核心功能,可作为未来映像的基础。 图像由一个或多个彼此叠置的图像层组成,以形成最终图像。

直接scratch工作时,没有标准实用程序或文件系统可用,这意味着您只能访问极其有限的功能。 虽然直接scratch创建的图像可以非常简化和最小化,但它们的主要目的是定义基本图像。 通常,您希望在父映像之上构建容器映像,以设置应用程序运行的基本环境,这样您就不必为每个映像构建完整的系统。

虽然有各种Linux发行版的基本映像,但最好是慎重考虑您选择的系统。 每台新机器都必须下载父图像和您添加的任何其他图层。 对于大型图像,这会占用大量带宽,并显着延长容器首次运行时的启动时间。 没有办法削减在容器构建过程中用作父下游的图像,因此从最小父项开始是个好主意。

像Ubuntu这样功能丰富的环境允许您的应用程序在您熟悉的环境中运行,但需要考虑一些权衡因素。 Ubuntu图像(和类似的传统分布图像)往往相对较大(超过100MB),这意味着从它们构建的任何容器图像都将继承该权重。

Alpine Linux是基本图像的流行替代品,因为它成功地将许多功能打包到一个非常小的基本图像(~5MB)中。 它包括一个包含大型存储库的软件包管理器,并且具有大多数Linux环境所需的标准实用程序。

在设计应用程序时,尝试为每个图像重用相同的父级是个好主意。 当您的图像共享父级时,运行容器的计算机将仅下载父级一次。 之后,他们只需要下载图片之间不同的图层。 这意味着如果您希望在每个图像中嵌入共同的特征或功能,那么创建一个可以继承的公共父图像可能是个好主意。 共享谱系的图像有助于最大限度地减少在新服务器上下载所需的额外数据量。

管理容器层

选择父映像后,可以通过添加其他软件,复制文件,公开端口以及选择要运行的进程来定义容器映像。 图像配置文件中的某些指令(如果使用Docker, Dockerfile )将为图像添加其他图层。

由于上一节中提到的许多相同原因,请注意由于生成的大小,继承和运行时复杂性而如何向图像添加图层。 为了避免构建大而笨重的图像,重要的是要充分了解容器层如何交互,构建引擎如何缓存层,以及类似指令的细微差异如何对您创建的图像产生重大影响。

了解图像层和构建缓存

Docker每次执行RUNCOPYADD指令时都会创建一个新的图像层。 如果再次构建映像,构建引擎将检查每条指令以查看它是否为操作缓存了图像层。 如果它在缓存中找到匹配项,则它使用现有图像层而不是再次执行该指令并重建该层。

此过程可以显着缩短构建时间,但了解用于避免潜在问题的机制非常重要。 对于COPYADD等文件复制指令,Docker会比较文件的校验和,以查看是否需要再次执行操作。 对于RUN指令,Docker会检查是否有为该特定命令字符串缓存的现有图像层。

虽然它可能不会立即显而易见,但如果您不小心,此行为可能会导致意外结果。 一个常见的例子是更新本地包索引并在两个单独的步骤中安装包。 我们将在这个例子中使用Ubuntu,但基本前提同样适用于其他发行版的基本图像:

包安装示例Dockerfile
FROM ubuntu:18.04
RUN apt -y update
RUN apt -y install nginx
. . .

这里,本地包索引在一个RUN指令( apt -y update )中apt -y update ,Nginx在另一个操作中安装。 首次使用时,这没有问题。 但是,如果稍后更新Dockerfile以安装其他软件包,则可能存在以下问题:

包安装示例Dockerfile
FROM ubuntu:18.04
RUN apt -y update
RUN apt -y install nginx php-fpm
. . .

我们在第二条指令运行的安装命令中添加了第二个包。 如果自上一次映像构建以来已经过了大量时间,则新构建可能会失败。 这是因为包索引更新指令( RUN apt -y update没有改变,因此Docker重用与该指令关联的图像层。 由于我们使用旧的包索引,我们在本地记录中的php-fpm包的版本可能不再存在于存储库中,导致第二条指令运行时出错。

要避免这种情况,请确保将相互依赖的任何步骤合并到单个RUN指令中,以便Docker在发生更改时重新执行所有必需的命令:

包安装示例Dockerfile
FROM ubuntu:18.04
RUN apt -y update && apt -y install nginx php-fpm
. . .

每当包列表更改时,该指令现在都会更新本地包缓存。

通过调整RUN指令减少图像层大小

前面的示例演示了Docker的缓存行为如何能够破坏期望,但是还有一些其他事情要记住RUN指令如何与Docker的分层系统交互。 如前所述,在每个RUN指令结束时,Docker将更改作为附加图像层提交。 为了控制所生成的图像层的范围,您可以通过关注所运行命令引入的工件来清除最终环境中不必要的文件。

通常,将命令链接到一个RUN指令(如前所示)可以对要写入的层提供大量控制。 对于每个命令,您可以设置图层的状态( apt -y update ),执行核心命令( apt install -y nginx php-fpm ),并删除任何不必要的工件以在环境提交之前清理环境。 例如,许多Dockerfiles将rm -rf /var/lib/apt/lists/*apt命令的末尾,删除下载的包索引,以减少最终的图层大小:

包安装示例Dockerfile
FROM ubuntu:18.04
RUN apt -y update && apt -y install nginx php-fpm && rm -rf /var/lib/apt/lists/*
. . .

要进一步减小正在创建的图像层的大小,尝试限制正在运行的命令的其他意外副作用可能会有所帮助。 例如,除了显式声明的包之外, apt还默认安装“推荐”包。 您可以在apt命令中包含--no-install-recommends以删除此行为。 您可能需要进行试验以了解您是否依赖推荐软件包提供的任何功能。

我们在本节中使用了包管理命令作为示例,但这些相同的原则适用于其他方案。 一般的想法是构造先决条件,执行最小可行命令,然后在单个RUN命令中清除任何不必要的工件,以减少您将要生成的层的开销。

使用多阶段构建

Docker 17.05中引入了多阶段构建 ,允许开发人员更严格地控​​制他们生成的最终运行时映像。 多阶段构建允许您将Dockerfile划分为表示不同阶段的多个部分,每个部分都使用FROM语句来指定单独的父级映像。

前面的部分定义了可用于构建应用程序和准备资产的图像。 这些通常包含生成应用程序所需的构建工具和开发文件,但不需要运行它。 文件中定义的每个后续阶段都可以访问前一阶段生成的工件。

最后一个FROM语句定义将用于运行应用程序的映像。 通常,这是一个简化的映像,它只安装必要的运行时需求,然后复制前一阶段生成的应用程序工件。

此系统使您不必担心在构建阶段优化RUN指令,因为这些容器层将不会出现在最终运行时映像中。 您仍然应该注意指令在构建阶段如何与层缓存交互,但您的努力可以用于最小化构建时间而不是最终图像大小。 在最后阶段注意指令对于减小图像大小仍然很重要,但是通过分离容器构建的不同阶段,在没有Dockerfile复杂性的情况下更容易获得流线型图像。

容器和Pod级别的范围功能

虽然您对容器构建说明的选择很重要,但有关如何将服务容器化的更广泛决策通常会对您的成功产生更直接的影响。 在本节中,我们将更多地讨论如何将应用程序从更传统的环境转移到在容器平台上运行。

按功能包容

通常,优良作法是将每个独立功能包装到单独的容器图像中。

这与虚拟机环境中采用的常见策略不同,在虚拟机环境中,应用程序经常在同一映像中组合在一起,以减小运行VM所需的大小和最小化资源。 由于容器是轻量级抽象,不会虚拟化整个操作系统,因此Kubernetes的这种权衡不那么引人注目。 因此,虽然Web虚拟机可能会将Nginx Web服务器与Gunicorn应用程序服务器捆绑在一台计算机上以便为Django应用程序提供服务,但在Kubernetes中,这些可能会被拆分为单独的容器。

设计为您的服务实现一个独立功能的容器提供了许多优势。 如果建立服务之间的标准接口,则可以独立开发每个容器。 例如,如果给定不同的配置,Nginx容器可能会用于代理多个不同的后端,或者可以用作负载均衡器。

部署后,每个容器映像可以独立扩展,以解决不同的资源和负载约束。 通过将应用程序拆分为多个容器映像,您可以在开发,组织和部署方面获得灵活性。

在Pods中组合容器图像

在Kubernetes中, 吊舱是可以由控制平面直接管理的最小单元。 Pod由一个或多个容器以及其他配置数据组成,以告诉平台应该如何运行这些组件。 pod中的容器始终安排在群集中的同一个工作节点上,系统会自动重新启动失败的容器。 pod抽象非常有用,但它引入了另一层关于如何将应用程序组件捆绑在一起的决策。

与容器图像一样,当将太多功能捆绑到单个实体中时,pod也变得不那么灵活。 可以使用其他抽象来缩放窗格本身,但不能单独管理或缩放其中的容器。 因此,为了继续使用我们之前的示例,单独的Nginx和Gunicorn容器可能不应该捆绑在一起,以便可以单独控制和部署它们。

但是,有些情况下将功能不同的容器组合为一个单元是有意义的。 通常,这些可以归类为其他容器支持或增强主容器的核心功能或帮助其适应其部署环境的情况。 一些常见的模式是:

  • Sidecar :辅助容器通过支持实用角色来扩展主容器的核心功能。 例如,当远程存储库更改时,sidecar容器可能会转发日志或更新文件系统。 主要容器仍然专注于其核心职责,但通过边车提供的功能得到增强。
  • 大使 :大使容器负责发现和连接(通常是复杂的)外部资源。 主容器可以使用内部pod环境连接到众所周知的接口上的ambassador容器。 大使抽象主容器和资源池之间的后端资源和代理流量。
  • 适配器 :适配器容器负责规范主容器接口,数据和协议,以与其他组件所期望的属性对齐。 主容器可以使用本机格式操作,并且适配器容器转换和规范化数据以与外界通信。

您可能已经注意到,这些模式中的每一个都支持构建标准的通用主容器映像的策略,然后可以在各种上下文和配置中进行部署。 辅助容器有助于弥合主容器与正在使用的特定部署环境之间的差距。 一些边车容器也可以重复使用以使多个主容器适应相同的环境条件。 这些模式受益于pod抽象提供的共享文件系统和网络命名空间,同时仍允许独立开发和灵活部署标准化容器。

设计运行时配置

在构建标准化,可重用组件的愿望与使应用程序适应其运行时环境所涉及的要求之间存在一些紧张关系。 运行时配置是弥合这些问题之间差距的最佳方法之一。 组件构建为通用且灵活,并且在运行时通过向软件提供附加配置信息来概述所需行为。 这种标准方法适用于容器以及应用程序。

在构建运行时配置时,您需要在应用程序开发和容器化步骤期间提前考虑。 应用程序应设计为在启动或重新启动时从命令行参数,配置文件或环境变量中读取值。 此配置解析和注入逻辑必须在容器化之前在代码中实现。

在编写Dockerfile时,还必须考虑到运行时配置而设计容器。 容器具有许多在运行时提供数据的机制。 用户可以将文件或目录作为容器内的卷从主机安装,以启用基于文件的配置。 同样,在启动容器时,可以将环境变量传递到内部容器运行时。 CMDENTRYPOINT Dockerfile指令也可以以允许运行时配置信息作为命令参数传递的方式定义。

由于Kubernetes操纵更高级别的对象(如pod)而不是直接管理容器,因此可以使用一些机制来定义配置并在运行时将其注入容器环境。 Kubernetes ConfigMapsSecrets允许您单独定义配置数据,然后在运行时将值作为环境变量或文件投影到容器环境中。 ConfigMaps是用于存储可能因环境,测试阶段等而异的配置数据的通用对象。秘密提供类似的界面,但专门针对敏感数据设计,如帐户密码或API凭据。

通过了解并正确使用每个抽象层中可用的运行时配置选项,您可以构建灵活的组件,从环境提供的值中获取其提示。 这使得可以在非常不同的场景中重用相同的容器映像,从而通过提高应用程序灵活性来减少开发开销。

使用容器实现流程管理

在转换到基于容器的环境时,用户通常首先将现有工作负载(几乎没有或没有更改)转移到新系统。 他们通过将已经使用的工具包装在新的抽象中来将应用程序打包到容器中。 尽管使用常用模式来启动和运行迁移的应用程序是有帮助的,但在容器中的先前实现中丢弃有时会导致无效的设计。

处理像应用程序,而不是服务的容器

当开发人员在容器内实现重要的服务管理功能时,经常会出 例如,在容器或守护Web服务器中运行systemd服务可能被认为是正常计算环境中的最佳实践,但它们经常与容器模型中固有的假设冲突。

主机通过将信号发送到作为容器内的PID(进程ID)1操作的进程来管理容器生命周期事件。 PID 1是第一个启动的进程,它将是传统计算环境中的init系统。 但是,由于主机只能管理PID 1,因此使用传统的init系统来管理容器内的进程有时意味着无法控制主应用程序。 主机可以启动,停止或终止内部init系统,但无法直接管理主应用程序。 信号有时会将预期的行为传播到正在运行的应用程序,但这会增加复杂性,并不总是必要的。

大多数情况下,最好简化容器内的运行环境,以便PID 1在前台运行主应用程序。 在必须运行多个进程的情况下,PID 1负责管理后续进程的生命周期。 某些应用程序(如Apache)通过生成和管理处理连接的工作程序来本地处理此问题。 对于其他应用程序,在某些情况下可以使用包装器脚本或非常简单的init系统,如dumb-init或包含的tini init系统。 无论您选择哪种实现方式,在容器中作为PID 1运行的进程都应该适当响应Kubernetes发送的TERM信号,使其按预期运行。

管理Kubernetes的容器健康

Kubernetes部署和服务为长时间运行的流程提供生命周期管理,并为应用程序提供可靠,持久的访问,即使需要重新启动底层容器或实现本身也发生变化。 通过从容器中提取监视和维护服务运行状况的责任,您可以利用平台的工具来管理健康的工作负载。

为了让Kubernetes正确管理容器,必须了解容器内运行的应用程序是否健康且能够执行工作。 要启用此功能,容器可以实现活动探测:网络端点或可用于报告应用程序运行状况的命令。 Kubernetes将定期检查定义的活性探针,以确定容器是否按预期运行。 如果容器没有正确响应,Kubernetes将重新启动容器以尝试重新建立功能。

Kubernetes还提供了准备探针,类似的构造。 准备探针确定应用程序是否已准备好接收流量,而不是指示容器内的应用程序是否正常。 当容器化应用程序具有必须在准备好接收连接之前完成的初始化例程时,这非常有用。 Kubernetes使用准备探针来确定是否向服务添加pod或从服务中删除pod。

为这两种探测器类型定义端点可以帮助Kubernetes有效地管理容器,并可以防止容器生命周期问题影响服务可用性。 响应这些类型的健康请求的机制必须内置到应用程序本身中,并且必须在Docker映像配置中公开。

结论

在本指南中,我们已经介绍了一些重要的注意事项,请记住
在Kubernetes中运行容器化应用程序。 一些,重申一下
我们提出的建议是:

  • 使用最小的可共享父图像来构建具有最小膨胀的图像并减少启动时间
  • 使用多阶段构建来分隔容器构建和运行时环境
  • 结合Dockerfile指令创建干净的图像层并避免图像缓存错误
  • 通过隔离离散功能来实现容器化,从而实现灵活的扩展和管理
  • 设计吊舱具有单一,专注的责任
  • 捆绑帮助程序容器以增强主容器的功能或使其适应部署环境
  • 构建应用程序和容器以响应运行时配置,以便在部署时提供更大的灵活性
  • 将应用程序作为容器中的主要进程运行,以便Kubernetes可以管理生命周期事件
  • 在应用程序或容器中开发健康和活力端点,以便Kubernetes可以监控容器的健康状况

在整个开发和实施过程中,您需要做出可能影响服务稳健性和有效性的决策。 了解容器化应用程序与传统应用程序的不同之处,以及了解它们在托管集群环境中的运行方式可以帮助您避免一些常见的陷阱,并允许您利用Kubernetes提供的所有功能。