[关闭]
@levinzhang 2022-09-12T21:36:16.000000Z 字数 6275 阅读 445

在Kubernetes上设计和部署可扩展应用的15条原则

摘要

借助Kubernetes,我们可以体验云原生应用开发和部署所带来的收益,它能够帮助我们编写可扩展、韧性和容错的应用。但Kubernetes是一个复杂的系统,如果使用不当也会带来难以解决的问题,本文总结了在Kubernetes中设计和部署应用的原则,能够指导我们开发更稳定和更安全的云原生应用。


本文最初发表于Elastisys的技术博客,经原作者Lars Larsson授权由InfoQ中文站翻译分享。

设计可扩展的云原生应用需要深思熟虑,因为我们需要克服很多的挑战。即便我们现在有了伟大云产品来部署应用,但著名的分布式计算谬误依然存在。的确,网络会造成延迟和错误。云原生应用通常是微服务,必须进行专门的设计部署,以克服这些挑战。

为了帮助解决这些问题,我们有一个针对Kubernetes的庞大生态系统,包含了大量的优秀软件。从传统分布式系统的角度来看,Kubernetes并不是一个“中间件”,但是它的确提供了一个平台,能够让这些优秀的软件组件帮助我们编写有韧性、高性能且设计良好的软件。通过有意识地设计软件来利用这些特性,并且按照相同的方式部署软件,我们就能创建出真正以云原生方式扩展的软件。

在本文中,我将会展示在Kubernetes上设计和部署云原生应用的15条原则。为了达到最好的效果,你还应该阅读其他的三篇文章。第一篇关于如何设计通用可扩展应用的,即可扩展性设计原则。另外两篇分别是关于如何部署应用以及它们之间如何以云原生方式进行协作的,分别是12-Factor应用宣言和以及研究论文“基于容器的分布式系统的设计原则”。不要介意它们是几年前的内容,它们都经历了时间的考验,依然极具价值。

导论:到底什么是云原生应用?

云原生计算基金会对“云原生”的含义有一个正式的定义。其主要内容如下:

这些技术能够构建具备韧性、易于管理和便于观测的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。

如果将上述内容翻译成可行的具体特征和属性,那么云原生软件需要满足如下条件:

另外,云原生软件主要是围绕着微服务架构构建的,在这种架构中,组件会处理明确定义的任务。这种方式使得组件的扩展和运维变得非常容易,其带来的影响之一就是组件大致上被分为有状态和无状态的。大规模架构中的主要组件都是无状态的,并且会依赖几个数据存储来管理应用的状态。

在Kubernetes上设计和部署可扩展应用的原则

Kubernetes使得部署和运维应用变得更容易。基于给定的容器镜像,我们只需一条命令就能部署,即便要部署多个实例也可以实现(kubectl create deployment nginx –image=nginx –replicas=3)。但是,这种简便性是一把双刃剑。因为这种方式部署的应用无法充分利用Kubernetes的高级特性,因此平台本身也没有达到最优的效果。

原则1:单个Pod基本上没有任何用武之地

因为Kubernetes可能随时终止Pod,所以我们几乎总要使用某个控制器(Controller)来管理Pod。除了一次性的调试之外,单个Pod几乎没有任何用武之地。

ReplicaSet也很少会直接使用。

相反,我们应该使用DeploymentStatefulSet 来处理Pod。不管我们是不是要运行一个以上的实例,均是如此。我们想要进行自动化的原因在于,Kubernetes不会保证Pod持续的生命周期,以防Pod中的容器出现故障。实际上,它明确表明,Pod随时会被终止

原则2:清晰划分有状态和无状态的组件

Kubernetes定义了很多不同的资源以及管理它们的控制器。每种资源都有自己的语义。我曾经见过有人困惑于Deployment、StatefulSet和DaemonSet分别是什么,以及它们能够做什么,不能做什么。要选择正确方案就意味着你需要清晰地表达自己的意图,Kubernetes将会帮助你实现目标。

如果使用得当的话,Kubernetes会强制我们按照它的规则行事,但是现在存在很多错综复杂的解决方案。最简单的规则就是将有状态的服务放到StatefulSet中,将无状态的服务放到Deployment中,因为这就是Kubernetes的方式。但是,需要记住,还有其他的Kubernetes控制器,请了解它们各自的特点和差异。

原则3:区分secret配置与非secret配置,确保清晰和安全性

ConfigMapSecret的技术差异很小。在Kubernetes内部的表示方式以及使用方式都很相似。但是,正确地使用它们能够表达我们的意图,另外,如果使用基于角色的访问控制,也更易于操作。比如,应用配置存储到ConfigMap中,而带有凭证的数据库连接字符串则属于Secret,这样会更加清晰。

原则4:启用自动扩展来实现容量管理

就像所有的Pod都是由Deployment管理,并且其前端都要使用Service一样,我们还需要为Deploymen考虑使用Horizontal Pod Autoscaler(HPA)

