--- title: 配置存活、就绪和启动探测器 content_type: task weight: 110 --- 这篇文章介绍如何给容器配置存活、就绪和启动探测器。 [kubelet](/zh/docs/admin/kubelet/) 使用存活探测器来知道什么时候要重启容器。例如,存活探测器可以捕捉到死锁(应用程序在运行,但是无法继续执行后面的步骤)。这样的情况下重启容器有助于让应用程序在有问题的情况下更可用。 kubelet 使用就绪探测器可以知道容器什么时候准备好了并可以开始接受请求流量, 当一个 Pod 内的所有容器都准备好了,才能把这个 Pod 看作就绪了。这种信号的一个用途就是控制哪个 Pod 作为 Service 的后端。在 Pod 还没有准备好的时候,会从 Service 的负载均衡器中被剔除的。 kubelet 使用启动探测器可以知道应用程序容器什么时候启动了。如果配置了这类探测器,就可以控制容器在启动成功后再进行存活性和就绪检查,确保这些存活、就绪探测器不会影响应用程序的启动。这可以用于对慢启动容器进行存活性检测,避免它们在启动运行之前就被杀掉。 ## {{% heading "prerequisites" %}} {{< include "task-tutorial-prereqs.md" >}} {{< version-check >}} ## 定义存活命令 {#define-a-liveness-command} 许多长时间运行的应用程序最终会过渡到断开的状态,除非重新启动,否则无法恢复。Kubernetes 提供了存活探测器来发现并补救这种情况。 在这篇练习中,会创建一个 Pod,其中运行一个基于 `k8s.gcr.io/busybox` 镜像的容器。下面是这个 Pod 的配置文件。 {{< codenew file="pods/probe/exec-liveness.yaml" >}} 在这个配置文件中,可以看到 Pod 中只有一个容器。`periodSeconds` 字段指定了 kubelet 应该每 5 秒执行一次存活探测。`initialDelaySeconds` 字段告诉 kubelet 在执行第一次探测前应该等待 5 秒。kubelet 在容器内执行命令 `cat /tmp/healthy` 来进行探测。如果命令执行成功并且返回值为 0,kubelet 就会认为这个容器是健康存活的。如果这个命令返回非 0 值,kubelet 会杀死这个容器并重新启动它。 当容器启动时,执行如下的命令: ```shell /bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600" ``` 这个容器生命的前 30 秒, `/tmp/healthy` 文件是存在的。所以在这最开始的 30 秒内,执行命令 `cat /tmp/healthy` 会返回成功码。30 秒之后,执行命令 `cat /tmp/healthy` 就会返回失败码。 创建 Pod: ```shell kubectl apply -f https://k8s.io/examples/pods/probe/exec-liveness.yaml ``` 在 30 秒内,查看 Pod 的事件: ```shell kubectl describe pod liveness-exec ``` 输出结果显示还没有存活探测器失败: ```shell FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 24s 24s 1 {default-scheduler } Normal Scheduled Successfully assigned liveness-exec to worker0 23s 23s 1 {kubelet worker0} spec.containers{liveness} Normal Pulling pulling image "k8s.gcr.io/busybox" 23s 23s 1 {kubelet worker0} spec.containers{liveness} Normal Pulled Successfully pulled image "k8s.gcr.io/busybox" 23s 23s 1 {kubelet worker0} spec.containers{liveness} Normal Created Created container with docker id 86849c15382e; Security:[seccomp=unconfined] 23s 23s 1 {kubelet worker0} spec.containers{liveness} Normal Started Started container with docker id 86849c15382e ``` 35 秒之后,再来看 Pod 的事件: ```shell kubectl describe pod liveness-exec ``` 在输出结果的最下面,有信息显示存活探测器失败了,这个容器被杀死并且被重建了。 ```shell FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 37s 37s 1 {default-scheduler } Normal Scheduled Successfully assigned liveness-exec to worker0 36s 36s 1 {kubelet worker0} spec.containers{liveness} Normal Pulling pulling image "k8s.gcr.io/busybox" 36s 36s 1 {kubelet worker0} spec.containers{liveness} Normal Pulled Successfully pulled image "k8s.gcr.io/busybox" 36s 36s 1 {kubelet worker0} spec.containers{liveness} Normal Created Created container with docker id 86849c15382e; Security:[seccomp=unconfined] 36s 36s 1 {kubelet worker0} spec.containers{liveness} Normal Started Started container with docker id 86849c15382e 2s 2s 1 {kubelet worker0} spec.containers{liveness} Warning Unhealthy Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory ``` 再等另外 30 秒,检查看这个容器被重启了: ```shell kubectl get pod liveness-exec ``` 输出结果显示 `RESTARTS` 的值增加了 1。 ```shell NAME READY STATUS RESTARTS AGE liveness-exec 1/1 Running 1 1m ``` ## 定义一个存活态 HTTP 请求接口 {#define-a-liveness-HTTP-request} 另外一种类型的存活探测方式是使用 HTTP GET 请求。下面是一个 Pod 的配置文件,其中运行一个基于 `k8s.gcr.io/liveness` 镜像的容器。 {{< codenew file="pods/probe/http-liveness.yaml" >}} 在这个配置文件中,可以看到 Pod 也只有一个容器。`periodSeconds` 字段指定了 kubelet 每隔 3 秒执行一次存活探测。`initialDelaySeconds` 字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。kubelet 会向容器内运行的服务(服务会监听 8080 端口)发送一个 HTTP GET 请求来执行探测。如果服务上 `/healthz` 路径下的处理程序返回成功码,则 kubelet 认为容器是健康存活的。如果处理程序返回失败码,则 kubelet 会杀死这个容器并且重新启动它。 任何大于或等于 200 并且小于 400 的返回码标示成功,其它返回码都标示失败。 可以在这里看服务的源码 [server.go](https://github.com/kubernetes/kubernetes/blob/master/test/images/agnhost/liveness/server.go)。 容器存活的最开始 10 秒中,`/healthz` 处理程序返回一个 200 的状态码。之后处理程序返回 500 的状态码。 ```go 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 的存活检测: ```shell kubectl apply -f https://k8s.io/examples/pods/probe/http-liveness.yaml ``` 10 秒之后,通过看 Pod 事件来检测存活探测器已经失败了并且容器被重新启动了。 ```shell kubectl describe pod liveness-http ``` 在 1.13(包括 1.13版本)之前的版本中,如果在 Pod 运行的节点上设置了环境变量 `http_proxy`(或者 `HTTP_PROXY`),HTTP 的存活探测会使用这个代理。在 1.13 之后的版本中,设置本地的 HTTP 代理环境变量不会影响 HTTP 的存活探测。 ## 定义 TCP 的存活探测 {#define-a-TCP-liveness-probe} 第三种类型的存活探测是使用 TCP 套接字。通过配置,kubelet 会尝试在指定端口和容器建立套接字链接。如果能建立链接,这个容器就被看作是健康的,如果不能则这个容器就被看作是有问题的。 {{< codenew file="pods/probe/tcp-liveness-readiness.yaml" >}} 如你所见,TCP 检测的配置和 HTTP 检测非常相似。下面这个例子同时使用就绪和存活探测器。kubelet 会在容器启动 5 秒后发送第一个就绪探测。这会尝试连接 `goproxy` 容器的 8080 端口。如果探测成功,这个 Pod 会被标记为就绪状态,kubelet 将继续每隔 10 秒运行一次检测。 除了就绪探测,这个配置包括了一个存活探测。kubelet 会在容器启动 15 秒后进行第一次存活探测。就像就绪探测一样,会尝试连接 `goproxy` 容器的 8080 端口。如果存活探测失败,这个容器会被重新启动。 ```shell kubectl apply -f https://k8s.io/examples/pods/probe/tcp-liveness-readiness.yaml ``` 15 秒之后,通过看 Pod 事件来检测存活探测器: ```shell kubectl describe pod goproxy ``` ## 使用命名端口 {#use-a-named-port} 对于 HTTP 或者 TCP 存活检测可以使用命名的[容器端口](/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#containerport-v1-core)。 ```yaml ports: - name: liveness-port containerPort: 8080 hostPort: 8080 livenessProbe: httpGet: path: /healthz port: liveness-port ``` ## 使用启动探测器保护慢启动容器 {#define-startup-probes} 有时候,会有一些现有的应用程序在启动时需要较多的初始化时间。要不影响对引起探测死锁的快速响应,这种情况下,设置存活探测参数是要技巧的。技巧就是使用一个命令来设置启动探测,针对HTTP 或者 TCP 检测,可以通过设置 `failureThreshold * periodSeconds` 参数来保证有足够长的时间应对糟糕情况下的启动时间。 所以,前面的例子就变成了: ```yaml ports: - name: liveness-port containerPort: 8080 hostPort: 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` 来设置 Pod 状态。 ## 定义就绪探测器 {#define-readiness-probes} 有时候,应用程序会暂时性的不能提供通信服务。例如,应用程序在启动时可能需要加载很大的数据或配置文件,或是启动后要依赖等待外部服务。在这种情况下,既不想杀死应用程序,也不想给它发送请求。Kubernetes 提供了就绪探测器来发现并缓解这些情况。容器所在 Pod 上报还未就绪的信息,并且不接受通过 Kubernetes Service 的流量。 {{< note >}} 就绪探测器在容器的整个生命周期中保持运行状态。 {{< /note >}} 就绪探测器的配置和存活探测器的配置相似。唯一区别就是要使用 `readinessProbe` 字段,而不是 `livenessProbe` 字段。 ```yaml readinessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5 ``` HTTP 和 TCP 的就绪探测器配置也和存活探测器的配置一样的。 就绪和存活探测可以在同一个容器上并行使用。两者都用可以确保流量不会发给还没有准备好的容器,并且容器会在它们失败的时候被重新启动。 ## 配置探测器 {#configure-probes} {{< comment >}} 最后,本节的一些内容可以放到某个概念主题里。 {{< /comment >}} [探测器](/zh/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#probe-v1-core)有很多配置字段,可以使用这些字段精确的控制存活和就绪检测的行为: * `initialDelaySeconds`:容器启动后要等待多少秒后存活和就绪探测器才被初始化,默认是 0 秒,最小值是 0。 * `periodSeconds`:执行探测的时间间隔(单位是秒)。默认是 10 秒。最小值是 1。 * `timeoutSeconds`:探测的超时后等待多少秒。默认值是 1 秒。最小值是 1。 * `successThreshold`:探测器在失败后,被视为成功的最小连续成功数。默认值是 1。存活探测的这个值必须是 1。最小值是 1。 * `failureThreshold`:当探测失败时,Kubernetes 的重试次数。存活探测情况下的放弃就意味着重新启动容器。就绪探测情况下的放弃 Pod 会被打上未就绪的标签。默认值是 3。最小值是 1。 [HTTP 探测器](/zh/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#httpgetaction-v1-core)可以在 `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`。 对于一次 TCP 探测,kubelet 在节点上(不是在 Pod 里面)建立探测连接,这意味着你不能在 `host` 参数上配置 service name,因为 kubelet 不能解析 service name。 ## {{% heading "whatsnext" %}} * 进一步了解[容器探测器](/docs/concepts/workloads/pods/pod-lifecycle/#container-probes)。 ### 参考 {#reference} * [Pod](/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#pod-v1-core) * [容器](/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#container-v1-core) * [探测器](/docs/reference/generated/kubernetes-api/{{< param "version" >}}/#probe-v1-core)