如何利用 Kubernetes 实现应用零宕机

我在本地和托管 Kubernetes 集群方面工作了七年多。我能说的是,容器已经彻底改变了托管格局!它带来了许多需要复杂设置的设施。拥有多个实例,具有滚动重启、零停机、健康检查等功能。以前真是费时费力(实现 VRRP 解决方案、使用 monit 之类的应用程序监控重启、负载均衡 haproxy 之类的)!


因此,现在使用 Kubernetes 可以更轻松地访问一切,但如果您想为应用程序的生命周期构建完美的设置,您仍然必须了解它的工作原理以及根据您的情况应遵循哪种策略。
在本文中,我将解释为什么以及如何使用 Kubernetes 实现零停机应用程序。

容器镜像位置

如果您已经使用Docker一段时间,那么这看起来很简单。拉取和使用容器镜像非常简单。但是,在生产环境中,如果您不是映像所有者,您通常不想依赖远程且不受控制的映像注册表。为什么?

  1. 注册表可能会消失,并且您无法再拉取镜像( Kubernetes 上出现ImagePullBackOff错误)
  2. 您正在使用的图像标签已被删除(相同的ImagePullBackOff错误)
  3. 图像标签没有改变,但图像内容不再相同(非不可变图像,因此图像哈希值不同)。集群的不同节点上的图像之间的行为不相同(取决于标签何时更改并在集群节点上拉取)
  4. 它不符合您要求控制这些图像的安全要求(SOC2、HIPPA…)。

存在多种解决方案。一种是将容器映像从源注册表同步到您自己的注册表


Pod 数量(应用程序实例)

这听起来很明显,但如果您正在寻求高可用性,则您的应用程序至少需要2 个 Kubernetes 副本(2 个 Pod)。例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
  ..

我多次听到的关于 Kubernetes 的一个常见错误是:“我不需要两个实例,因为 Kubernetes 执行滚动更新,因此它将在关闭当前实例之前启动一个新实例”。确实如此,但它仅适用于部署更新
以下是不适用此规则的其他场景:

  • 当您丢失运行应用程序的节点时(节点崩溃、硬件故障……)。您的应用程序pod 必须从头开始:
    • 1/镜像拉取(如果节点上尚未存在):拉取时间取决于镜像大小
    • 2/磁盘附件(如果有):需要几秒钟(通常观察到最多 1 分钟)
    • 3/应用程序启动:可能会有所不同,具体取决于受影响的资源和应用程序类型(Java 应用程序通常需要更长的时间才能启动)
    • 4 / probes:它们正在等待您的应用程序准备好提供服务,并增加了几秒钟的时间
  • 当集群请求节点耗尽时(例如在 EKS 升级或节点类型更改期间):
    要替换的节点上的 Pod 会收到SIGTERM信号以正常停止。Pod 正在从RUNNING状态切换到TERMINATING状态。此处,Kubernetes 服务已更新,以停止向TERMINATING pod 发送流量。因此,您不会再收到流量并且会出现停机时间。然后创建一个新的 Pod(请参阅上面的场景),在此期间,您不会收到任何流量。不幸的是,Kubernetes 在杀死一个 pod 之前不会启动一个新的 pod。

这就是为什么设置两个实例是避免停机的最低要求(另请参阅 Pod 反关联性部分)。


Pod 中断预算

PodDisruptionBudget (PDB) 是一个 Kubernetes 对象,它指定在部署、维护或任何给定时间不可用的 Pod 数量。这有助于确保您的应用程序保持可用,即使某些 pod 被终止或驱逐也是如此。
让我们举一个例子,我的应用程序有三个 pod(实例);我总是希望始终拥有至少两个running;我可以应用一个 PDB 对象,这将保证我始终有至少两个正在运行的 pod!

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: my-app


部署策略

Kubernetes 部署有两种策略:
RollingUpdate:默认更新,部署顺利。重新创建:在启动较新版本的应用程序之前强制应用程序完全关闭。
如何利用 Kubernetes 实现应用零宕机
如何利用 Kubernetes 实现应用零宕机
如何利用 Kubernetes 实现应用零宕机
如何利用 Kubernetes 实现应用零宕机
如何利用 Kubernetes 实现应用零宕机

默认情况下,应用 RollingUpdate 策略。但是您可以使用其他选项(例如最大不可用百分比和最大激增)来调整部署的方式。当您面临繁重的流量负载并且想要控制部署速度以最大程度地减少性能影响时,这些选项非常有用。


自动回滚部署

不幸的是,自动回滚并不是 Kubernetes 默认提供的功能。一般来说,您必须使用 Helm、ArgoCD、Spinnaker 等第三方工具才能实现自动回滚。
大多数人想要的很简单:如果我的应用程序无法正常启动,不要向其发送流量并回滚。
例如,对于 Helm,使用 Helm 实现它的一些选项很有趣:

  • — wait
  • — wait-for-jobs
  • — atomic

为了获得运行良好的解决方案,必须设置并正确配置探针(请参阅下一节)。如果 pod 没有通过其活性探针正常启动,则会自动回滚。


Probe 探针

不幸的是,探针经常被低估,但它们对于实现零停机非常重要!
验证应用程序健康状况的两个最重要的探测器是“Liveness”和“Readiness”探测器。比长篇大论更好的是,这里有一个解释目标的模式:

Kubernetes 探针工作流程
Liveness探针可确保您的应用程序处于活动状态,并将决定 pod 是存活还是死亡!
如果活性探测未成功:

  1. Pod 停止接收流量
  2. Pod 重新启动,尝试恢复健康状态。任何进一步的重新启动都会应用指数退避(指数延迟)