没有人希望在生产环境出现容量耗尽的情况。类似的,也没有人希望终端用户因为Pod的容量分配不当而受到影响。我们从一开始就要考虑好这个问题,这意味着我们必须要认识到,扩展是必然发生的事情。这比容量耗尽要好得多。

根据通用的可扩展性原则,我们需要为运行每个应用组件的多个实例做好准备。对于可用性和可扩展性,这至关重要。

需要注意,借助HPA,我们也可以很容易地扩展StatefulSet。但是,一般来讲,有状态组件仅在绝对必要的时候才会进行扩展。

例如,扩展数据库会导致大量的数据复制和额外的事务管理,如果数据库已经处于高负载的状态,这绝对不是我们想要的行为。如果你确实要对有状态组件进行自动扩展,请考虑禁用自动收缩功能。如果有状态组件需要与其他实例以某种方式进行同步时,更要如此。手动触发这种行为是更为安全的。

原则5:通过为容器生命周期管理添加钩子来增强和实现自动化

容器可以定义PostStart和PreStop钩子(hook),它们都可以用来执行重要的工作,通知应用的其他组件即将创建新的实例或终止现有实例。PreStop钩子会在Pod终止前调用,并且有一个(可配置的)完成时间。使用它能够确保即将终止的实例能够完成它的任务,将文件提交至Persistent Volume,或者其他需要完成的事项,以实现有序和自动的关闭。

原则6:正确使用探针实现故障探查以及自动恢复

与单进程系统相比,分布式系统的故障不那么直观。网络连接问题造成了一大类新的故障。我们探测故障的能力越强,就越有机会从故障中自动恢复。

因此,Kubernetes为我们提供了探针功能。其中,就绪性探针(readiness probe)特别有用,因为发送给Kubernetes的失败信号能够表明我们的容器(也就是Pod)还没有为接收请求做好准备。

尽管有明确的文档,但存活性探针(liveness probe)经常会被误解。存活性探针的失败表明组件已经永久陷入糟糕的境地,需要通过强行重启来解决。这并不能说明它处于“活跃”状态(正如就绪性探针所指示的那样),因为在分布式系统中,存活性实际上有其他的意义。

Kubernetes添加了启动探针(startup probe),表示何时能够使用其他的探针进行探测。因此,这是一种延迟其他探针执行的方式。

原则7:让组件快速、严重地失败,并使其众所周知

如果应用组件失败的话,请确保它足够严重(崩溃)、快速(出现问题时马上失败),并且能够众所周知(在日志中包含翔实的信息)。这种方式能够避免应用中的数据处于一种奇怪的状态,将流量路由至健康的实例,并且能够为问题原因分析提供所需的全部信息。本文中提到的所有自动化原则都能帮助我们使应用处于良好的状态,同时又能找到问题发生的根本原因。

应用中的组件必须能够处理重启。故障迟早都会发生,不管是组件还是集群,均是如此。因为故障难以避免,所以我们必须要有能力处理它们。

原则8:让应用为可观测性做好准备

监控、日志和跟踪是可观测性的三大支柱。将自定义指标提供给监控系统(如Prometheus)、编写结构化的日志(如JSON格式)并有意识地保留HTTP头信息(比如包含correlation ID的头信息)并将其作为日志的一部分,这些措施都能为应用提供必要的可观测性。

如果你想要获取更详细的跟踪信息,那么可以将应用与Open Telemetry API集成。但是,不管是对于人类运维人员还是自动化,前文所述的操作足以让应用易于观测了。基于对应用有意义的指标进行自动扩展,始终要比使用原始指标(如CPU使用率)更好一些。

站点可靠性工程的“四个黄金信号”是延迟、流量、错误和饱和度。用应用特定的指标来跟踪这些信号,比通用的资源消耗测量手段获得的原始指标要有用得多。

原则9:恰当地设置Pod的资源请求和限制

通过恰当地设置Pod的资源请求和限制Horizontal Pod AutoscalerCluster Autoscaler都能更好地完成自己的任务。如果它们知道需要多少容量以及有多少可用的容量,那么当它们确定Pod和整个集群需要多少容量的时候就容易多了。

不要将资源请求和限制设置地太低。这种方式起初可能会比较诱人,因为这样能够在集群中运行更多的容器。但是,除非请求和限制设置的值是相等的(即“Guaranteed” QoS类的Pod),否则Pod在正常(常规流量)操作中可能会被赋予更多的资源。这是因为,它们可能会给予一些宽裕的资源。因此,起初一切都能很好地运行。但是在高峰期,它们会被限制到我们声明的容量。当然,此时发生这样的事情是最糟糕的时间点,此时进行扩展我们可能会得到性能更差的Pod。虽然是无意的,但这可能确实是我们要求调度器所做的事情。

原则10:保留容量并设置Pod的优先级

在容量管理方面,命名空间资源配额、在节点上预留计算资源以及设置Pod的优先级有助于确保集群容量和可扩展性免受影响。

我曾经亲眼看到过一个不堪重负的集群,以至于网络插件的Pod都会移除掉了。对于故障排查来说,这是一件很糟糕的事情(但是却很有教育意义)。

