容器探测

容器探测用于检测容器中的应用实例是否正常工作,是保障业务可用性的一种传统机制。如果经过探测,实例的状态不符合预期,那么kubernetes就会把该问题实例” 摘除 “,不承担业务流量。kubernetes提供了三种探针来实现容器探测,分别是:

  • livenessProbe:指示容器是否正在运行。如果存活态探测失败,则 kubelet 会杀死容器, 并且容器将根据其重启策略决定未来。如果容器不提供存活探针, 则默认状态为 Success
  • readinessProbe:指示容器是否准备好为请求提供服务。如果就绪态探测失败, 端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址。 初始延迟之前的就绪态的状态值默认为 Failure。 如果容器不提供就绪态探针,则默认状态为 Success
  • startupProbe:指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被禁用,直到此探针成功为止。如果启动探测失败,kubelet 将杀死容器, 而容器依其重启策略进行重启。 如果容器没有提供启动探测,则默认状态为 Success

何时该使用存活态探针?

如果容器中的进程能够在遇到问题或不健康的情况下自行崩溃,则不一定需要存活态探针; kubelet 将根据 Pod 的 restartPolicy 自动执行修复操作。

如果你希望容器在探测失败时被杀死并重新启动,那么请指定一个存活态探针, 并指定 restartPolicy 为 “Always“ 或 “OnFailure“。

何时该使用就绪态探针?

如果要仅在探测成功时才开始向 Pod 发送请求流量,请指定就绪态探针。 在这种情况下,就绪态探针可能与存活态探针相同,但是规约中的就绪态探针的存在意味着 Pod 将在启动阶段不接收任何数据,并且只有在探针探测成功后才开始接收数据。

如果你希望容器能够自行进入维护状态,也可以指定一个就绪态探针, 检查某个特定于就绪态的因此不同于存活态探测的端点。

如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。

如果你的容器需要在启动期间加载大型数据、配置文件或执行迁移, 你可以使用启动探针。 然而,如果你想区分已经失败的应用和仍在处理其启动数据的应用,你可能更倾向于使用就绪探针。

说明:请注意,如果你只是想在Pod被删除时能够排空请求,则不一定需要使用就绪态探针;

在删除Pod时,Pod会自动将自身置于未就绪状态,无论就绪态探针是否存在。 等待Pod中的容器停止期间,Pod 会一直处于未就绪状态

何时该使用启动探针?

对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。 你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选定, 对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。

如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值,你应该设置一个启动探测,对存活态探针所使用的同一端点执行检查。 periodSeconds 的默认值是 10 秒。你应该将其 failureThreshold 设置得足够高, 以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。 这一设置有助于减少死锁状况的发生。

探针可以使用相同探测方式,下面以livenessProbe为使用示例做演示

存活命令(Exec)

许多长时间运行的应用程序最终会过渡到故障状态,并且无法恢复,除非被重启。Kubernetes 提供存活探针来检测和解决此类情况。

在本练习中,你将创建一个 Pod,该 Pod 运行基于 registry.k8s.io/busybox 镜像的容器。以下是 Pod 的配置文件pods/probe/exec-liveness.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: registry.k8s.io/busybox
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 只有一个 ContainerperiodSeconds 字段指定 kubelet 应该每 5 秒执行一次存活探针。initialDelaySeconds 字段告诉 kubelet 它应该等待 5 秒钟再执行第一个探针。为了执行探针,kubelet 在目标容器中执行命令 cat /tmp/healthy。如果命令成功,它将返回 0,kubelet 将认为容器处于活动状态并且健康。如果命令返回非零值,kubelet 将杀死容器并重启它。

当容器启动时,它将执行此命令

1
/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

1
kubectl apply -f https://k8s.io/examples/pods/probe/exec-liveness.yaml

在 30 秒内,查看 Pod 事件

1
kubectl describe pod liveness-exec

输出表明尚未失败任何存活探针。

1
2
3
4
5
6
7
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"
Normal Pulled 7s kubelet, node01 Successfully pulled image "registry.k8s.io/busybox"
Normal Created 7s kubelet, node01 Created container liveness
Normal Started 7s kubelet, node01 Started container liveness

35 秒后,再次查看 Pod 事件

1
kubectl describe pod liveness-exec

在输出的底部,有消息表明存活探针已失败,并且失败的容器已被杀死并重新创建。

1
2
3
4
5
6
7
8
9
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"
Normal Pulled 53s kubelet, node01 Successfully pulled image "registry.k8s.io/busybox"
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 秒,并验证容器是否已重启

1
kubectl get pod liveness-exec

输出显示 RESTARTS 已递增。请注意,RESTARTS 计数器会在失败的容器恢复到运行状态后立即递增。

1
2
NAME            READY     STATUS    RESTARTS   AGE
liveness-exec 1/1 Running 1 1m

存活 HTTP 请求(httpGet)

另一种存活探针使用 HTTP GET 请求。以下是运行基于 registry.k8s.io/e2e-test-images/agnhost 镜像的容器的 Pod 的配置文件。pods/probe/http-liveness.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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。

1
2
3
4
5
6
7
8
9
10
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 将杀死并重启容器。

要尝试 HTTP 存活检查,请创建一个 Pod

1
kubectl apply -f https://k8s.io/examples/pods/probe/http-liveness.yaml

10 秒后,查看 Pod 事件以验证存活探针是否已失败,并且容器是否已重启

1
kubectl describe pod liveness-http

TCP 存活探针(tcpSocket)

第三种存活探针使用 TCP 套接字。使用此配置,kubelet 将尝试在指定的端口上打开到你的容器的套接字。如果它能够建立连接,则容器被认为是健康的,如果它无法建立连接,则被认为是失败的。pods/probe/tcp-liveness-readiness.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 秒运行第一个存活探针。这将尝试连接到端口 8080 上的 goproxy 容器。如果存活探针失败,容器将被重启。kubelet 将继续每 10 秒运行此检查。

