设计可扩展的云原生应用程序需要深思熟虑,即便拥有大量云来部署我们的应用程序,仍然有许多挑战需要克服。以复杂而臭名昭著的分布式计算仍然是真实的。另外网络会导致速度变慢和意外错误。因为云原生应用程序通常是微服务,所以必须专门设计和部署以克服这些挑战。
为了帮助我们,我们拥有一个庞大的针对 Kubernetes 的优秀软件生态系统。Kubernetes 不是传统分布式系统意义上的“中间件”,但它确实为非常令人兴奋的软件组件提供了一个平台,帮助我们编写弹性、高性能和设计良好的软件。通过设计软件来利用 Kubernetes 的这些特性,并以同样的方式部署它们,我们可以创建真正能够以云原生方式扩展的软件。
在本文中,我将介绍如何设计云原生应用程序并将其部署在 Kubernetes 上的 15 条原则。
15 条原则
- 从不使用单 Pod
- 有状态与无状态区别
- 秘密与非秘密
- 自动缩放
- 生命周期管理
- 探针
- 快速失败
- 可观测性
- 资源请求与限制
- 预留资源和优先级
- 调度要求
- Pod SLO
- 不停机部署
- 权限限制
- 攻击面限制
原则 1:单个 Pod 几乎是不可用的
因为 Kubernetes 可以在必要时自行决定终止 Pod,所以您几乎总是需要一个控制器来创建您的 Pod。除了一次性调试之外,很少使用单个 Pod。
副本集也几乎不是您想直接使用的东西。
相反,您应该有一个 Deployment 或 StatefulSet 创建 Pod。无论您是否打算运行多个实例,这都适用。您希望实现自动化的原因是 Kubernetes 不保证 Pod 的持续生命周期,以防万一其中的容器发生故障,所以需要同时运行多个实例。
原则 2:明确区分有状态和无状态组件
Kubernetes 定义了许多不同的资源和管理它们的控制器。每个都有自己的语义。我看到了关于 Deployment、StatefulSet 和 DaemonSet 是什么以及它们能做什么或不能做什么的困惑。用对了意味着你清楚地表达了你的意图,Kubernetes 可以帮助你实现目标。
如果使用得当,Kubernetes 会强制你这样做,但存在许多复杂的解决方案。简单的经验法则是让所有有状态的东西都在 StatefulSet 中,而在 Deployments 中是无状态的,因为这样做是 Kubernetes 的方式。使用时还请仔细阅读官方文档。
原则 3:将秘密与非秘密配置分开,以明确使用以保证安全
ConfigMap 和 Secret 之间几乎没有技术差异 。既包括它们在 Kubernetes 内部的表示方式,也包括它们的使用方式。例如,应用程序配置存储在 ConfigMap 中,然后带有凭据的数据库连接字符串属于 Secret。
原则 4:启用自动扩容选项
就像所有 Pod 实际上都由 Deployment 管理并以 Service 为前端一样,您还应该始终考虑 为您的 Deployment 使用 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 使用率等原始指标更好。
SRE 的 “四个黄金信号” 是延迟、流量、错误和饱和度。从经验上看,使用特定于应用程序的指标跟踪这些监控信号比使用通用基础资源获得的原始指标要有用得多。
原则 9:适当限制 Pod 资源请求
通过适当地设置 Pod 资源请求和限制 , Horizontal Pod Autoscaler 和 Cluster Autoscaler 都可以做得更好。如果他们知道需要多少容量 和 可用容量,他们确定您的 Pod 和整个集群需要多少容量的工作会容易 得多。
不要将您的请求和限制设置得太低!一开始这可能很诱人,因为它允许集群运行更多的 Pod。但除非请求和限制设置相同(为 Pod 提供 “有保证的” QoS 类),否则您的 Pod 在正常(常规流量)操作期间可能会获得更多资源。看起来一切都很好地工作。但在高峰期,它们的QPS将被限制在您指定的数量。而扩大规模实际上意味着每个部署的 Pod 占用更多的资源,但是整体性能可能会更差。
原则 10:预留容量并优先考虑 Pod 优先级
在容量管理方面, 命名空间资源配额、节点上预留的计算资源以及适当设置 Pod 优先级 有助于确保集群容量和稳定性不受影响。
我个人看到一个集群负载过高,以至于网络插件的 Pod 被驱逐。
原则 11:根据需要强制合并或分散 Pod
Pod 拓扑传播约束以及亲和性和反亲和性规则可以做到将 Pod 放在一起(以提高网络流量效率)或将它们分散(以实现冗余)跨多云区域保证可用性的好方法。
原则 12:在可能导致停机的计划操作期间确保 Pod 可用性
Pod Disruption Budget 指定一次允许多少个 Pod 集合(例如在 Deployment 中)被 自愿中断 (即,由于您的命令,而不是故障)。尽管管理员标记了部分不可用集群节点,这有助于确保高可用性。例如,在集群升级期间会发生这种情况,并且通常每月发生一次,因为 Kubernetes 更新速度很快。
请注意,如果您错误地设置 Pod 中断预算,您可能会限制管理员进行集群升级的能力。这会干扰自动操作系统修补并危及环境的安全状况。
PDB 会限制因自愿中断而同时停机的复制应用程序的 Pod 数量。
原则 13:选择蓝/绿或金丝雀部署而不是停机部署
在这个时代,为了升级维护而关闭整个应用程序是不可接受的。这现在被称为“stop-the-world 部署”,其中应用程序暂时无法访问。通过更复杂的部署策略,可以实现更平滑和更渐进的变化。最终用户根本不需要知道应用程序已更改。
蓝/绿 和 金丝雀 部署曾经是一门黑色艺术,但 Kubernetes 让所有人都可以更廉价的使用它。更快的推出组件的新版本。不过可能需要您在自己的脚本中或多或少地手动实现它们,但是更多好的方式是选择一些 CD 发布工具,以执行高级部署策略,例如 ArgoCD(蓝/绿或金丝雀)。
请注意,在技术层面上,大多数部署策略归结为同时部署同一组件的两个版本,并以不同的方式将请求拆分给它们。您可以通过 Service 本身执行此操作,例如,用适当的标签标记新版本的 5% 的 Pod,以使 Service 将流量路由到它们。或者即将推出的 Kubernetes Gateway 将开箱即用。
原则 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。Kubernetes 中畅通无阻的默认网络是一场安全噩梦,因为这样,攻击者只需进入一个 Pod 即可直接访问所有其他 Pod。
完美的 Log4J 漏洞 ( CVE-2021-44228 ) 幽默地命名为 Log4Shell 对具有锁定网络策略的容器完全无效,这将禁止所有出口流量,除了白名单上的流量(以及那个来自漏洞利用的 LDAP 服务不会生效!)。
概括
本文介绍了如何设计云原生应用程序并将其部署在 Kubernetes 上的 15 条原则。通过遵循这些原则,您的云原生应用程序可以与 Kubernetes 工作负载编排器协同工作。这样做可以让您获得 Kubernetes 平台以及设计和操作软件的云原生方式提供的所有好处。
您已经学习了如何正确使用 Kubernetes 资源、为自动化做准备、如何处理故障、利用 Kubernetes 探测功能提高稳定性、为应用程序准备可观察性、使 Kubernetes 调度程序为您工作、使用高级策略执行部署,以及如何限制已部署应用程序的攻击面。
将所有这些方面融入到您的软件架构工作中,可以让您的日常 DevOps 流程更加顺畅和可靠。可以说,几乎到了无聊的地步。这很好,因为软件的顺利部署和管理意味着一切都按预期工作。正如这句话所说的,“没有消息就是最好的消息”。