这篇文章介绍如何给容器配置存活(Liveness)、就绪(Readiness)和启动(Startup)探针。
有关探针的更多信息,请参阅存活、就绪和启动探针。
kubelet 使用存活探针来确定什么时候要重启容器。例如,存活探针可以探测到应用死锁(应用在运行,但是无法继续执行后面的步骤)情况。重启这种状态下的容器有助于提高应用的可用性,即使其中存在缺陷。
存活探针的常见模式是为就绪探针使用相同的低成本 HTTP 端点,但具有更高的 failureThreshold。这样可以确保在硬性终止 Pod 之前,将观察到 Pod 在一段时间内处于非就绪状态。
kubelet 使用就绪探针可以知道容器何时准备好接受请求流量。这种信号的一个用途就是控制哪个 Pod 作为 Service 的后端。当 Pod 的 Ready 状况 为true 时,Pod 被认为是就绪的。若 Pod 未就绪,会被从 Service 的负载均衡器中剔除。当 Pod 所在节点的 Ready 状况不为 true 时、当 Pod 的某个 readinessGates 为 false时,或者当 Pod 中有任何一个容器未就绪时,Pod 的 Ready 状况为 false。
kubelet 使用启动探针来了解应用容器何时启动。如果配置了这类探针,存活探针和就绪探针在启动探针成功之前不会启动,从而确保存活探针或就绪探针不会影响应用的启动。启动探针可以用于对慢启动容器进行存活性检测,避免它们在启动运行之前就被杀掉。
存活探针是一种从应用故障中恢复的强劲方式,但应谨慎使用。你必须仔细配置存活探针,确保它能真正标示出不可恢复的应用故障,例如死锁。
错误的存活探针可能会导致级联故障。这会导致在高负载下容器重启;例如由于应用无法扩展,导致客户端请求失败;以及由于某些Pod 失败而导致剩余 Pod 的工作负载增加。了解就绪探针和存活探针之间的区别,以及何时为应用配置使用它们非常重要。
你必须拥有一个 Kubernetes 的集群,且必须配置 kubectl 命令行工具让其与你的集群通信。建议运行本教程的集群至少有两个节点,且这两个节点不能作为控制平面主机。如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:
许多长时间运行的应用最终会进入损坏状态,除非重新启动,否则无法被恢复。Kubernetes 提供了存活探针来发现并处理这种情况。
在本练习中,你会创建一个 Pod,其中运行一个基于 registry.k8s.io/busybox:1.27.2 镜像的容器。下面是这个 Pod 的配置文件。
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: registry.k8s.io/busybox:1.27.2
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在这个配置文件中,可以看到 Pod 中只有一个 Container。periodSeconds 字段指定了 kubelet 应该每 5 秒执行一次存活探测。initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 5 秒。kubelet 在容器内执行命令 cat /tmp/healthy 来进行探测。如果命令执行成功并且返回值为 0,kubelet 就会认为这个容器是健康存活的。如果这个命令返回非 0 值,kubelet 会杀死这个容器并重新启动它。
当容器启动时,执行如下的命令:
/bin/sh -c "touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600"
这个容器生命的前 30 秒,/tmp/healthy 文件是存在的。所以在这最开始的 30 秒内,执行命令 cat /tmp/healthy 会返回成功代码。30 秒之后,执行命令 cat /tmp/healthy 就会返回失败代码。
创建 Pod:
kubectl apply -f https://k8s.io/examples/pods/probe/exec-liveness.yaml
在 30 秒内,查看 Pod 的事件:
kubectl describe pod liveness-exec
输出结果表明还没有存活探针失败:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 11s default-scheduler Successfully assigned default/liveness-exec to node01
Normal Pulling 9s kubelet, node01 Pulling image "registry.k8s.io/busybox:1.27.2"
Normal Pulled 7s kubelet, node01 Successfully pulled image "registry.k8s.io/busybox:1.27.2"
Normal Created 7s kubelet, node01 Created container liveness
Normal Started 7s kubelet, node01 Started container liveness
35 秒之后,再来看 Pod 的事件:
kubectl describe pod liveness-exec
在输出结果的最下面,有信息显示存活探针失败了,这个失败的容器被杀死并且被重建了。
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 57s default-scheduler Successfully assigned default/liveness-exec to node01
Normal Pulling 55s kubelet, node01 Pulling image "registry.k8s.io/busybox:1.27.2"
Normal Pulled 53s kubelet, node01 Successfully pulled image "registry.k8s.io/busybox:1.27.2"
Normal Created 53s kubelet, node01 Created container liveness
Normal Started 53s kubelet, node01 Started container liveness
Warning Unhealthy 10s (x3 over 20s) kubelet, node01 Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 10s kubelet, node01 Container liveness failed liveness probe, will be restarted
再等 30 秒,确认这个容器被重启了:
kubectl get pod liveness-exec
输出结果显示 RESTARTS 的值增加了 1。请注意,一旦失败的容器恢复为运行状态,RESTARTS 计数器就会增加 1:
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 1m
另外一种类型的存活探测方式是使用 HTTP GET 请求。下面是一个 Pod 的配置文件,其中运行一个基于 registry.k8s.io/e2e-test-images/agnhost 镜像的容器。
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- name: liveness
image: registry.k8s.io/e2e-test-images/agnhost:2.40
args:
- liveness
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
在这个配置文件中,你可以看到 Pod 也只有一个容器。periodSeconds 字段指定了 kubelet 每隔 3 秒执行一次存活探测。initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。kubelet 会向容器内运行的服务(服务在监听 8080 端口)发送一个 HTTP GET 请求来执行探测。如果服务器上 /healthz 路径下的处理程序返回成功代码,则 kubelet 认为容器是健康存活的。如果处理程序返回失败代码,则 kubelet 会杀死这个容器并将其重启。
返回大于或等于 200 并且小于 400 的任何代码都标示成功,其它返回代码都标示失败。
你可以访问 server.go
阅读服务的源码。容器存活期间的最开始 10 秒中,/healthz 处理程序返回 200 的状态码。之后处理程序返回 500 的状态码。
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
duration := time.Now().Sub(started)
if duration.Seconds() > 10 {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
} else {
w.WriteHeader(200)
w.Write([]byte("ok"))
}
})
kubelet 在容器启动之后 3 秒开始执行健康检查。所以前几次健康检查都是成功的。但是 10 秒之后,健康检查会失败,并且 kubelet 会杀死容器再重新启动容器。
创建一个 Pod 来测试 HTTP 的存活检测:
kubectl apply -f https://k8s.io/examples/pods/probe/http-liveness.yaml
10 秒之后,通过查看 Pod 事件来确认存活探针已经失败,并且容器被重新启动了。
kubectl describe pod liveness-http
在 1.13 之后的版本中,设置本地的 HTTP 代理环境变量不会影响 HTTP 的存活探测。
第三种类型的存活探测是使用 TCP 套接字。使用这种配置时,kubelet 会尝试在指定端口和容器建立套接字链接。如果能建立连接,这个容器就被看作是健康的,如果不能则这个容器就被看作是有问题的。
apiVersion: v1
kind: Pod
metadata:
name: goproxy
labels:
app: goproxy
spec:
containers:
- name: goproxy
image: registry.k8s.io/goproxy:0.1
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
如你所见,TCP 检测的配置和 HTTP 检测非常相似。下面这个例子同时使用就绪探针和存活探针。kubelet 会在容器启动 15 秒后运行第一次存活探测。此探测会尝试连接 goproxy 容器的 8080 端口。如果此存活探测失败,容器将被重启。kubelet 将继续每隔 10 秒运行一次这种探测。
除了存活探针,这个配置还包括一个就绪探针。kubelet 会在容器启动 15 秒后运行第一次就绪探测。与存活探测类似,就绪探测会尝试连接 goproxy 容器的 8080 端口。如果就绪探测失败,Pod 将被标记为未就绪,且不会接收来自任何服务的流量。
要尝试 TCP 存活检测,运行以下命令创建 Pod:
kubectl apply -f https://k8s.io/examples/pods/probe/tcp-liveness-readiness.yaml
15 秒之后,通过查看 Pod 事件来检测存活探针:
kubectl describe pod goproxy
Kubernetes v1.27 [stable]如果你的应用实现了gRPC 健康检查协议,这个例子展示了如何配置 Kubernetes 以将其用于应用的存活性检查。类似地,你可以配置就绪探针和启动探针。
下面是一个示例清单:
apiVersion: v1
kind: Pod
metadata:
name: etcd-with-grpc
spec:
containers:
- name: etcd
image: registry.k8s.io/etcd:3.5.1-0
command: [ "/usr/local/bin/etcd", "--data-dir", "/var/lib/etcd", "--listen-client-urls", "http://0.0.0.0:2379", "--advertise-client-urls", "http://127.0.0.1:2379", "--log-level", "debug"]
ports:
- containerPort: 2379
livenessProbe:
grpc:
port: 2379
initialDelaySeconds: 10
要使用 gRPC 探针,必须配置 port 属性。如果要区分不同类型的探针和不同功能的探针,可以使用 service 字段。你可以将 service 设置为 liveness,并使你的 gRPC健康检查端点对该请求的响应与将 service 设置为 readiness 时不同。这使你可以使用相同的端点进行不同类型的容器健康检查而不是监听两个不同的端口。如果你想指定自己的自定义服务名称并指定探测类型,Kubernetes项目建议你使用使用一个可以关联服务和探测类型的名称来命名。例如:myservice-liveness(使用 - 作为分隔符)。
与 HTTP 或 TCP 探针不同,gRPC 探测不能按名称指定健康检查端口,也不能自定义主机名。
配置问题(例如:错误的 port 或 service、未实现健康检查协议)都被认作是探测失败,这一点与 HTTP 和 TCP 探针类似。
kubectl apply -f https://k8s.io/examples/pods/probe/grpc-liveness.yaml
15 秒钟之后,查看 Pod 事件确认存活性检查并未失败:
kubectl describe pod etcd-with-grpc
当使用 gRPC 探针时,需要注意以下一些技术细节:
-tls)。ExecProbeTimeout 特性门控被设置为 false,则 grpc-health-probe
不会考虑 timeoutSeconds 设置状态(默认值为 1s),而内置探针则会在超时时返回失败。对于 HTTP 和 TCP 存活检测可以使用命名的port
(gRPC 探针不支持使用命名端口)。
例如:
ports:
- name: liveness-port
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
有时候,会有一些现有的应用在启动时需要较长的初始化时间。在这种情况下,若要不影响对死锁作出快速响应的探测,设置存活探测参数是要技巧的。解决办法是使用相同的命令来设置启动探测,针对 HTTP 或 TCP 检测,可以通过将failureThreshold * periodSeconds 参数设置为足够长的时间来应对最糟糕情况下的启动时间。
这样,前面的例子就变成了:
ports:
- name: liveness-port
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 10
startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30
periodSeconds: 10
幸亏有启动探测,应用将会有最多 5 分钟(30 * 10 = 300s)的时间来完成其启动过程。一旦启动探测成功一次,存活探测任务就会接管对容器的探测,对容器死锁作出快速响应。如果启动探测一直没有成功,容器会在 300 秒后被杀死,并且根据 restartPolicy
来执行进一步处置。
有时候,应用会暂时性地无法为请求提供服务。例如,应用在启动时可能需要加载大量的数据或配置文件,或是启动后要依赖等待外部服务。在这种情况下,既不想杀死应用,也不想给它发送请求。Kubernetes 提供了就绪探针来发现并缓解这些情况。容器所在 Pod 上报还未就绪的信息,并且不接受通过 Kubernetes Service 的流量。
就绪探针在容器的整个生命周期中保持运行状态。
存活探针与就绪性探针相互间不等待对方成功。如果要在执行就绪性探针之前等待,应该使用 initialDelaySeconds 或 startupProbe。
就绪探针的配置和存活探针的配置相似。唯一区别就是要使用 readinessProbe 字段,而不是 livenessProbe 字段。
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
HTTP 和 TCP 的就绪探针配置也和存活探针的配置完全相同。
就绪和存活探测可以在同一个容器上并行使用。两者共同使用,可以确保流量不会发给还未就绪的容器,当这些探测失败时容器会被重新启动。
Probe 有很多配置字段,可以使用这些字段精确地控制启动、存活和就绪检测的行为:
initialDelaySeconds:容器启动后要等待多少秒后才启动启动、存活和就绪探针。如果定义了启动探针,则存活探针和就绪探针的延迟将在启动探针已成功之后才开始计算。在某些较旧的 Kubernetes 版本中,如果 periodSeconds 的值大于 initialDelaySeconds,则 initialDelaySeconds 将被忽略。然而,在当前版本中,initialDelaySeconds 总是被遵守,并且探测不会在这个初始延迟之前开始。默认是 0 秒,最小值是 0。periodSeconds:执行探测的时间间隔(单位是秒)。默认是 10 秒。最小值是 1。当容器未就绪时,ReadinessProbe 可能会在除配置的 periodSeconds
间隔以外的时间执行。这是为了让 Pod 更快地达到可用状态。timeoutSeconds:探测的超时后等待多少秒。默认值是 1 秒。最小值是 1。successThreshold:探针在失败后,被视为成功的最小连续成功数。默认值是 1。存活和启动探测的这个值必须是 1。最小值是 1。failureThreshold:探针连续失败了 failureThreshold 次之后,Kubernetes 认为总体上检查已失败:容器状态未就绪、不健康、不活跃。默认值为 3,最小值为 1。对于启动探针或存活探针而言,如果至少有 failureThreshold 个探针已失败,Kubernetes 会将容器视为不健康并为这个特定的容器触发重启操作。kubelet 遵循该容器的 terminationGracePeriodSeconds 设置。对于失败的就绪探针,kubelet 继续运行检查失败的容器,并继续运行更多探针;因为检查失败,kubelet 将 Pod 的 Ready
状况设置为 false。terminationGracePeriodSeconds:为 kubelet配置从为失败的容器触发终止操作到强制容器运行时停止该容器之前等待的宽限时长。默认值是继承 Pod 级别的 terminationGracePeriodSeconds 值(如果不设置则为 30 秒),最小值为 1。更多细节请参见探针级别 terminationGracePeriodSeconds。如果就绪态探针的实现不正确,可能会导致容器中进程的数量不断上升。如果不对其采取措施,很可能导致资源枯竭的状况。
HTTP Probe
允许针对 httpGet 配置额外的字段:
host:连接使用的主机名,默认是 Pod 的 IP。也可以在 HTTP 头中设置 "Host" 来代替。scheme:用于设置连接主机的方式(HTTP 还是 HTTPS)。默认是 "HTTP"。path:访问 HTTP 服务的路径。默认值为 "/"。httpHeaders:请求中自定义的 HTTP 头。HTTP 头字段允许重复。port:访问容器的端口号或者端口名。如果数字必须在 1~65535 之间。对于 HTTP 探测,kubelet 发送一个 HTTP 请求到指定的端口和路径来执行检测。除非 httpGet 中的 host 字段设置了,否则 kubelet 默认是给 Pod 的 IP 地址发送探测。如果 scheme 字段设置为了 HTTPS,kubelet 会跳过证书验证发送 HTTPS 请求。大多数情况下,不需要设置 host 字段。这里有个需要设置 host 字段的场景,假设容器监听 127.0.0.1,并且 Pod 的 hostNetwork
字段设置为了 true。那么 httpGet 中的 host 字段应该设置为 127.0.0.1。可能更常见的情况是如果 Pod 依赖虚拟主机,你不应该设置 host 字段,而是应该在httpHeaders 中设置 Host。
针对 HTTP 探针,kubelet 除了必需的 Host 头部之外还发送两个请求头部字段:
User-Agent:默认值是 kube-probe/1.35,其中1.35 是 kubelet 的版本号。Accept:默认值 */*。你可以通过为探测设置 httpHeaders 来重载默认的头部字段值。例如:
livenessProbe:
httpGet:
httpHeaders:
- name: Accept
value: application/json
startupProbe:
httpGet:
httpHeaders:
- name: User-Agent
value: MyUserAgent
你也可以通过将这些头部字段定义为空值,从请求中去掉这些头部字段。
livenessProbe:
httpGet:
httpHeaders:
- name: Accept
value: ""
startupProbe:
httpGet:
httpHeaders:
- name: User-Agent
value: ""
当 kubelet 使用 HTTP 探测 Pod 时,仅当重定向到同一主机时,它才会遵循重定向。如果 kubelet 在探测期间收到 11 个或更多重定向,则认为探测成功并创建相关事件:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 29m default-scheduler Successfully assigned default/httpbin-7b8bc9cb85-bjzwn to daocloud
Normal Pulling 29m kubelet Pulling image "docker.io/kennethreitz/httpbin"
Normal Pulled 24m kubelet Successfully pulled image "docker.io/kennethreitz/httpbin" in 5m12.402735213s
Normal Created 24m kubelet Created container httpbin
Normal Started 24m kubelet Started container httpbin
Warning ProbeWarning 4m11s (x1197 over 24m) kubelet Readiness probe warning: Probe terminated redirects
如果 kubelet 收到主机名与请求不同的重定向,则探测结果将被视为成功,并且kubelet 将创建一个事件来报告重定向失败。
在处理 httpGet 探针时,kubelet 在读取了 10KiB 的响应体后会停止读取。探针的成功与否仅由响应状态码决定,此状态码位于响应头中。
如果你探测一个返回的响应体大于 10KiB 的端点,kubelet 仍然会根据状态码将探针标记为成功,但在达到 10KiB 限制后关闭连接。这种突然的关闭可能导致connection reset by peer 或 broken pipe error 出现在你的应用程序日志中,这可能难以与合法的网络问题区分。
对于可靠的 httpGet 探针,强烈建议使用专用的健康检查端点,这些端点返回最小的响应体。如果你必须使用具有大量数据负载的现有端点,可以考虑使用 exec 探针来执行 HEAD 请求。
对于 TCP 探测而言,kubelet 在节点上(不是在 Pod 里面)发起探测连接,这意味着你不能在 host 参数上配置服务名称,因为 kubelet 不能解析服务名称。
terminationGracePeriodSecondsKubernetes v1.28 [stable]在 1.25 及以上版本中,用户可以指定一个探针层面的 terminationGracePeriodSeconds
作为探针规约的一部分。当 Pod 层面和探针层面的 terminationGracePeriodSeconds
都已设置,kubelet 将使用探针层面设置的值。
当设置 terminationGracePeriodSeconds 时,请注意以下事项:
terminationGracePeriodSeconds 字段(如果它存在于 Pod 上)。terminationGracePeriodSeconds
字段并且不再希望使用针对每个探针的终止宽限期,则必须删除现有的这类 Pod。例如:
spec:
terminationGracePeriodSeconds: 3600 # Pod 级别设置
containers:
- name: test
image: ...
ports:
- name: liveness-port
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 60
# 重载 Pod 级别的 terminationGracePeriodSeconds
terminationGracePeriodSeconds: 60
探针层面的 terminationGracePeriodSeconds 不能用于就绪态探针。这一设置将被 API 服务器拒绝。
你也可以阅读以下的 API 参考资料: