Kubernetes应用程序设计指南

Kubernetes应用程序设计指南

引言

设计和运行考虑到可伸缩性、可移植性和健壮性的应用程序可能具有挑战性,尤其是在系统复杂性增加的情况下。应用程序或系统的体系结构规定了它必须如何运行、它希望从其环境中得到什么,以及它与相关组件的耦合程度。在设计阶段遵循特定的模式并遵循特定的操作实践可以帮助应对应用程序在高度分布式环境中运行时面临的一些最常见的问题。

Docker和Kubernetes等技术帮助团队打包软件,然后在分布式计算机平台上分发、部署和扩展。了解如何最好地利用这些工具的功能可以帮助你以更高的灵活性、控制力和响应性来管理应用程序。

在本指南中,我们将讨论你可能想要采用的一些原则和模式,以帮助你扩展和管理Kubernetes上的工作负载。虽然Kubernetes可以运行许多类型的工作负载,但你的选择可能会影响操作的简易性和系统的可能性。

Kubernetes应用程序设计指南_第1张图片

针对应用程序可伸缩性进行设计

在开发软件时,许多需求会影响你选择采用的模式和体系结构。对于Kubernetes,最重要的因素之一是能够水平扩展,增加并行运行的应用程序的相同副本的数量,以分布负载和提高可用性。这是垂直扩展的替代方案,垂直扩展通常指增加单个应用程序堆栈的容量。

特别是,微服务是一种软件设计模式,可以很好地用于集群上的可伸缩部署。开发人员创建小型、可组合的应用程序,这些应用程序通过定义明确的API在网络上通信,而不是通过内部调用的单体复合程序。通过将单一应用程序重构为离散的单一用途组件,可以独立扩展每个功能。通常存在于应用程序级别的大部分复杂性和开销被转移平台上,在那里可以由Kubernetes等平台进行管理。

除了特定的软件模式之外,设计云原生应用程序时还考虑了一些额外的考虑因素。云本地应用程序通常遵循微服务架构模式,具有内置的弹性、可观察性和管理功能,以最大限度地利用云平台。

例如,使用运行状况报告指标构建云本地应用程序,以便在实例变得不健康时使平台能够管理应用程序生命周期。它们产生强大的监控数据,以提醒操作员注意问题,并使他们能够做出决定。定期重启和故障、后端程序出错以及高负载,应用程序不会无响应或是丢失数据。

遵循12要素应用原则

十二要素应用程序原则是一种流行的方法,可以帮助你专注于创建云原生Web应用程序时最重要的特征。这些原则最初是为了帮助开发人员和运营团队理解为在云上运行而设计的Web服务所共有的核心品质,现在非常适用于将在Kubernetes这样的集群环境中运行的软件。虽然单体应用程序可以从遵循这些建议中受益,但围绕这些原则设计的微服务架构效果更好。

以下是12个因素的简要总结:

  1. **代码库:**在代码版本管理系统(如 Git 或 Mercurial)中管理所有代码。代码库全面规定了部署的内容。
  2. **依赖项:**依赖项应该完全由代码库明确地管理,无论是供应商软件(与代码一起存储)还是在包管理器指定固定的版本。
  3. **配置:**将配置参数从应用程序中分离出来,并在部署环境中定义它们,而不是将它们放置到应用程序本身中。
  4. **后台服务:**本地和远程服务都被抽象为网络可访问资源,并在配置中设置连接详细信息。
  5. **构建、发布、运行:**你的应用程序的构建阶段应该与你的应用程序发布和运行过程完全分开。构建阶段从源代码创建部署制品,发布阶段结合制品和配置,运行阶段执行发布。
  6. **进程:**应用程序被实现为不应依赖本地存储状态的进程。应该将状态落到后台服务,如第四个因素中所述。
  7. **端口绑定:**应用程序应本机绑定到端口并侦听连接。路由和请求转发应该在外部处理。
  8. **并发性:**应用程序应该依赖于通过流程模型进行扩展。同时运行应用程序的多个副本,可能跨多个服务器,允许在不调整应用程序代码的情况下进行扩展。
  9. **可处置性:**进程应该能够快速启动并优雅地停止而不会产生严重的副作用。
  10. **开发/生产:**您的测试、试运行和生产环境应紧密匹配并保持同步。环境之间的差异可能造成不兼容和未经测试的配置。
  11. **日志:**应用程序应该将日志流式传输到标准输出,以便外部服务可以决定如何最好地处理它们。
  12. **管理流程:**一次性管理流程应针对特定版本运行,并与主流程代码一起提供。

