原文由Elastisys 团队在Elastisys的博客上发表的客座文章
设计可扩展的云原生应用需要大量的思考,因为有许多挑战需要克服。即使我们今天拥有伟大的云来部署我们的应用程序,分布式计算的臭名昭著的谬论仍然是真实的。的确,网络既会造成速度减慢,也会造成错误。云原生应用程序,通常是微服务,必须专门设计和部署以克服这些挑战。
为了帮助我们,我们有一个庞大的针对Kubernetes的优秀软件生态系统可供支配。Kubernetes不是传统分布式系统意义上的 "中间件",但它确实为非常令人兴奋的软件组件提供了一个平台,帮助我们编写有弹性、有性能、设计良好的软件。通过设计软件来专门利用这些功能,并以同样的方式部署它们,我们可以创建真正能够以云原生方式扩展的软件。
在这篇文章中,我介绍了关于如何设计云原生应用程序并将其部署在Kubernetes上的15条原则。 为了获得最大的收获,你还应该阅读其他三篇文章。第一篇是关于如何设计一般的可扩展应用程序。可扩展性设计原则。另外两篇是关于我们如何部署应用程序,以及它们如何以云原生的方式进行协作:12因素应用程序宣言和研究论文 "基于容器的分布式系统的设计模式"。不要被它们都是几年前的东西所迷惑:它们经受住了时间的考验,仍然具有高度的相关性。
15条原则
首先。到底什么是云原生应用?
云原生计算基金会对 "云原生 "的含义有一个正式定义。主要部分在下面一段。
"这些技术使松散耦合的系统具有弹性、可管理和可观察性。与强大的自动化相结合,它们使工程师能够以最小的工作量频繁地、可预测地进行高影响的改变"。
将其转化为可操作的具体特征和属性意味着云原生软件。
- 可以运行一个组件的多个实例,以确保高可用性和可扩展性。
- 由依赖底层平台和基础设施进行扩展、自动化、容量分配、故障处理、重新启动、服务发现等的组件组成。
云原生软件也主要是围绕着微服务启发的架构来构建的,其中组件处理定义明确的任务。这样做的一个很大的影响是,由于它使这种组件的扩展和操作明显更容易,所以组件基本上被分为有状态和无状态。大型架构中的大多数组件通常是无状态的,它们依赖于一些数据存储来管理其应用状态。
在Kubernetes上设计和部署可扩展应用程序的原则
Kubernetes使应用程序的部署和运行变得简单。给定一个容器镜像,你只需要输入一个命令来部署它,甚至在多个实例中部署(kubectl create deployment nginx -image=nginx -replicas=3)。然而,这种简单性既是一种祝福也是一种诅咒。因为以这种方式部署的应用程序无法利用Kubernetes的高级功能,因此,对平台本身的使用也是不理想的。
原则1:单个Pod几乎不是你想用的东西
因为Kubernetes可以随时终止Pod,所以你几乎总是需要有一个控制器来照顾你的Pod。单一的直接Pod在一次性调试之外很少使用。
Replica Sets也几乎不是你想直接使用的东西。
相反,你应该让一个部署或StatefulSet为你处理Pods。无论你是否打算运行一个以上的实例,这都适用。你想要自动化的原因是,Kubernetes对Pod的持续生命周期没有任何保证,以防其中的一个容器出现故障。事实上,它明确规定Pod将被终止。
原则2:明确区分有状态和无状态组件
Kubernetes定义了许多不同的资源和管理它们的控制器。每一个都有自己的语义。我见过关于什么是部署(Deployment)、什么是有状态(StatefulSet)、什么是守护(DaemonSet),以及它们能做什么或不能做什么的困惑。正确使用意味着你清楚地表达了你的意图,并且Kubernetes可以帮助你实现你的目标。
如果使用得当,Kubernetes会迫使你这样做,但外面存在许多错综复杂的解决方案。简单的经验法则是让所有有状态的东西都放在StatefulSet中,而无状态的放在Deployments中,因为这样做是Kubernetes的方式。但是请记住,还有其他的Kubernetes控制器存在,请阅读它们各自的保证和区别。
原则3:将秘密配置和非秘密配置分开,以使使用清晰并保证安全
ConfigMap和Secret之间的技术差异非常小。无论是在Kubernetes内部如何表示它们,还是在如何使用它们。但是,如果你使用基于角色的访问控制,使用正确的配置图会表明你的意图,而且也更容易操作。例如,应用程序配置存储在ConfigMap中,然后带有证书的数据库连接字符串属于Secret中,这就更清楚了。
原则4:启用自动扩展以确保容量管理
就像所有的Pod都由一个部署和一个服务来管理一样,你也应该始终考虑为你的部署配备一个Horizontal Pod Autoscaler(HPA)。
从来没有人希望在他们的生产环境中出现容量不足的情况。同样地,没有人希望终端用户因为Pod的容量分配不当而受到影响。从一开始就为这个问题做准备,意味着你将被迫进入这样一种心态:扩展可以而且会发生。这比容量耗尽要好得多。
根据一般的可扩展性设计原则,你应该已经准备好运行每个应用程序组件的多个实例。这对可用性和可扩展性至关重要。
请注意,你也可以用HPA自动扩展一个StatefulSet。然而,有状态的组件通常只有在绝对需要的情况下才能被扩展。
例如,扩展数据库会导致大量的数据复制和额外的事务管理发生,如果数据库已经处于重载状态,这绝对不是你想要的。另外,如果你对你的有状态组件进行自动扩展,请考虑关闭自动扩展功能。特别是,如果有状态组件需要以某种方式与其他实例进行同步。更安全的做法是手动触发此类动作。
原则5:通过与容器生命周期管理挂钩来增强和实现自动化
容器可以定义一个PostStart 和 PreStop 钩子,这两个钩子都可以用来执行重要的工作,以告知应用程序的其他组件一个实例的新存在或它即将终止。PreStop钩子将在终止前被调用,并有一个(可配置的)完成时间。使用它来确保即将终止的实例完成其工作,将文件提交到持久卷,或者其他任何需要发生的事情,以实现有序和自动的关闭。
原则6:正确使用探针来检测和自动恢复故障
与单进程系统相比,分布式系统会以更多、更不直观的方式失败。互联网络是造成一大类新的故障原因的原因。我们越是能检测到故障,就越有机会自动恢复故障。
为此,Kubernetes为我们提供了探测功能。特别是就绪性探测是非常有用的,因为失败向Kubernetes发出信号,你的容器(也就是Pod)还没有准备好接受请求。
尽管有明确的文档,但有效性探针经常被误解。一个失败的有效性探针表明该组件永久地陷入了一个糟糕的情况,需要强行重启来解决。而不是说它 "活着"(这是就绪度探针所指示的),因为在分布式系统中,有效性实际上是别的东西。
启动探针被添加到Kubernetes中,以示何时开始与其他探针进行探测。因此,这是一种拖住它们的方法,直到执行它们可以开始有意义。
原则7:让组件严重、快速、大声地失败
让你的应用程序组件严重失败(崩溃),快速(一旦出现问题),并且大声(在其日志中提供翔实的错误信息)。这样做将防止数据在你的应用程序中停留在一个奇怪的状态,将流量仅路由到健康的实例,并且还将提供根源分析所需的所有信息。本文中所有自动化的其他原则的实施将有助于保持你的应用程序的良好状态,同时你可以找到根本原因。
你的应用程序中的组件必须能够处理重新启动。故障可能而且将会发生。无论是在你的组件中,还是在集群本身。因为故障是不可避免的,它们必须能够处理。
原则8:为你的应用程序准备好可观察性
监控、日志和跟踪是可观察性的三大支柱。简单地将自定义指标提供给你的监控系统(Prometheus,对吗?),编写结构化的日志(例如JSON格式),不故意删除HTTP头(例如携带相关ID的头),而是将其作为日志的一部分,将为你的应用程序提供所有需要的可观察性。
如果你想要更详细的追踪信息,可以将你的应用程序与Open Telemetry API集成。但前面的步骤使你的应用程序很容易被观察到,无论是对人类操作者还是对自动化来说。基于对你的应用程序有意义的指标进行自动缩放,几乎总是比基于CPU使用率这样的原始指标要好。
网站可靠性工程的 "四个黄金信号 "是延迟、流量、错误和饱和度。根据经验和传闻,用特定于应用程序的指标来跟踪这些监测信号,比用一般的资源使用测量获得的原始指标要有用得多。
原则9:适当地设置Pod资源请求和限制
通过适当地设置Pod资源请求和限制,Horizontal Pod Autoscaler和Cluster Autoscaler都可以做得更好。如果他们知道需要多少容量和有多少可用的容量,他们确定你的Pod和整个集群需要多少容量的工作就会容易得多。
不要把你的请求和限制设置得太低!这起初可能很诱人,因为它允许集群运行更多的Pod。但是,除非请求和限制被平等地设置(给予Pod"保证 "QoS等级),否则你的Pod在正常(常规流量)操作中可能会被给予更多的资源。这是因为通常情况下,可以给他们一些松弛。因此,一切都会很好地进行。但是在高峰期,他们将被节流到你指定的数量。这当然是最糟糕的发生时间,扩大规模实际上意味着你可能会得到更差的每个Pod部署性能。这不是故意的,但也完全是调度员被告知要做的事情。
原则10:保留容量并优先考虑Pod
在容量管理方面,命名空间资源配额、在节点上预留计算资源以及适当设置Pod优先级有助于确保集群容量和稳定性不受影响。
我曾亲眼看到一个集群变得不堪重负,以至于网络插件的Pod被驱逐。这并不有趣(但很有教育意义),因为要排除故障。
原则11:根据需要强制将Pod放在一起或分散开来
Pod拓扑传播限制以及亲和和反亲和规则是一种很好的方式来表达你是否想在你的云区域和可用性区域内共同定位Pod(为了网络流量效率)或分散它们(为了冗余)。
原则12:确保Pod在可能导致停机的计划操作中的可用性
Pod中断预算规定了一组Pod(如部署中的Pod)中的多少个允许自愿中断(即由于你的命令,而不是故障),在同一时间。这有助于确保高可用性,尽管集群节点被管理员耗尽。例如,这发生在集群升级期间,而这些通常发生在每月的基础上,因为Kubernetes动作很快。
请注意,如果你不正确地设置Pod中断预算,你可能会限制管理员进行集群升级的能力。它们很容易被错误地配置为阻止耗尽节点,这干扰了自动操作系统的补丁,并损害了环境的安全态势。
如果你能设计你的应用程序来处理中断,并使用PDB让Kubernetes在无法做到的情况下帮助你,这是非常可取的。
原则13:选择蓝/绿或金丝雀部署,而不是停止世界的部署
在这个时代,为了维护而关闭整个应用程序是不可接受的。这现在被称为 "停止世界部署",即应用程序暂时无法访问。通过更复杂的部署策略,可以实现更平稳和更渐进的变化。终端用户根本不需要意识到应用程序已经改变。
蓝/绿和金丝雀的部署曾经是一门黑科技,但Kubernetes让所有人都可以使用。推出组件的新版本,并使用服务中的标签和选择器将流量路由到它们,这使得即使你在自己的脚本中或多或少地手动实现它们,也可以做到高级部署策略,当然也可以通过良好的部署工具,如ArgoCD(蓝/绿或金丝雀)。
请注意,大多数部署策略,在技术层面上,可以归结为从侧面部署同一组件的两个版本,并以不同的方式将请求分发给它们。你可以通过服务本身做到这一点,比如说,给新版本的Pod的5%贴上适当的标签,让服务把流量导向它们。或者,即将推出的Kubernetes网关资源将具有开箱即用的功能(但Ingress没有)。
原则14:避免给予Pod他们不需要的权限
Kubernetes本身并不安全,默认也是如此。但是你可以配置它来执行安全的最佳实践,比如限制容器在节点上可以做什么。
以非root用户身份运行你的容器。在Docker中构建容器镜像,使容器默认以root身份运行,这可能给黑客们带来了近十年的喜悦之泪。在你的容器构建过程中只使用root来安装依赖,然后做一个非root用户,让它来运行你的应用程序。
如果你的应用程序实际上需要提升权限,那么仍然使用一个非root用户,放弃所有的Linux功能,并只添加最小的功能集。
就在2022年1月,一个利用3年前的漏洞的容器逃逸漏洞浮出水面(CVE-2022-0185)。没有必要的Linux能力的容器?完全无法进行攻击。
原则15:限制Pod在你的集群中能做什么
禁用默认的服务账户,使其不暴露在你的应用程序中。除非你特别需要与Kubernetes API互动,否则你不应该将默认的服务账户令牌装入其中。然而,这就是Kubernetes中的默认行为。
设置并执行最严格的Pod安全策略或Pod安全标准,以确保你在默认情况下,不会不必要地使不安全的操作模式成为可能。
使用网络策略来限制你的Pod可以连接到哪些其他Pod。Kubernetes中默认的自由的网络流量是一个安全噩梦,因为这样,攻击者只需要进入一个Pod就可以直接访问所有其他的Pod。
CVSS评分为10分的Log4J漏洞(CVE-2021-44228)被幽默地命名为Log4Shell,对有锁定网络策略的容器完全无效,该策略将禁止所有出口流量,除了允许列表中的内容(而漏洞中的假LDAP服务器不会有!)。
总结
本文介绍了如何设计云原生应用并将其部署在Kubernetes上的15条原则。通过遵循这些原则,你的云原生应用可以与Kubernetes工作负载协调器协同工作。这样做可以让你获得Kubernetes平台以及云原生设计和操作软件的方式所提供的所有好处。
你已经学会了如何正确使用Kubernetes资源,为自动化做准备,如何处理故障,利用Kubernetes探测功能来提高稳定性,为你的应用程序做好可观察性的准备,让Kubernetes调度器为你工作,用高级策略进行部署,以及如何限制你部署的应用程序的攻击面。
将所有这些方面纳入你的软件架构工作,使你的日常DevOps流程更加顺畅,更加可靠。可以说,几乎到了无聊的地步。这很好,因为软件的顺利部署和管理意味着一切都在按计划进行。正如他们所说,"没有消息就是好消息"。