原文链接:
https://jaadds.medium.com/building-resilient-applications-on-kubernetes-9e9e4edb4d33
Kubernetes 提供的某些特性可以帮助企业充分利用云原生应用的优势,例如无需关闭整个集群即可更改基础设施、自动修复应用程序以及根据流量进行动态扩展。
然而,正是这种动态特性引发了一个问题:Kubernetes 是否足够稳定,能够运行那些任务关键型、高吞吐量的应用程序?
例如,Pod 随时可能出现故障,这意味着流量和用户体验可能会受到影响。虽然在配置不当的情况下确实会发生这种情况,但通过正确的探针、Pod 分配策略以及扩展选项,任何应用程序都可以在 Kubernetes 上稳健运行。
本文中,我们将讨论如何减少宕机时间并介绍一款开源工具帮助您提高在 Kubernetes 上运行的应用程序的弹性。
配置健康探针(Probe)
健康探针用于告知用户 Kubernetes 控制平面应用程序是否健康、是否准备好接收流量或仍处于启动状态。正确配置的健康探针可以提升应用程序的响应能力,而配置不当则可能导致不必要的宕机。
健康探针常常是容易配置错误的部分,因此理解不同类型健康探针的作用非常重要。
不同探针的作用
-
就绪探针(Readiness Probe): 在将流量路由到 Pod 之前进行检查。如果就绪探针检测失败,Pod 将不再接收流量。
-
存活探针(Liveness Probe): 用于判断 Pod 内的容器是否正常运行。如果存活探针检测失败,容器将被重新启动。
就绪探针
当 Pod 完全启动并准备好接收流量时,就绪探针才会通过检测。如果需要通过启动缓存或初始化数据库连接池来预热应用程序,请在就绪探针检测之前完成这些操作。
只有当您的 Pod 通过 Kubernetes 服务对外提供流量时,才需要配置就绪探针。如果您的 Pod 只是执行任务,例如运行批处理作业或从队列中消费数据,则不需要配置就绪探针。
就绪探针检测失败的情况
-
容器被过多的流量压垮,需要先完成当前请求才能接受新的流量。
-
应用程序接收到
SIGTERM
信号(我们将在下一节中进一步讨论这一点)。
选择探针时的注意事项
检查 endpoint 和服务的 endpoint 保持一致是一个不错的选择;当请求处理的 endpoint 被完全占用时,发送给就绪探针的请求也会失败,从而减少流量进入 Pod。
为了更好地评估应用程序的就绪状态,可以使用多种指标,例如数据库连接池的利用率、正在使用的线程数以及并发请求数。当这些指标显示应用程序已经达到负载极限时,您可以使就绪探针检测失败,以防止性能的进一步恶化。
根据服务的类型,您可以选择使用命令、HTTP 探针、TCP 探针或 gRPC 探针。
需要注意的是,尽量避免使用 TCP 探针,因为它只检查服务是否可以通过端口访问,不会深入到应用程序层面,因此不能正确反映应用程序是否能够接受新请求。
一般情况下,Web 应用程序和暴露 REST 或 GraphQL 接口的微服务/应用程序应使用 HTTP 探针。而对于使用 gRPC 接口的服务,则应使用 gRPC 探针。
几点建议
-
在应用程序代码中记录就绪探针的执行时间。如果探针检测失败过于频繁,有助于调整阈值。
-
避免在就绪探针中调用后端服务,因为这可能导致探针超时并检测失败。探针的目的是判断应用程序是否准备好接收流量。如果后端服务出现故障,应用程序应该通过适当的错误处理机制来应对。
-
如果应用程序完全依赖后端服务,且需要在通过就绪探针前获取后端服务状态,请异步获取这些状态信息。
存活探针(Liveness Probe)
存活探针用于判断应用程序是否正在运行或存活。应用程序未准备好并不意味着它没有存活(它可能正在处理进行中的请求,但暂时不接受新的请求)。然而,如果应用程序不存活,那么它永远无法准备好接收流量。
存活探针检测失败的情况
-
容器遇到了无法通过非重启方式解决的问题。
-
应用程序进入了冻结状态,无法通过等待或冷却恢复。例如,由于数据库服务器响应缓慢,数据库连接池被停滞。
-
与关键后端服务(如数据库或队列)的连接失败,且无法在不重启的情况下重新建立连接。
选择探针时的注意事项
在我看来,只有当应用程序可以通过重启恢复时,才应该使用存活探针。一个理想的存活探针应返回非常简单(或硬编码)的响应,除非应用程序出现严重问题,否则不会导致探针失败。
如果您的应用程序需要通过重启来重新建立与后端服务(如数据库或队列)的连接,那么在通过存活探针前检查这些连接可能是个好做法。
不过需要注意的是,使用现代框架(如 Express、Spring Boot)开发的云原生应用通常不需要重启来重新建立连接——处理后端连接池的库通常能够自动完成此过程。如果您发现不需要存活探针,最好避免使用它。
注意事项
-
存活探针和就绪探针不应该使用相同的配置。这两个探针的要求不同,您在就绪探针中采取的操作可能会导致存活探针过早报错。
-
如果必须为两个探针使用相同的端点,请确保它们有不同的配置(如
initialDelay
、timeout
、period
等)。 -
Kubernetes 在执行探针时不保证顺序,因此确保就绪探针先于存活探针失败。如果就绪探针允许三次失败,存活探针应容忍五次失败。
优雅处理 Pod 终止
Pod 终止时是请求失败的另一个常见原因。除非能优雅地处理 Pod 的终止,否则发送到终止 Pod 的请求可能会失败。
当 Pod 内的容器没有优雅地关闭(即突然停止或崩溃)时,正在处理中的请求会受到影响。此外,与 pod 关联的 Kubernetes 服务对象会一直发送流量,直到应用程序退出,从而导致某些请求收到“连接拒绝”的报错。
防止出现此类突发故障的方式是正确处理 SIGTERM
信号。为了理解其工作原理,让我们详细看看 Pod 的终止生命周期。
-
当 Pod 被标记为删除时,kubelet 会调用 Pod 中定义的所有容器的
preStop
处理程序。同时,优雅终止周期(由terminationGracePeriodSeconds
定义)的倒计时也开始了。 -
在
prestop
钩子执行完毕后,kubelet 会向 Pod 内所有容器发出SIGTERM
信号。SIGTERM
是请求终止容器内运行的应用程序的信号。然而,应用程序在收到SIGTERM
后仍然可以继续运行。 -
在优雅终止周期结束后,kubelet 会向所有容器发出
SIGKILL
信号。如果已经有正在运行的应用程序,该命令将使其停止运行。 -
Kubernetes 控制平面随后会移除 Pod,此后,任何客户端都无法看到 Pod。
通常情况下,当 Pod 开始终止时,SIGTERM
是容器收到的第一个信号。如果指定了 prestop
处理器,它将优先被调用。在某些情况下,指定 prestop
处理器有助于实现 Pod 的优雅终止。
接下来,我们将首先探讨如何处理 SIGTERM
信号,然后再了解 preStop
处理器如何实现相同的效果。
处理 SIGTERM 信号
停止接收流量的最可靠方法是在 pod 开始终止时,通过捕捉 SIGTERM
信号使就绪探测器失败。
PS:妥善处理 SIGTERM 信号被认为是提高应用程序弹性的最重要环节。这是因为应用程序通过此信号得知自己即将被终止。Kubernetes 的动态特性要求它出于各种原因(如推出新应用版本、缩减副本、集群维护任务等)频繁终止 pod。如果不处理 SIGTERM,您将为 Kubernetes 的动态特性破坏应用程序的稳定性埋下隐患。
在收到 SIGTERM
后,应用程序需要捕获该信号并进行优雅关闭,具体步骤如下:
-
通过明确标记条件或关闭接受请求的线程池,使准备就绪探针失败。
-
让正在进行的请求完成。
-
关闭任何双向连接,如 WebSocket 或 gRPC。
-
关闭数据库连接和队列连接。
当就绪探针失败时,连接到 Pod 的服务对象将不再将请求发送到该 Pod 的 IP,这样可以立即减少流量。
要接收 SIGTERM
信号,容器内的应用程序应该以进程 ID 1
运行。如果您使用脚本来启动应用程序,那么该脚本将是 PID 为 1 的进程。在这种情况下,您需要保存应用程序的 PID
,并使用该 PID
将 SIGTERM
信号转发回应用程序。
调整滚动更新
在推出新版本应用程序时,也可能会出现宕机。当部署被扩展到最大副本数并进行滚动更新时,有资格处理流量的 Pod 可能会出现故障,导致某些请求失败。
通过将 maxUnavailable
设置为 0,您可以确保在关闭任何现有 Pod 之前,先调度新 Pod。
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
请注意,一旦新 Pod 开始运行,旧 Pod 就会被关闭。控制平面并不会检查新 Pod 是否已准备好接收流量。如果您需要在新 Pod 准备好接收流量之前延迟终止旧 Pod,可以使用就绪门(readiness gates)。
目前,这一功能仅由 AWS 负载均衡器原生支持。有关如何在 AWS 负载均衡器中使用就绪门的更多信息,请查看AWS官方文档[1]
定义 Pod 中断预算
在进行集群维护任务时,例如更新 Kubernetes 版本或升级集群节点,Pod 可能会被终止或重新调度到不同的节点。Pod 中断预算(Pod Disruption Budget, PDB)通过允许保留最小数量的 Pod,来保护部署不受这些中断的影响。以下配置表示在发生中断时,至少有 50% 的 Pod 处于可用状态:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: pdb-front–end
spec:
minAvailable: 50%
selector:
matchLabels:
app: front–end
如果您要保护的是无状态应用程序,最好使用百分比来表示 minAvailable
和 maxUnavailable
属性。因为随着时间的推移,运行应用程序所需的副本数量可能会增加,从而使 PDB 定义很快过时。
如果应用程序是有状态的,并且需要维护最低数量的 Pod 以达到基本要求,那么最好将属性指定为具体数字。
需要注意的是,并非所有中断都考虑 PDB。例如,使用 kubectl 删除部署并不会触发 PDB。通常,与节点相关的更改,如 kubectl delete
、kubectl drain
和 kubectl cordon
才会评估 PDB。这篇博客[2]详细讨论了经过 PDB 的自愿中断。您还可以参考 K8s 官方文档[3],以获取有关定义 PDB 的更多建议。
此外,开源K8s自动扩缩容工具 Karpenter 可以进一步赋能 PDB,它能够配置中断预算并且精确管理节点更新的速度。
例如,Karpenter 可以设置单个节点每15分钟更新一次,从而大大降低对运行服务的潜在影响。这种循序渐进、可控的方法可确保服务保持稳定,并最大限度地减少升级期间的宕机时间。
在节点之间分配 Pod
如果发生节点级故障,将 Pod 分布在不同节点上将有助于减少故障的影响。您可以通过使用 Pod 亲和性(Pod Affinity)或拓扑分布约束(Topology Spread Constraints)来实现这一点。
Pod 反亲和性(Pod Anti-affinity)
通过使用反亲和性规则,您可以指示 Kubernetes 调度器不要将两个 Pod 调度到同一个节点上。
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- transaction-service
topologyKey: kubernetes.io/hostname
根据这一规则,标签为 app
且值为 transaction-service
的两个 Pod 将不会运行在具有相同 kubernetes.io/hostname
值的节点上。
根据您对这一行为的严格程度,可以选择 requiredDuringSchedulingIgnoredDuringExecution
或 preferredDuringSchedulingIgnoredDuringExecution
策略。当使用前者时,kube-scheduler
只有在满足条件的情况下才能调度 Pod。
如果找不到没有 transaction-service 应用的节点,它将等待新的节点可用。使用后者时,调度器会尽量避免将 transaction-service Pod 放置在同一节点上,但如果没有合适的节点可用,它将违反条件进行调度。
拓扑分布约束(Topology Spread Constraints)
假设您在一个托管的 Kubernetes 集群上运行,例如 EKS 或 AKS,并且需要将 Pod 均匀分布在不同的可用区(Availability Zones)中。您可以使用 topologySpreadConstraints
来实现这一点。
可用区是云基础设施提供商用来隔离故障的一种方法。通过在可用区之间分散 Pod,可以使应用程序具有更高的可用性。
即使有适当的反亲和性规则将 Pod 放置在不同的节点上,您的应用程序也可能会被调度成如下情况:
通过定义如下的 topologySpreadConstraint
,您可以实现均匀分布:
topologySpreadConstraints:
- labelSelector:
matchLabels:
app: transaction-service
maxSkew: 1
topologyKey: topology.kubernetes.io/zone
matchLabelKeys:
- pod-template-hash
whenUnsatisfiable: DoNotSchedule
您可能会想知道,为什么在这种情况下不能使用反亲和性,只需将 Topology key 更改为 topology.kubernetes.io/zone
。如果不仔细配置策略和参数,则会限制应用程序的扩展性。例如,如果您只是简单地通过更改 Topology key 来更改我们看到的反亲和性策略,那么您的应用程序将无法扩展到超过三个副本。
因此,当您的目的是将一个 Pod 与另一个 Pod 放在一起,或避免将 Pod 放在一起时,最好使用亲和;而使用拓扑分布约束则可以在节点、可用区和区域之间分配 Pod。
借助 Karpenter 提升应用弹性
Karpenter是一款开源的 Kubernetes 集群自动扩缩容工具。它可以通过观察不可调度的 Pod 的聚合资源请求并做出启动和终止节点的决策,以最大限度地减少调度延迟,从而在几秒钟内(而不是几分钟)提供合适的计算资源来满足您的应用程序的需求。
它根据 pod 的调度需求自动配置和取消配置节点,从而实现高效扩展和成本优化。它的主要功能包括:
-
监控 Kubernetes 调度器因资源限制而无法调度的 pod。
-
评估无法调度 pod 的调度要求(资源请求、节点选择器、亲和力、容忍度等)。
-
提供满足这些 pod 要求的新节点。
-
在不再需要节点时将其移除。
2个月前,Karpenter 发布了 1.0(GA) 版本,在这一版本中 Karpenter 包含以下2个重大更新可以帮助您提升应用弹性和稳定性:
1、按原因设置中断预算(Disruption Budget)
Karpenter 的中断控制在节约成本和可用性方面已经是一项了不起的功能。Karpenter 会自动发现可中断的节点,并在需要时启动替代节点。
1.0 版引入了中断预算,可按原因(如未充分利用、空闲或漂移)设置中断预算。该功能在对服务可用性要求极高的生产环境中非常关键。
假如您在“双十一”期间运行一个线上购物平台。在流量高峰期,可能会合并利用率低的节点以节省成本,但这可能会无意间中断正在进行的交易,从而导致糟糕的客户体验。
如果没有 Karpenter 合理的中断预算,则无法轻松避免此类情况。现在,您可以定义策略,在关键时期限制节点合并,而在非高峰期(如闪购或节假日促销)允许节点合并,确保您的服务不中断并降低成本。
2、新的优雅终止期限(Termination Grace Period)
安全性和合规性对于维护稳定、安全的 Kubernetes 环境至关重要。但是管理节点的生命周期以确保它们保持合规性具有挑战。
例如,如果您需要遵守严格的安全和合规规定,您可能希望确保节点的运行时间不超过预定的期限,以避免出现潜在的漏洞。
在 Karpenter 1.0 之前,管理节点生命周期以满足这些规定需要手动操作或自定义自动化。terminationGracePeriod
通过强制执行节点的最长生命周期,自动终止和替换超过预定寿命的节点,从而实现了这一过程的自动化。
这可以防止使用过时或可能不合规的节点,确保基础设施在无需人工监督的情况下保持安全和合规,并降低软件或配置过时的风险。
Karpenter 的这些重大更新意味着 K8s 集群的自动扩展正在向智能化迈进。
随着越来越多的知名企业(如Slack、阿迪达斯、奥迪等)采用 Karpenter 来帮助他们优化成本,我们可以预见:在未来,集群管理的复杂性将越来越自动化,从而使 DevOps 团队能够专注于更高层次的战略举措。
如果您正在寻求以更智能、自动化的方式优化 Kubernetes 基础设施,Karpenter + 云妙算将您的绝佳选择。
云妙算通过智能节点选择、Spot自动化及智能 AI 预测功能,让用户可以采用最多样化的实例类型,最大化利用 Spot 实例并且提前预测 Spot 实例中断时刻,降低应用50%以上的云成本,同时保证应用稳定性。
结 论
尽管实现 Kubernetes 的弹性和零宕机部署似乎是不可能的,但通过精心规划和适当配置,您可以利用该平台的动态特性来运行关键任务应用程序。
通过采用本文介绍的策略——例如配置适当的健康探针、优雅地处理 Pod 终止和定义 Pod 中断预算——您可以显著减少宕机时间并提高应用程序的整体稳定性。此外,配置集群自动扩展工具 Karpenter 和 Pod 自动扩展可以更有效地应对流量激增。
随着 Kubernetes 的不断发展,了解最新的功能和最佳实践将进一步增强您构建和维护弹性应用程序的能力。
标签:搞定,就绪,Kubernetes,宕机,探针,应用程序,Pod,K8s,节点 From: https://www.cnblogs.com/cloudpilot-ai/p/18515506/k8s-ha-policies