管理版本控制系统(如Git或Mercurial)中的所有代码。依赖项:依赖项应该完全显式地由代码库管理,要么是提供的(与代码一起存储的),要么是以包管理器可以安装的格式固定的版本。配置:将配置参数从应用程序中分离出来,并在部署环境中定义它们,而不是将它们烘焙到应用程序本身中。后台服务:本地和远程服务都抽象为网络可访问的资源,并在配置中设置连接细节。构建、发布、运行:应用程序的构建阶段应该与应用程序的发布和操作过程完全分开。构建阶段从源代码创建部署构件,发布阶段组合构件和配置,运行阶段执行发布。过程:应用程序被实现为不应该依赖于本地存储状态的过程。应将状态卸载到支持服务,如第四个因素所述。端口绑定:应用程序应本地绑定到端口并侦听连接。路由和请求转发应该在外部处理。

并发性:应用程序应该依赖于通过流程模型进行扩展。同时运行应用程序的多个副本(可能跨多个服务器)允许在不调整应用程序代码的情况下进行扩展。可处置性:进程应该能够快速启动和优雅地停止,而不会产生严重的副作用。开发/生产奇偶校验:你的测试、试运行和生产环境应该紧密匹配,并保持同步。环境之间的差异是不兼容和未测试配置出现的机会。日志:应用程序应该将日志流到标准输出,以便外部服务可以决定如何最好地处理它们。管理进程:应该针对特定版本运行一次性管理进程,并与主进程代码一起提供。通过遵循十二要素提供的指导原则,你可以创建和运行非常适合Kubernetes的应用程序。十二个因素鼓励开发人员将重点放在应用程序的主要用途上,考虑组件之间的操作条件和接口,并使用输入、输出和标准流程管理功能在Kubernetes中可预测地运行。

通过遵循十二要素提供的指导原则,你可以创建和运行非常适合Kubernetes的应用程序。十二个因素鼓励开发人员将重点放在应用程序的主要用途上,考虑组件之间的接口,并使用输入、输出和标准流程管理功能在Kubernetes中可控地运行。

将应用程序组件容器化

Kubernetes使用容器在其集群节点上运行隔离的打包应用程序。要在Kubernetes上运行,你的应用程序必须封装在一个或多个容器镜像中,并使用Docker等容器运行时执行。尽管组件的容器化是Kubernetes的一项要求,但它也有助于强化上面讨论的十二要素应用程序方法中的许多原则,从而实现更好的伸缩性和管理。

例如,容器在应用程序环境和外部主机系统之间提供隔离。它们支持应用程序间通信的联网方式,通常通过环境变量进行配置,并公开写入stdoutstderr的日志。容器本身鼓励基于进程的并发性,并通过独立可伸缩的运行时环境来帮助维护开发/生产环境。这些特性使得打包你的应用程序成为可能,以便它们能够在Kubernetes上流畅地运行。

优化容器的指导方针

容器技术的灵活性允许封装应用程序的多种不同方式。但是,在 Kubernetes 环境中,某些方法比其他方法效果更好。

大多数关于应用程序容器的最佳实践都与映像构建有关,你可以在其中定义如何在容器中设置和运行你的软件。一般而言,保持镜像小而简单可以带来许多好处。优化大小的镜像可以通过在镜像更新之间重用现有层来减少在集群上启动新容器所需的时间和资源,这是Docker和其他容器框架设计为自动完成的。

创建容器映像的第一步是尽最大努力将构建步骤与将在生产中运行的最终映像分开。编译软件通常需要额外的工具,花费额外的时间,并产生可能在不同容器之间不一致或对最终运行时环境不必要的构件(例如,跨平台依赖)。将构建过程与运行时环境完全分开的一种方法是使用Docker多阶段构建。多阶段构建配置允许你指定一个基本映像以在构建过程中使用,并定义另一个以在运行时使用。这使得可以使用安装了所有构建工具的映像来构建软件,并将生成的构件复制到一个精简的映像中,以后每次都会使用该映像。