除了存活探针之外,此配置还包括一个就绪探针。kubelet 将在容器启动后 15 秒运行第一个就绪探针。与存活探针类似,这将尝试连接到端口 8080 上的 goproxy 容器。如果探针成功,Pod 将被标记为就绪,并将接收来自服务的流量。如果就绪探针失败,pod 将被标记为未就绪,并且不会接收来自任何服务的流量。

要尝试 TCP 存活检查,请创建一个 Pod

1
kubectl apply -f https://k8s.io/examples/pods/probe/tcp-liveness-readiness.yaml

15 秒后,查看 Pod 事件以验证存活探针

1
kubectl describe pod goproxy

gRPC 存活探针(grpc)

如果你的应用程序实现了 gRPC 健康检查协议,本示例展示了如何配置 Kubernetes 以将其用于应用程序存活检查。类似地,你也可以配置就绪探针和启动探针。

以下是一个示例清单pods/probe/grpc-liveness.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 健康检查端点在设置 servicereadiness 时以不同方式响应此请求。这使你能够为不同类型的容器健康检查使用相同的端点,而不是监听两个不同的端口。如果你想指定你自己的自定义服务名称并指定探针类型,Kubernetes 项目建议你使用将这些名称连接起来的名称。例如:myservice-liveness(使用 - 作为分隔符)。

注意

与 HTTP 或 TCP 探针不同,你无法按名称指定健康检查端口,也无法配置自定义主机名。

配置问题(例如:端口或服务错误、未实现的健康检查协议)被视为探测失败,类似于 HTTP 和 TCP 探测。

要尝试 gRPC 存活检查,请使用以下命令创建一个 Pod。在下面的示例中,etcd pod 被配置为使用 gRPC 存活探测。

1
kubectl apply -f https://k8s.io/examples/pods/probe/grpc-liveness.yaml

15 秒后,查看 Pod 事件以验证存活检查是否未失败。

1
kubectl describe pod etcd-with-grpc

使用 gRPC 探测时,需要注意一些技术细节。

  • 探测针对 Pod IP 地址或其主机名运行。确保将您的 gRPC 端点配置为监听 Pod 的 IP 地址。
  • 探测不支持任何身份验证参数(如 -tls)。
  • 内置探测没有错误代码。所有错误都被视为探测失败。
  • 如果 ExecProbeTimeout 功能门设置为 false,则 grpc-health-probe 不会 遵守 timeoutSeconds 设置(默认为 1 秒),而内置探测会在超时时失败。

使用命名端口

您可以为 HTTP 和 TCP 探测使用命名 port。gRPC 探测不支持命名端口。

例如

1
2
3
4
5
6
7
8
ports:
- name: liveness-port
containerPort: 8080

livenessProbe:
httpGet:
path: /healthz
port: liveness-port

使用启动探测保护启动缓慢的容器

有时,您需要处理在第一次初始化时需要额外启动时间的应用程序。在这种情况下,在不影响导致此类探测的死锁的快速响应的情况下,设置存活探测参数可能很棘手。解决方案是使用相同的命令、HTTP 或 TCP 检查设置启动探测,并使用一个 failureThreshold * periodSeconds 的足够长的时间来覆盖最坏情况的启动时间。

因此,前面的示例将变为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 = 300 秒)的时间完成启动。一旦启动探测成功一次,存活探测就会接管,以提供对容器死锁的快速响应。如果启动探测从未成功,则容器将在 300 秒后被杀死,并受 pod 的 restartPolicy 影响。

探针层面的 terminationGracePeriodSeconds

特性状态: Kubernetes v1.28 [stable]

在 1.25 及以上版本中,用户可以指定一个探针层面的 terminationGracePeriodSeconds 作为探针规约的一部分。 当 Pod 层面和探针层面的 terminationGracePeriodSeconds 都已设置,kubelet 将使用探针层面设置的值。

当设置 terminationGracePeriodSeconds 时,请注意以下事项:

  • kubelet 始终优先选用探针级别 terminationGracePeriodSeconds 字段 (如果它存在于 Pod 上)。

  • 如果你已经为现有 Pod 设置了 terminationGracePeriodSeconds 字段并且不再希望使用针对每个探针的终止宽限期,则必须删除现有的这类 Pod。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 服务器拒绝

容器重启策略

Pod 的 spec 中包含一个 restartPolicy 字段,其可能取值包括 Always、OnFailure 和 Never。默认值是 Always。

restartPolicy 应用于 Pod 中的应用容器和常规的 Init 容器Sidecar 容器忽略 Pod 级别的 restartPolicy 字段:在 Kubernetes 中,Sidecar 被定义为 initContainers 内的一个条目,其容器级别的 restartPolicy 被设置为 Always。 对于因错误而退出的 Init 容器,如果 Pod 级别 restartPolicyOnFailureAlways, 则 kubelet 会重新启动 Init 容器。

  • Always:只要容器终止就自动重启容器。
  • OnFailure:只有在容器错误退出(退出状态非零)时才重新启动容器。
  • Never:不会自动重启已终止的容器。

当 kubelet 根据配置的重启策略处理容器重启时,仅适用于同一 Pod 内替换容器并在同一节点上运行的重启。当 Pod 中的容器退出时,kubelet 会以指数级回退延迟机制(10 秒、20 秒、40 秒……)重启容器, 上限为 300 秒(5 分钟)。一旦容器顺利执行了 10 分钟, kubelet 就会重置该容器的重启延迟计时器。 Sidecar 容器和 Pod 生命周期中解释了 init containers 在指定 restartpolicy 字段时的行为。