为 Kubernetes 构建优化的容器

简介

容器镜像是在Kubernetes中定义应用程序的主要打包格式。图片是Pod和其他对象的基础,在有效利用Kubernetes的功能方面发挥着重要作用。设计良好的图像是安全的、高性能的和聚焦的。他们能够对Kubernetes提供的配置数据或指令做出反应。它们实现部署用来了解其内部应用程序状态的端点。

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

高效容器镜像的特点

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

一些素质的目标是:

单一、明确的目的

集装箱图像应该有一个单独的离散焦点。避免将容器映像视为虚拟机,因为将相关功能打包在一起可能是有意义的。相反,应该像对待Unix实用程序一样对待您的容器镜像,严格专注于做好一件小事。应用程序可以在单个容器范围之外进行协调,以提供更复杂的功能。

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

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

图片尺寸较小

在Kubernetes这样的集群环境中,较小的映像有很多好处。它们可以快速下载到新节点,并且通常安装的软件包较少,这可以提高安全性。压缩的容器镜像通过最大限度地减少所涉及的软件数量,使调试问题变得更简单。

外部管理状态

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

简单易懂

尽量使容器图像简单易懂是很重要的。在故障排除时,能够直接查看配置和日志,或测试容器行为,可以帮助您更快地解决问题。考虑将容器图像作为应用程序的打包格式,而不是机器配置,可以帮助您实现正确的平衡。

遵循集装箱化软件最佳实践

图像的目标应该是在容器模型内工作,而不是对其采取行动。避免实施传统的系统管理实践,例如包含完整的init系统和守护应用程序。登录到stdout,这样Kubernetes就可以向管理员公开数据,而不是使用内部日志守护程序。这些建议与完整操作系统的最佳实践大相径庭。

充分利用Kubernetes功能

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

既然我们已经确定了定义高功能容器图像的一些品质,我们就可以更深入地研究帮助您实现这些目标的策略。

重用最小共享底层

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

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

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

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

Alpine Linux是一个流行的基础映像替代品,因为它成功地将大量功能打包到一个非常小的基础映像(~ 5 MB)中。 它包括一个包管理器和一个相当大的存储库,并具有您期望从最小的Linux环境中获得的大多数标准实用程序。

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

管理容器层

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

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

了解镜像层和构建缓存

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

此过程可以显著缩短构建时间,但重要的是要了解用于避免潜在问题的机制。对于COPYADD这样的文件复制指令,Docker会比较文件的校验和,以确定是否需要重新执行操作。对于RUN指令,Docker检查是否已为该特定命令字符串缓存了一个图像层。

虽然这可能不是一目了然,但如果您不小心,此行为可能会导致意外的结果。更新本地程序包索引和安装程序包分两步进行,这是一个常见的示例。我们将在本例中使用Ubuntu,但基本前提同样适用于其他发行版的基本映像:

1[label Package installation example Dockerfile]
2FROM ubuntu:20.04
3RUN apt -y update
4RUN apt -y install nginx
5. . .

在这里,本地包索引在一个RUN指令(apt-y update)中更新,Nginx在另一个操作中安装。当它第一次使用时,它可以正常工作。但是,如果稍后更新Dockerfile以安装其他程序包,则可能会出现问题:

1[label Package installation example Dockerfile]
2FROM ubuntu:20.04
3RUN apt -y update
4RUN apt -y install nginx php-fpm
5. . .

我们已经将第二个包添加到由第二条指令运行的安装命令中。如果距离上一次映像构建已经过去了很长时间,则新的构建可能会失败。这是因为包索引更新指令(Run apt-y update)有_NOT_CHANGE,所以Docker重用了该指令关联的图像层。由于我们使用的是旧的程序包索引,我们本地记录中的php-fpm程序包版本可能不再位于存储库中,从而导致在运行第二条指令时出错。

为了避免这种情况,请确保将所有相互依赖的步骤合并到一个RUN指令中,以便Docker在发生变化时重新执行所有必要的命令。在外壳脚本中,使用_LOGICAL AND_OPERATOR&&可以很好地实现这一点,它将在同一行上执行多个命令,只要每个命令都成功:

1[label Package installation example Dockerfile]
2FROM ubuntu:20.04
3RUN apt -y update && apt -y install nginx php-fpm
4. . .

现在,每当程序包列表更改时,该指令都会更新本地程序包缓存。另一种方法是运行包含多行指令的整个shell.sh脚本,但必须首先使其可用于容器。

调整运行指令缩小图像层大小

前面的例子演示了Docker的缓存行为是如何颠覆预期的,但是关于RUN指令如何与Docker的分层系统交互,还有一些其他的事情需要记住。 如前所述,在每个RUN指令的末尾,Docker会将更改作为额外的图像层提交。 为了控制生成的图像层的范围,您可以通过注意运行的命令引入的伪影来清理不必要的文件。