有了这种可用的功能,在最小的父映像上构建生产映像通常是一个好主意。如果你希望完全避免ubuntu:20.04(包含完整的Ubuntu 20.04服务器环境)之类的发行版风格父层的臃肿,你可以使用Docker最小的基础镜像scratch作为父镜像来构建你的镜像。然而,scratch基本层并不提供对许多核心工具,并且经常会打破关于Linux环境的一些基线。作为另一种选择,Alpine Linux alpine镜像已经变得流行起来,因为它是一个坚实的、最小的基础环境,提供了一个很小但功能齐全的Linux发行版。

对于像Python或Ruby这样的解释型语言,原则稍有变化,因为没有编译阶段,而且解释器必须可用于在生产环境中运行代码。然而,由于超薄镜像仍然是理想的,许多特定语言构建在Alpine Linux之上的优化镜像在Docker Hub上可用。对解释语言使用较小镜像的好处与对编译语言使用较小镜像的好处相似:Kubernetes将能够快速将所有必要的容器镜像拖到新节点上,以开始进行有意义的工作。

确定容器和 Pod 的范围

虽然你的应用程序必须被容器化才能在Kubernetes集群上运行,但Pod是Kubernetes可以直接管理的最小抽象单元。Pod是由一个或多个紧密耦合的容器组成的Kubernetes对象。Pod中的容器共享一个生命周期,并作为一个单元一起管理。例如,容器始终部署在同一节点(服务器)上,同时启动或停止,并共享文件系统和IP寻址等资源。

了解Kubernetes如何处理这些组件以及每个抽象层为你的系统提供了什么,这一点很重要。一些注意事项可以帮助您使用这些抽象中的每一个来确定应用程序的一些自然封装点。

确定容器的有效范围的一种方法是寻找自然开发边界。如果你的系统使用微服务体系结构运行,则通常会构建设计良好的容器来表示分散的功能单元,这些功能单元通常可以在各种上下文中使用。这种抽象级别允许你的团队发布对容器映像的更改,然后将此新功能部署到使用这些映像的任何环境中。应用程序可以通过组成单独的容器来构建,每个容器都可以完成给定的功能,但可能不会单独完成整个过程。

与上面的情况不同,Pod通常是通过考虑系统的哪些部分可能从独立管理中获益最多来构建的。由于Kubernetes使用Pod作为其最小的面向用户的抽象,因此这些是Kubernetes工具和API可以直接交互和控制的最原始单元。你可以启动、停止和重新启动Pod,或者使用构建在Pod上的更高级别的对象来引入复制和生命周期管理功能。Kubernetes不允许你独立管理Pod中的容器,因此你不应该将单独管理的容器组合在一起。

因为Kubernetes的许多特性和抽象直接涉及Pod,所以将应该在单个Pod中一起扩展的项捆绑在一起,并将应该独立扩展的项分开。例如,将Web服务器与应用程序服务器分离在不同的Pod中,可以根据需要独立扩展每一层。但是,如果数据库提供Web服务器正常工作所需的基本功能,则将Web服务器和数据库捆绑到同一个Pod中可能是正确的选择。

通过捆绑支持容器增强Pod功能

考虑到这一点,应该将哪些类型的容器捆绑在一个 pod 中?通常,主容器负责完成 pod 的核心功能,但可以定义附加容器来修改或扩展主容器或帮助其连接到独特的部署环境。

例如,在 Web 服务器 pod 中,Nginx 容器可能会侦听请求并提供内容,而关联的容器会在存储库更改时更新静态文件。将这两个组件打包在一个容器中可能很诱人,但是将它们实现为单独的容器有很大的好处。Web 服务器容器和存储库拉取程序都可以在不同的上下文中独立使用。它们可以由不同的团队维护,并且每个都可以被开发以概括他们的行为以与不同的配套容器一起工作。

Brendan Burns 和 David Oppenheimer 在他们关于基于容器的分布式系统的设计模式的论文中确定了捆绑支持容器的三种主要模式。这些代表了将容器打包到一个 pod 中的一些最常见的用例:

  • **Sidecar:**在这种模式中,辅助容器扩展并增强了主容器的核心功能。此模式涉及在单独的容器中执行非标准或实用功能。例如,转发日志或监视更新配置值的容器可以在不改变其主要关注点的情况下增强 pod 的功能。
  • **大使:**大使模式使用补充容器为主容器抽象远程资源。主容器直接连接到大使容器,大使容器又连接并抽象出潜在复杂的外部资源池,例如分布式 Redis 集群。主容器不必知道或关心连接到外部服务的实际部署环境。
  • **适配器:**适配器模式用于转换主容器的数据、协议或接口,以符合外部各方期望的标准。适配器容器支持对集中式服务的统一访问,即使它们所服务的应用程序可能仅原生支持不兼容的接口。

