Kubernetes探针的工作原理
摘要
本文解释了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 知识
一位求职者发现,小型初创公司采用 Kubernetes 并非出于技术可扩展性,而是为了组织层面的好处,如一致性、共享知识和可追溯性。这篇文章反思了 Kubernetes 对小型团队的非技术优势。
Kubernetes In Anger
一份全面指南,介绍在生产环境中调试和管理 Amazon EKS 集群,重点关注常见故障模式、事件响应和安全升级。涵盖 EKS 与标准 Kubernetes 的关键差异。
@jaga_prasanna: https://x.com/jaga_prasanna/status/2079614504451838426
一个教育性的推文串,使用像亚马逊仓库和巡航控制这样易于理解的类比来解释Kubernetes的核心概念,分解了组件和期望状态机制。
@BenjDicken: 对Kubernetes的精彩介绍。没有它,需要处理100种复杂性和边缘情况。有了它,你就有…
一条推文强调了对Kubernetes的精彩介绍,并引用了Fatih Arslan关于控制理论和反馈循环的文章,这些理论用于构建能够自我修复、弹性伸缩、可扩展数千个数据库的系统。
修复 Kubernetes 1.36 中 kubelet 的内存泄漏
详细记录追踪并修复 Kubernetes 1.36 中 kubelet 组件的一个内存泄漏问题,该问题由 startPodSync 中的上下文泄漏引起。作者使用 Go pprof 分析工具识别了该问题并实施了修复。