原则11:根据需要将Pod放到一起或分散部署

在表达要将Pod放到一起(为了高效的网络流量)还是跨云region与可用区分散部署(为了冗余)方面,Pod的拓扑传播限制以及亲和与反亲和规则是一种很好的方式。

原则12:确保Pod在可能导致停机的运维操作中的可用性

Pod干扰预算(Pod Disruption Budget)声明了在一组Pod中(如一个Deployment中的Pod),允许每次有多少进行自愿干扰(voluntarily disrupted,也就是由我们的命令触发的,而不是故障)。即便是在集群节点被管理员排空(drain)时,这也能够确保高可用性。这种情况可能发生在集群升级的时候,通常这种行为都是按月进行的,因为Kubernetes的演进非常快。

需要注意,如果没有正确设置Pod干扰预算,那我们可能会限制管理员进行升级的能力。这方面很容易出现错误配置,从而阻碍排空节点,这会影响自动化操作系统补丁升级,并损害环境的安全性。

如果你能为应用设计如何处理干扰,并且在无法实现的时候通过PDB(PodDisruptionBudget)让Kubernetes来提供帮助,那么这是非常不错的。

原则13:选择蓝/绿或金丝雀部署,而不是全停机方式的部署

在现在这个时代,为了进行维护,而将整个应用关闭是难以让人接受的。目前,这种方式被称为“全停机(stop-the-world )部署”,在这个过程中,应用会在一段时间内无法访问。通过更复杂的部署策略,可以实现更平稳和渐进式的变更。终端用户根本不需要感知应用已经发生了变化。

蓝/绿金丝雀部署曾经是一种黑科技,但Kubernetes让所有人都能使用它们。我们可以上线组件的新版本,并在Service中通过标签和选择器将流量路由到它们上面,这一切使得即便脚本中存在或多或少的手动操作也能实现这种高级的部署策略,当然也能通过更好的部署工具来实现,如ArgoCD(蓝/绿金丝雀部署)

请注意,大多数的部署策略都可以在技术层面上归结为同时部署相同组价的两个版本,并按照不同的方式将请求转发给它们。我们可以通过Service本身做到这一点,比如为新版本的Pod打上特定的标签,以便于Service将流量路由到它们。即将推出的Kubernetes Gateway资源将会提供开箱即用的特性(但是Ingress并没有该特性)。

原则14:避免赋予Pod不需要的权限

Kubernetes本身并不安全,其默认就是如此。但是,我们可以对其进行配置,以强化安全的最佳实践,比如限制容器在节点上都能做些什么。

以非root用户运行容器。在Docker中构建容器镜像时,容器默认是以root身份来运行的,这恐怕是近十年来黑客们最兴奋的事情。在容器构建的过程中,只使用root用户来安装依赖,然后切换至非root用户,并使用该用户运行应用。

如果应用确实需要提升权限,我们依然要使用非root用户,为其移除所有的Linux功能,然后将最小的功能集添加回来。

在2022年1月份,一个存在3年之久的容器漏洞被披露出来(CVE-2022-0185)。如果容器没有所需的Linux功能会怎样呢?那将完全无法进行攻击。

原则15:限制Pod在集群中的行为

禁用默认的服务账号,防止将其暴露到应用中。除非明确需要与Kubernetes API进行交互,否则不要将默认的服务账号token放入应用中。但是,这是Kubernetes中的默认做法。

尽可能设置最安全的Pod安全策略Pod安全标准,确保在默认情况下,避免出现不安全的操作。

使用网络策略来限制哪些Pod可以访问你的Pod。Kubernetes内默认的自由网络流量是一个安全噩梦,因为在这种情况下,攻击者只要进入一个Pod,就可以直接访问其他所有的Pod。

CVSS评分为10分的Log4J漏洞(CVE-2021-44228)被幽默地称为Log4Shell,它对于设置锁定网络策略的容器完全无效,除了允许列表中内容之外,所有的出站流量都会被禁用(漏洞中的模拟LDAP服务器并没有设置策略)。

总结

本文介绍了设计云原生应用并将其部署在Kubernetes上的15条原则。通过遵循这些原则,云原生应用可以很好地与Kubernetes工作负载编排器协同工作。这样,我们可以获得Kubernetes平台以及按照云原生方式设计和运维软件提供的所有收益。

你已经学会了如何正确地使用Kubernetes资源,为自动化做好准备,如何处理故障,利用Kubernetes探针功能来提高稳定性,为应用程序的可观测性做好准备,让Kubernetes调度器按照我们希望的方式运行,利用高级策略进行部署,以及如何限制应用程序的攻击面。

请将所有的这些因素都纳入软件架构的工作中,使你的日常DevOps流程更加顺畅、更加可靠。可以说,要使这种顺畅几乎达到无聊的程度。这是非常好的事情,因为软件的顺利部署和管理意味着一切都在按计划进行。正如常言所说,“没有消息就是好消息”。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注