将配置提取到ConfigMaps和Secrets中

虽然可以将应用程序配置放置到容器映像中,但最好使你的组件在运行时可配置,以支持在多个上下文中的部署,并允许更灵活的管理。为了管理运行时配置参数,Kubernetes提供了两种不同类型的对象,称为ConfigMaps和Secrets。

ConfigMap是一种用于存储数据的机制,这些数据可以在运行时公开给Pod和其他对象。存储在ConfigMaps中的数据可以作为环境变量呈现,也可以作为Pod中的文件装载。通过将应用程序设计为从这些位置读取,你可以在运行时使用ConfigMaps注入配置并修改组件的行为,而不必重新构建容器映像。

Secret是一种类似的Kubernetes对象类型,用于安全地存储敏感数据,并根据需要选择性地允许Pod和其他组件访问它。Secret是一种将敏感信息传递给应用程序的便捷方式,而无需将它们以纯文本形式存储在正常配置中易于访问的位置。在功能上,它们的工作方式与ConfigMaps基本相同,因此应用程序可以使用相同的机制使用ConfigMaps和Secrets中的数据。

ConfigMaps和Secrets帮助你避免将配置参数直接放入Kubernetes对象定义中。你可以映射配置键而不是值,从而允许你通过修改ConfigMap或Secret来动态更新配置。这使你有机会更改Pod和其他Kubernetes对象的活动运行时行为,而无需修改资源的Kubernetes定义。

实现就绪和健康探测

Kubernetes包括大量开箱即用的功能,用于管理组件生命周期并确保你的应用程序始终健康且可用。然而,要利用这些功能,Kubernetes必须了解它应该如何监视和解释你的应用程序的运行状况。为此,Kubernetes允许你定义活跃度和就绪探测。

活跃度探测允许Kubernetes确定容器中的应用程序是否处于活动状态并且正在运行。Kubernetes可以在容器中定期运行命令来检查基本的应用程序行为,或者可以将HTTP或TCP网络请求发送到指定位置,以确定进程是否可用并能够按预期做出响应。如果活性探测失败,Kubernetes会重启容器以尝试在Pod内重新建立功能。

就绪探头是用于确定Pod是否已准备好为流量提供服务的类似工具。容器中的应用程序可能需要在它们准备好接受客户端请求之前执行初始化过程,或者它们可能需要在配置更改后重新加载。当就绪探测失败时,Kubernetes将暂时停止向实例发送请求,而不是重启容器。这允许Pod完成其初始化或维护例程,而不会影响整个组的健康。

通过健康探测和就绪探测相结合,你可以指示Kubernetes自动重启实例或将其从后端组中移除。通过配置你的基础架构利用这些功能,Kubernetes无需额外的操作工作即可管理应用程序的可用性和运行状况。

使用Deployments管理规模和可用性

早些时候,在讨论一些Pod设计基础时,我们提到了其他Kubernetes对象构建在这些原语之上,以提供更高级的功能。Deployments,一个这样的复合对象,可能是最常见的定义和操作的Kubernetes对象。

Deployments是构建在其他Kubernetes原语之上以添加附加功能的复合对象。它们向称为ReplicaSet的中间对象添加了生命周期管理功能,如执行滚动更新、回滚到较早版本以及在状态之间转换的功能。这些ReplicaSet允许你定义Pod模板,以启动和管理单个Pod设计的多个副本。这有助于你轻松扩展基础设施、管理可用性要求,并在出现故障时自动重启Pod。

这些附加功能为基础Pod层提供了管理框架和自我修复功能。虽然Pod是最终运行你定义的工作负载的单元,但它们不是你通常应该配置和管理的单元。相反,可以将Pod视为一个构建块,当通过Deployments等更高级别的对象进行配置时,它可以健壮地运行应用程序。

创建服务和入口规则以管理对应用层的访问