一般而言,将命令链接在一起形成单一的‘RUN’指令提供了对将要写入的层的极大控制。对于每个命令,您可以设置层的状态(apt-y upate),执行core命令(apt install-y nginx php-fpm),并在提交之前移除任何不必要的Artisire来清理环境。例如,很多Dockerfile会将rm-rf/var/lib/apt/list/* 链接到apt命令的末尾,去掉下载的包索引,以减小最终的层大小:

1[label Package installation example Dockerfile]
2FROM ubuntu:20.04
3RUN apt -y update && apt -y install nginx php-fpm && rm -rf /var/lib/apt/lists/*
4. . .

为了进一步减小您正在创建的图像层的大小,尝试限制您正在运行的命令的其他意外副作用会很有帮助。例如,除了显式声明的包外,apt还默认安装推荐包。您可以在您的apt命令中添加--no-install-suggess来移除此行为。您可能需要进行实验,以确定您是否依赖于推荐的包提供的任何功能。

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

使用多阶段构建

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

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

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

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

容器级和实例级功能范围划分

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

按函数集装化

通常,将每个独立的功能打包到单独的容器映像中是一种良好的做法。

这与在虚拟机环境中使用的常见策略不同,在虚拟机环境中,应用程序经常组合在同一映像中,以减小大小并最大限度地减少运行VM所需的资源。由于容器是轻量级抽象,不会对整个操作系统堆栈进行虚拟化,因此这种权衡对Kubernetes来说不那么有说服力。因此,虽然Web堆栈虚拟机可能会在一台机器上捆绑一台Nginx Web服务器和一台Gunicorn应用服务器来为Django应用程序提供服务,但在Kubernetes中,这些服务器可能会被拆分到单独的容器中。

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

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

组合实例中的容器图片

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

与容器镜像一样,当太多功能被捆绑到单个实体中时,Pod也会变得不那么灵活。Pod本身可以使用其他抽象进行扩展,但其中的容器不能独立管理或扩展。因此,为了继续使用前面的示例,可能不应该将单独的nginx和Gunicorn容器捆绑在一起放在一个Pod中。这样,就可以单独控制和部署它们。

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

  • Sidecar :辅助容器作为辅助工具,扩展了主容器的核心功能。例如,当远程存储库发生更改时,SideCar容器可能会转发日志或更新文件系统。主容器仍然专注于其核心责任,但通过侧车提供的功能进行了增强。
  • 大使 :大使容器负责发现和连接(通常是复杂的)外部资源。主容器可以使用内部Pod环境连接到已知接口上的大使容器。大使提取后端资源并代理主容器和资源池之间的流量。
  • 适配器 :适配器容器负责规范主容器的接口、数据和协议,以与其他组件期望的属性保持一致。主容器可以使用本机格式操作,适配器容器转换和标准化数据以与外部世界通信。

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

运行时配置设计

在构建标准化、可重用组件的愿望和使应用程序适应其运行时环境所涉及的需求之间存在一些矛盾。_运行时配置_是弥合这些问题之间差距的最佳方法之一。通过这种方式,组件被构建为通用的,并通过提供额外的配置详细信息在运行时概述其所需的行为。这种标准方法既适用于容器,也适用于应用程序。

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

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

由于Kubernetes操作的是像Pod这样的更高级别的对象,而不是直接管理容器,因此有一些机制可以定义配置并在运行时将其注入容器环境。KubernetesConfigMaps 和** Secrets** 允许您单独定义配置数据,然后在运行时将这些值投影到容器环境中。ConfigMap是通用对象,旨在存储可能因环境、测试阶段等而异的配置数据。机密提供类似的接口,但专门为敏感数据设计,如帐户密码或API凭据。

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

使用容器实现进程管理

当过渡到基于容器的环境时,用户通常会将现有的工作负载转移到新系统,很少或根本没有变化。 它们通过在新的抽象中包装它们已经使用的工具来将应用程序打包在容器中。 虽然使用通常的模式来启动和运行迁移后的应用程序很有帮助,但在容器中丢弃以前的实现有时会导致无效的设计。

把容器当作应用,而不是服务

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

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

大多数情况下,最好简化容器内的运行环境,以便PID1在前台运行主应用程序。在必须运行多个进程的情况下,PID1负责管理后续进程的生命周期。某些应用程序,如阿帕奇,通过派生和管理处理连接的工作进程来本机处理这一问题。对于其他应用程序,可以使用包装器脚本或非常精简的初始化系统,如dumb-init或附带的TINI初始化系统)。无论您选择哪种实现方式,容器内以PID1身份运行的进程都应该适当地响应Kubernetes发送的TERM信号,以达到预期的效果。

Kubernetes容器健康管理

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

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

Kubernetes还提供了就绪探测器,这是一个类似的结构。就绪探测确定应用程序是否准备好接收流量,而不是指示容器中的应用程序是否健康。当容器化应用程序的初始化例程必须在准备好接收连接之前完成时,这会很有用。Kubernetes使用就绪探测器来确定是向服务添加Pod还是从服务中删除Pod。

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

结论

在本指南中,我们介绍了在以下情况下需要记住的一些重要注意事项 在Kubernetes中运行容器化应用程序。要重申的是,一些 我们看过的建议是:

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

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

Published At
Categories with 技术
comments powered by Disqus