Kubernetes探针的工作原理

Hacker News Top 工具

摘要

本文解释了Kubernetes探针(包括启动探针、就绪探针和存活探针)如何协同工作以提升应用程序的弹性,并通过交互式演示展示其功能。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/19 19:08

# Kubernetes探针如何工作 | ngrok博客 来源:https://ngrok.com/blog/probes 我将向您*真实展示*Kubernetes中探针的工作原理。它们如何让您的应用更具韧性,以及如何帮助您避免可预防的错误。比如那些需要数小时才能恢复的重启循环,以及在滚动更新期间丢失的请求。 本文中的所有交互演示都使用webernetes (https://github.com/ngrok/webernetes),这是我对Kubernetes进行TypeScript部分移植的项目。它包含了超过10万行移植的Kubernetes Go代码,可在*您的浏览器中*运行模拟集群。我已通过k3s验证了这些演示的行为,并发现了一个Kubernetes的bug!稍后会详细介绍。 ### 您将学到什么 ## https://ngrok.com/blog/probes#a-pod-without-probes 没有探针的Pod 我将运行一个包含单个容器的Pod。以下是它的清单文件`pod-a.yaml`: ### pod-a.yaml ``` 1apiVersion: "v1"2kind: "Pod"3metadata:4 name: "pod-a"5spec:6 containers:7 - name: "app"8 image: "my-app:latest" `` 这个名为`my-app:latest`的镜像在监听8080端口前需要几秒初始化时间。当您在下方演示中点击重启按钮向容器发送信号导致其崩溃并被Kubernetes重启时,您将看到这一点。您可以随时暂停或重置任何演示。 - 0/2 重启容器 尚未完成。 首次崩溃后,容器立即重启。第二次崩溃后,Kubernetes会对其施加CrashLoopBackOff(崩溃循环退避)再重新启动。默认延迟是10秒,每次崩溃时间翻倍,最长等待5分钟。本演示中我将其缩短至3秒。 两种情况下,Kubernetes都会在容器启动时立即将其视为就绪,即使我们明知它还没有准备好。它仍在执行启动工作,尚未监听8080端口。 接下来我将添加`pod-b`,它每隔2秒向`pod-a`发送请求。在整篇文章中,您可以将`pod-b`视为任何客户端流量的来源:入口控制器、负载均衡器、服务间请求等。 如果在下方演示中重启`pod-a`时恰好有请求正在传输中,该请求将会失败。 - 导致请求失败 尚未完成。 从您重启容器到其启动工作完成期间,即使容器被认为已就绪,请求也会失败!这不是我想要的结果。我需要Kubernetes知道`pod-a`何时准备好接收流量。 为此,Kubernetes提供了**探针**。探针是定期发送到容器的健康检查,用于确定其健康状态。它们有三种类型: - 启动探针确定容器内的应用是否已启动。 - 就绪探针确定应用是否准备好接收流量。 - 存活探针确定应用是否需要重启。 听起来启动探针最适合解决我在上面演示中展示的问题,所以我们从这里开始。 ## https://ngrok.com/blog/probes#startup-probes 启动探针 下面,我在`pod-a.yaml`中添加了启动探针: ### pod-a.yaml ``` 1apiVersion: "v1"2kind: "Pod"3metadata:4 name: "pod-a"5spec:6 containers:7 - name: "app"8 image: "my-app:latest"9 startupProbe:10 httpGet:11 path: "/startup"12 port: 808013 periodSeconds: 114 failureThreshold: 5 `` 这是一个`httpGet`探针,每`periodSeconds`秒向Pod的8080端口发送`GET /startup`请求。状态码200-399视为成功。在被Kubernetes终止前,允许连续失败`failureThreshold`次。这给了我的容器约5秒时间完成启动工作。 Kubernetes还支持`tcpSocket`、`exec`和`grpc`探针。它们分别通过建立TCP连接、在容器内运行命令或调用gRPC健康检查协议来判断容器健康状况。您可以在Kubernetes文档(https://kubernetes.io/docs/concepts/workloads/pods/probes/)中阅读相关内容。本文将全程使用`httpGet`探针。 探针由名为kubelet的进程发送。集群中的每个节点都有自己的kubelet,kubelet负责确保正确的Pod在每个节点上运行并接受探针检查。 当您在下方重启`pod-a`时,它现在显示为未就绪。Kubernetes现在*意识到*`pod-a`尚未完成初始化。只有在首次启动探针成功后才会变为就绪状态。 - 0/2 重启容器 尚未完成。 kubelet 这是具有启动探针容器的Pod的默认状态。然而,即使未就绪,`pod-b`仍然会向`pod-a`发送请求,且在容器启动期间这些请求仍然会失败。这是因为`pod-b`被配置为直接向`pod-a`的IP地址发送请求,绕过了就绪机制。 ### 关于"未就绪"状态的说明 *严格来说*Kubernetes没有`NotReady`条件,它有一个`Ready`条件,值可以是`True`、`False`或`Unknown`。在演示中我使用`NotReady`是为了简化表示,避免反复出现`Ready=True`或`Ready=False`。 为了解决这些失败的请求,我需要升级到更生产级的部署:多个`pod-a`副本,请求在它们之间负载均衡。我将创建一个副本集(ReplicaSet),配置为运行2个`pod-a`副本,以及一个Service在它们之间进行负载均衡。 ### replica-set-a.yaml ``` 1apiVersion: "apps/v1"2kind: "ReplicaSet"3metadata:4 name: "replica-set-a"5spec:6 # 运行 `template` 下定义的Pod的2个副本。7 replicas: 28 selector:9 matchLabels:10 # 将具有此标签的Pod视为此副本集的一部分。11 app: "pod-a"12 template:13 metadata:14 labels:15 app: "pod-a"16 spec:17 # 与之前相同的Pod规格。18 containers:19 - name: "app"20 image: "my-app:latest"21 startupProbe:22 httpGet:23 path: "/startup"24 port: 808025 periodSeconds: 126 failureThreshold: 5 `` ### service-a.yaml ``` 1apiVersion: "v1"2kind: "Service"3metadata:4 name: "service-a"5spec:6 selector:7 # 在具有此标签的Pod之间进行负载均衡。8 app: "pod-a"9 ports:10 # 将请求发送到Pod的此端口。11 - port: 8012 targetPort: 8080 `` 从现在开始,`pod-b`将向Kubernetes为Service创建的DNS名称(本例中为`service-a.default.svc.cluster.local`)发送请求,而不是直接向单个Pod发送。Kubernetes使用Pod的`Ready`条件来决定是否将其包含在Service负载均衡中。 在下方,您可以点击重启按钮仅让顶部容器崩溃。注意当顶部容器正在启动时,请求始终被发送到底部容器。当容器未就绪时,它会将整个Pod标记为未就绪,并且不会收到来自其所属任何Service的流量。 - 0/2 重启顶部容器 尚未完成。 kubelet 尽管如此,如果您在重启顶部容器时请求仍在传输中,请求*仍可能*失败。这是因为重启按钮会突然终止容器,它没有机会完成正在处理的请求。 更好的做法是*删除*Pod并依赖副本集来创建新Pod。这样更好有两个原因: 1. Kubernetes默认给Pod 30秒的终止宽限期,在本文中我配置为2秒,以免您等待太久。删除时,Pod会被视为正在终止,Kubernetes会将其从所属的任何Service中移除。它们不会接收任何新请求。 2. 副本集不将正在终止的Pod计入活动副本,因此一旦被删除的Pod开始终止,它们就会立即创建替代副本。 结合优雅终止和启动探针,可以阻止流量到达正在启动或停止的容器。在接下来的演示中,点击删除不会导致`pod-b`的任何请求失败。 - 0/2 等待容器就绪 尚未完成。 kubelet 始终有Pod就绪可以处理新请求,这使得可以安全地删除Pod而不会中断用户流量。 ### 这个宽限期实际上是如何工作的? ### https://ngrok.com/blog/probes#how-to-misconfigure-a-startup-probe 如何错误配置启动探针 之前我提到,通过将`failureThreshold`设置为5且`periodSeconds`为1,给Pod约5秒时间完成启动工作。请谨慎为您的容器选择这些值。时间太短可能导致容器进入崩溃循环。 在下方设置`failureThreshold`将使用新值重启容器。将其设置为1或2,看看会发生什么。 - 使**pod-a**崩溃循环 尚未完成。 kubelet 几次重启后,`pod-a`进入CrashLoopBackOff状态。启动探针从未给容器足够的启动时间,因此此演示会一直崩溃循环,直到您将`failureThreshold`设置回3或更高。在为自己的容器配置时,请选择能容纳最坏情况启动时间的值。 ## https://ngrok.com/blog/probes#readiness-probes 就绪探针 在任何启动探针成功后,就绪探针将在容器的剩余生命周期中监控容器。就绪探针失败会将容器标记为未就绪,并将其从接收所属任何Service的请求中移除。 我修改了`pod-a.yaml`,目前只包含就绪探针: ### pod-a.yaml ``` 1apiVersion: "v1"2kind: "Pod"3metadata:4 name: "pod-a"5spec:6 containers:7 - name: "app"8 image: "my-app:latest"9 readinessProbe:10 httpGet:11 path: "/ready"12 port: 808013 periodSeconds: 314 failureThreshold: 115 successThreshold: 1 `` 每3秒向`/ready`端点发送一次请求。单次失败后,容器将获得未就绪条件。在下方演示中将`/ready`从200切换为503,观察容器变为未就绪。 - 等待**pod-a**就绪 尚未完成。 kubelet ### 带外探针 上面的演示将`failureThreshold`和`successThreshold`都设置为1,但我不希望单次瞬时故障就将Pod从其Service中移除。下面我将阈值设置为2。再次将`/ready`设置为503,注意现在需要2次失败后容器才会变为未就绪。 - 等待**pod-a**就绪 尚未完成。 kubelet 您可能会注意到,当从未就绪切换到就绪状态时,可能会触发带外探针。原因与之前相同。Pod未就绪且其状态刚刚更新。 默认情况下`successThreshold`为1,`failureThreshold`为3。这些通常是很好的默认值,除非有充分理由,否则不建议更改。 ### https://ngrok.com/blog/probes#why-do-we-need-startup-probes-if-we-have-readiness-probes 为什么有就绪探针还需要启动探针? 上面的演示只使用就绪探针。探针立即开始并在容器完成启动工作之前不会成功。这正是启动探针的工作,那么为什么我们需要两种类型的探针呢? 几个充分的理由: 1. 启动探针延迟就绪和存活检查,直到初始化完成。 2. 它们允许启动阶段有独立的`periodSeconds`和`failureThreshold`,因此慢速初始化可以比稳态就绪和存活检查更频繁地被探测。 3. 重复的启动失败会终止容器并应用其重启策略。就绪失败则不会。重启*可能*帮助卡住的容器变为就绪状态。 您可以同时使用多个探针。例如,我可能每秒发送一次启动探针以快速检测初始化,然后将就绪探针减慢到每5秒一次,以减少容器和kubelet的稳态探针负载。 ### pod-a.yaml ``` 1apiVersion: "v1"2kind: "Pod"3metadata:4 name: "pod-a"5spec:6 containers:7 - name: "app"8 image: "my-app:latest"9 startupProbe:10 httpGet:11 path: "/startup"12 port: 808013 periodSeconds: 114 failureThreshold: 515 readinessProbe:16 httpGet:17 path: "/ready"18 port: 808019 periodSeconds: 5 `` 就绪探针在启动探针成功之前不会开始。我在下方演示中以暂停状态开始,以便您从头观看。准备好时点击播放按钮,如果想重新开始请按重置。 这个保证——就绪探针在启动成功前不会开始——允许我在`/startup`端点中检查启动特定的内容。我可以确保初始配置已加载、缓存已预热等。实际上,启动探针的使用频率低于就绪探针。但知道有这个选项以备不时之需还是很好的。 如果您确实发现希望就绪探针能够重启容器,那么我正好有适合您的方案。 ## https://ngrok.com/blog/probes#liveness-probes 存活探针 最后一种探针类型是存活探针。这种探针的工作方式与就绪探针类似,但当达到其`failureThreshold`时,它不会将容器标记为未就绪,而是终止容器。然后Kubernetes应用Pod的`restartPolicy`(默认为`"Always"`),这意味着被终止的容器将被重启。 ### pod-a.yaml ``` 1apiVersion: "v1"2kind: "Pod"3metadata:4 name: "pod-a"5spec:6 containers:7 - name: "app"8 image: "my-app:latest"9 startupProbe:10 httpGet:11 path: "/startup"12 port: 808013 periodSeconds: 114 failureThreshold: 515 livenessProbe:16 httpGet:17 path: "/live"18 port: 808019 periodSeconds: 220 failureThreshold: 1 `` 当容器无法自行恢复时,例如主线程死锁或关键后台线程死亡时,这很有帮助。如果我能可靠地检测到这些情况,我可以使存活探针失败并依赖Kubernetes重启容器。 下面的演示显示`pod-a`在完成启动前一直接收启动探针,之后开始接收存活探针。将`/live`端点设置为返回503,观察容器被重启。 - 导致存活重启 尚未完成。 kubelet ### 上面的演示有些问题... ### https://ngrok.com/blog/probes#how-to-misconfigure-a-liveness-probe 如何错误配置存活探针 让我的存活探针检查数据库是否健康将是个坏主意。数据库的短暂故障如果持续足够长时间,可能会导致我所有的容器进入崩溃循环。 当您在下方演示中关闭数据库时,`pod-a`的存活探针将会失败。几次失败后,每个容器将进入CrashLoopBackOff状态。为了强调这可能有多糟糕,我使退避延迟像真实Kubernetes中一样缩放:最初10秒,每次崩溃时间翻倍。去制造些混乱吧! - 关闭数据库 尚未完成。 database kubelet 如果客户端重试,这个问题会更严重。目前在其他演示中尚未出现这种情况,但我的`pod-a`容器每秒只能处理3个请求。如果超过这个数量,它们会过载并崩溃!在下面的演示中,我配置了`pod-b`在循环中重试失败的请求,并将最大CrashLoopBackOff延迟再次设置为5秒。制造另一次中断,看看您能否从中恢复。 - 关闭数据库 尚未完成。 database kubelet 重试会产生所谓的**惊群效应**,导致**级联故障**。数据库是否可用已经不重要了,任何敢于恢复的容器都会被流量洪流再次击垮。 遗憾的是,探针无法帮助我摆脱这种困境。我需要创建某种机制,只允许一小部分流量通过,让容器有时间恢复,然后逐步增加到满流量。或者如果我控制客户端——例如它们是我也开发的移动应用——我可以在重试中添加退避延迟。这将减缓流量增长,使其更容易恢复。

相似文章

求职面试教会我的 Kubernetes 知识

Hacker News Top

一位求职者发现,小型初创公司采用 Kubernetes 并非出于技术可扩展性,而是为了组织层面的好处,如一致性、共享知识和可追溯性。这篇文章反思了 Kubernetes 对小型团队的非技术优势。

Kubernetes In Anger

Lobsters Hottest

一份全面指南,介绍在生产环境中调试和管理 Amazon EKS 集群,重点关注常见故障模式、事件响应和安全升级。涵盖 EKS 与标准 Kubernetes 的关键差异。

一个怀疑者与 Kubernetes 的初次接触(2024)

Hacker News Top

一位资深系统工程师兼 Kubernetes 怀疑者分享了他亲身体验 Kubernetes 的过程,他认为 Kubernetes 的价值主要源于其控制循环和声明式期望状态,而不仅仅是一张编排架构图。