Readiness 探针决定是否将流量发送到您的 Pod。

  • 如果您的流量出现突发(并且活性探针正在响应),但您的应用程序开始变慢,则就绪状态可以决定停止向您的应用程序发送流量。让它恢复到更加健康的状态。
  • 如果就绪探针没有响应,则不会重新启动您的 Pod。它仅请求负载均衡器停止向该 Pod 发送流量。

在什么情况下配置自定义活跃度和就绪度探测器有用?
答案是“永远”!当然,您可以使用简单的 TCP 检查,但它永远不会像您自己在应用程序中构建的自定义探针(例如,REST API 上的专用端点)那样可靠。


初始启动时间延迟

初始启动时间可能需要延迟。它可能发生在不同的情况下:

  • 您的应用程序使用大量 CPU 来启动(例如 SpringBoot 应用程序)。
  • 您的应用程序需要在启动时执行比以前更多的操作(由于新功能),并且您没有升级分配的 CPU 资源。
  • 您的应用程序必须加载数据库中的架构和其中的数据,并且在数据库准备就绪之前它才可用。
  • 还存在其他几个例子……

但这是可能发生片状启动的示例。如果您遇到这种情况或想要预见它,您应该像这样更新initialDelaySeconds :

livenessProbe:
  initialDelaySeconds: 60
  httpGet:
  ...

注意:存在专用启动探测器,但在大多数情况下可能没有用。一般来说,initialDelaySeconds选项就足够了。


优雅终止期 GrancePeriodSeconds

这个 Kubernetes 选项并不直接与零停机功能相关,而是更多地涉及忽略应用程序正常关闭的重要性的缺点效应。
仅当应用程序能够拦截 SIGTERM时,优雅终止期才能起作用!如果应用程序未编码为拦截 SIGTERM,则它只会硬终止应用程序,无论是否存在大于 30 秒的优雅终止期,这都可能导致数据丢失。
不管理 SIGTERM 可能会带来几个问题:

  • 糟糕的用户体验:用户遇到错误、空白页面或更糟的情况
  • 丢失数据:数据尚未提交,用户事务丢失
  • 不可恢复的数据:刷新磁盘上的数据突然停止,您的应用程序无法处理它

还存在其他原因,但您会看到让应用程序足够快地关闭是多么重要。
ℹ️ 硬件故障总是有可能的,因此您的应用程序应该始终能够在此类故障后恢复。然而,类似的常规故障不应频繁发生。
多给你一点时间让你的应用程序正常停止通常是很好的做法(<5 分钟)。Kubernetes 默认值为 30 秒,但您可以使用终止GracePeriodSeconds 选项进行调整。


Pod 反亲和力

Pod 反关联性可让您避免同一节点上存在同一应用程序 (Pod) 的多个实例。当所有实例都位于同一节点上时发生节点崩溃时,您可以想象会发生停机。
为了避免这种情况,您可以要求 Kubernetes 避免所有 pod 都位于同一节点上。存在两个版本:

  1. 软反亲和性Soft Anti-Affinity
    1. PreferredDuringSchedulingIgnoredDuringExecution:它将尽最大努力避免它,但如果它不能(缺乏资源),它将在同一节点上添加两个实例。这个版本具有成本效益,并且在 95% 的情况下都能发挥作用。
  2. Hard Anti-Affinity
    1. requiredDuringSchedulingIgnoredDuringExecution:这将是一个硬性要求,不能在同一节点上有两个 pod。但如果您要求同一应用程序有 50 个 pod,则需要 50 个节点。这很快就会变得非常昂贵。

这是它在 Kubernetes 上的样子:

affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone


资源

资源是最常见的问题之一。当设置的资源不足时,您的应用程序可以:

  • 内存不足 (OOM) 并被内核驱逐。所以你会遇到停机、连接严重关闭等情况……
  • 你设置的CPU够用了吗?您的应用程序可能需要很长时间才能响应,甚至有时在活动检查成功之前无法启动。运行 100% 的 CPU 可能会强制自动缩放程序添加过多的实例。而您只需要利用当前的 CPU 数量即可。除非您知道自己在做什么,否则低于 100m 通常不好。


自动缩放Autoscaling

自动缩放是避免流量负载下停机的好方法。默认基于CPU(可以使用其他自定义指标)。这是自动部署更多实例(Pod)的简单方法。
⚠️ 自动缩放并不是魔法!您必须在 Kubernetes 上正确配置您的应用程序。
因此,例如,当您的 pod 短时间内运行超过 60% 的 CPU 时,Kubernetes 会触发一个新的 pod 来处理负载并减少当前正在运行的应用程序的使用率。
以下是 Kubernetes 上的示例:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
...
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50


结论

Kubernetes 确实有神奇的作用,但只有当应用程序尽可能是云原生且配置正确时,它才能发挥神奇作用。
总之,当您想将应用程序引入 Kubernetes 时,您至少应该注意:

  • 最少两个实例
  • 添加健康检查(探针)
  • 您的应用程序必须处理 Sigterm
  • 配置自动缩放器
  • 给予足够的资源
  • 使用 pod 反亲和力
  • 添加 PDB

如果一切设置正确,Kubernetes 体验将令人难以置信,您将不会再遭受任何停机。

链接:https://blog.devops.dev/how-to-achieve-zero-downtime-application-with-kubernetes-ba52fdea9a9b

(版权归原作者所有,侵删)


标签

发表评论

苏ICP备2023047577号-1