Deployments允许你调配和管理多组可互换Pod,以扩展你的应用程序并满足用户需求。但是,将流量路由到配置的Pod是另一个需要考虑的问题。当Pod作为滚动更新的一部分被换出、重新启动或由于主机故障而被移动时,以前与运行组相关联的网络地址将改变。Kubernetes服务允许你通过维护动态Pod池的路由信息并控制对基础设施各个层的访问来管理这种复杂性。

在Kubernetes中,服务是控制如何将流量路由到Pod集的特定机制。无论是转发来自外部客户端的流量,还是管理几个内部组件之间的连接,服务都允许你控制流量的流动方式。然后,Kubernetes将更新和维护将连接转发到相关Pod所需的所有信息,即使环境发生变化和网络寻址发生变化也是如此。

访问内部服务

要有效地使用服务,你必须首先确定每组Pod的目标消费者。如果你的服务仅供Kubernetes集群内部署的其他应用使用,则ClusterIP服务类型允许你使用稳定的IP地址连接到一组实例,该IP地址只能从集群内进行路由。部署在集群上的任何对象都可以通过将流量直接发送到服务的IP地址来与复制的Pod组进行通信。这是最直接的服务类型,适用于内部应用程序层。

可选的DNS插件使Kubernetes能够为服务提供DNS名称。这允许Pod和其他对象通过名称而不是IP地址与服务通信。这种机制不会显著改变服务的使用情况,但基于名称的标识符可使组件或定义交互变得更简单,而不必知道服务IP地址。

面向公众提供的公开服务

如果接口应该是公开访问的,则你的最佳选择通常是负载均衡器服务类型。这使用你特定的云提供商的API来提供负载均衡器,该均衡器通过公开的IP地址向服务Pod提供流量。这允许你将外部请求路由到你的服务中的Pod,为你的内部集群网络提供受控的网络通道。

由于负载均衡器服务类型为每个服务创建一个负载均衡器,因此使用此方法公开Kubernetes服务可能会变得非常昂贵。为了帮助缓解这一问题,可以使用Kubernetes ingress对象来描述如何基于一组预先确定的规则将不同类型的请求路由到不同的服务。例如,对“a.example.com”的请求可能会发送到服务A,而对“b.example.com”的请求可能会被路由到服务B。Ingress对象提供了一种描述如何根据预定义模式将混合请求流逻辑地路由到其目标服务的方法。

入口规则必须由入口控制器解释——通常是某种负载平衡,如 Nginx——作为 pod 部署在集群中,它实现入口规则并将流量相应地转发到 Kubernetes 服务。Ingress 实现可用于最大限度地减少集群所有者需要运行的外部负载均衡器的数量。

Ingress规则必须由在群集中部署的入口控制器(通常是某种类型的负载平衡,如Nginx)解释为Pod,该控制器实施入口规则并相应地将流量转发到Kubernetes服务。入口实现可用于最大限度地减少集群所有者需要运行的外部负载均衡器的数量。

使用声明性语法管理Kubernetes状态

Kubernetes在定义和控制部署到集群的资源方面提供了相当大的灵活性。使用kubectl这样的工具,你可以定义特定对象以立即部署到你的集群。虽然这对于在学习Kubernetes时快速部署资源很有用,但这种方法存在缺陷,不适合长期的生产管理。

命令式管理的主要问题之一是,它不会为你部署到集群的更改留下任何记录。这使得很难或不可能在发生故障时进行恢复,或者跟踪应用到你的系统中的操作更改。

幸运的是,Kubernetes 提供了另一种声明性语法,允许您在文本文件中完全定义资源,然后使用它kubectl来应用配置或更改。将这些配置文件存储在版本控制存储库中是监视更改并与用于组织其他部分的审核流程集成的好方法。基于文件的管理还可以通过复制和编辑现有定义来使现有模式适应新资源。将 Kubernetes 对象定义存储在版本化目录中,您可以在每个时间点维护所需集群状态的快照。这在恢复操作、迁移或追踪引入系统的意外更改的根本原因时非常有用。

结论

管理将运行你的应用程序的基础设施并学习如何最好地利用现代编排环境提供的功能可能会令人望而生畏。然而,当你的开发和操作实践与工具所围绕的概念保持一致时,Kubernetes之类的系统和容器之类的技术提供的许多好处就会变得更加明显。使用Kubernetes擅长的模式构建你的系统,并了解某些功能如何缓解复杂部署的挑战,可以改善你在平台上运行的体验。

你可能感兴趣的