Kubernetes 中的健康检查机制
1、概述
健康检查(Health Check)用于检测您的应用实例是否正常工作,是保障业务可用性的一种传统机制,一般用于负载均衡下的业务,如果实例的状态不符合预期,将会把该实例“摘除”,不承担业务流量。
Kubernetes中的健康检查使用存活性探针(liveness probes)和就绪性探针(readiness probes)来实现,service即为负载均衡,k8s保证 service 后面的 Pod 都可用,是k8s中自愈能力的主要手段,基于这两种探测机制,可以实现如下需求:
- 异常实例自动剔除,并重启新实例
- 多种类型探针检测,保证异常Pod不接入流量
- 不停机部署,更安全的滚动升级
目前支持的探测方式包括:
- HTTP
- TCP
- Exec命令
2、探针类型
2.1 默认机制
如果把 k8s 对 Pod 中容器的 crash 状态判断也能称之为“健康检查”的话,那算是默认的健康检查机制了,每个容器启动时都会执行一个主进程,如果进程退出返回码不是0,则认为容器异常,即Pod异常,k8s 会根据restartPolicy策略选择是否杀掉 Pod,再重新启动一个。
restartPolicy分为三种:
- Always:当容器终止退出后,总是重启容器,默认策略。
- Onfailure:当容器异常退出(退出码非0)时,才重启容器。
- Never:当容器终止退出时,才不重启容器。
更多默认机制相关内容,请参见《Kubernetes Pod重启策略》这篇博文。
2.2 健康检查机制
上面的默认机制中,容器进程返回值非0则认为容器发生故障,需要重启。但很多情况下服务出现问题,进程却没有退出,如系统超载 5xx 错误,资源死锁等。这种情况下就需要健康检查机制出场了。
2.2.1 存活探针
存活探针(Liveness probe):让Kubernetes知道你的应用程序是否健康,如果你的应用程序不健康,Kubernetes将启动一个新的替换它。这里的“健康”不再是进程状态,而是用户自定义探测方式:HTTP、TCP、Exec。
更多存活探针相关内容,请参见《Kubernetes存活探针(Liveness Probe)》这篇博文。
2.2.2 就绪探针
就绪探针(Readiness probe):让Kubernetes知道您的应用是否准备好其流量服务。 Kubernetes确保Readiness探针检测通过,然后允许服务将流量发送到Pod。 如果Readiness探针开始失败,Kubernetes将停止向该容器发送流量,直到它通过。 判断容器是否处于可用Ready状态, 达到Ready状态表示Pod可以接受请求, 如果不健康, 从service的后端endpoint列表中把Pod隔离出去。
用户通过 Liveness 探测可以告诉 Kubernetes 什么时候通过重启容器实现自愈;而就绪探针Readiness则是告诉 Kubernetes 什么时候可以将容器加入到 Service 负载均衡中,对外提供服务。
更多就绪探针相关内容,请参见《Kubernetes 就绪探针(Readiness Probe)》这篇博文。
2.2.3 启动探针
对于慢启动容器来说,存活探针和就绪探针这两种健康检查机制不太好用。
慢启动容器:指需要大量时间(一到几分钟)启动的容器。启动缓慢的原因可能有多种:
- 长时间的数据初始化:只有第一次启动会花费很多时间。
- 负载很高:每次启动都花费很多时间。
- 节点资源不足/过载:即容器启动时间取决于外部因素。
这种容器的主要问题在于,在存活探针失败之前,应该给它们足够的时间来启动它们。对于这种问题,现有的机制的处理方式为:
- 方法一:livenessProbe中把延迟初始时间 initialDelaySeconds 设置的很长,以允许容器启动(即initialDelaySeconds大于平均启动时间)。虽然这样可以确保 livenessProbe不会检测失败,但是不知道 initialDelaySeconds 应该配置为多少,因为启动时间不是一个固定值。另外,因为 livenessProbe 在启动过程还没运行,因此 Pod 得不到反馈,Events 看不到内容,如果 initialDelaySeconds 是 10 分钟,那这 10 分钟内你不知道在发生什么。
- 方法二:增加 livenessProbe 的失败次数。即 failureThreshold*periodSeconds 的乘积足够大,这样可以挺过容器启动,虽然简单粗暴,但是在容器在初次成功启动后,由于livenessProbe 的失败次数过大,就算容器死锁或以其他方式挂起,livenessProbe 也会不断探测到,这样就失去了存活检查的意义。
注意 1: livenessProbe的设计是为了在 Pod 启动成功后进行健康探测,最好前提是 Pod 已经启动成功,在启动阶段的多次失败是没有意义的。
以上两种方式都可以对慢启动容器进行监控检查,但都不够优雅。因此官方提出了一种新的探针:即startupProbe,startupProbe并不是一种新的数据结构,他完全复用了livenessProbe,只是名字改了下,多了一种概念。
启动探针使用方式:
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 YAMLCopy
这个配置的含义是:
startupProbe首先检测,该应用程序最多有5分钟(30 * 10 = 300s)完成启动。一旦startupProbe成功一次,livenessProbe将接管,以对后续运行过程中容器死锁提供快速响应。如果startupProbe从未成功,则容器将在300秒后被杀死。
k8s 1.16 才开始支持startupProbe这个特性。
注意 1:k8s 如果Pod中的容器只配置了启动探针,那么在容器成功启动后启动探针还会定期监控检查吗?
启动探针的主要作用是在容器启动过程中进行初始的健康检查,以确定容器是否已经准备好接收流量。如果只配置了启动探针而没有配置存活探针或就绪探针,启动探针在容器成功启动后不会继续执行定期的监控检查。启动探针仅在容器启动过程中进行一次健康检查,用于确定容器是否已经准备好接收流量。
因此,如果你希望在容器成功启动后持续进行健康检查和监控,建议配置存活探针和/或就绪探针,以便 Kubernetes 可以定期检查容器的健康状态,并根据探针的结果采取相应的操作。
注意 2:k8s 如果Pod中的容器同时配置了启动探针、存活探针和就绪探针,那么容器启动过程中存活探针和就绪探针不会起作用?
在 Kubernetes 中,如果一个 Pod 中的容器同时配置了启动探针(Startup Probe)、存活探针(Liveness Probe)和就绪探针(Readiness Probe),那么在容器的启动过程中存活探针和就绪探针将不会起作用。
启动探针的目的是在容器启动过程中进行初始的健康检查,以确定容器是否已经准备好接收流量。启动探针在容器启动后执行一次健康检查,然后根据探针的结果来确定容器是否成功启动。
一旦启动探针成功,并且容器被认为已经成功启动,存活探针和就绪探针才会开始起作用。存活探针定期检查容器是否保持存活状态,而就绪探针定期检查容器是否已经准备好接收流量。
因此,在容器的启动过程中,存活探针和就绪探针将被暂时忽略,直到启动探针成功为止。一旦启动探针成功,存活探针和就绪探针将开始定期监控容器的健康状态。
需要注意的是,启动探针只在容器启动期间起作用,一旦容器成功启动,存活探针和就绪探针将持续定期监控容器的健康状态,以保证容器的可用性和就绪状态。
2.2.4 启动探针、存活探针和就绪探针对比
- 如果不特意配置这三种健康检查机制,Kubernetes 将采取的默认机制,即通过判断容器启动主进程的返回值是否为零来判断探测是否成功。
- startupProbe 完全复用了 livenessProbe,只是名字改了下,多了一种概念,startupProbe常用于慢启动程序,启动探针只在容器启动期间起作用,在容器成功启动后不会继续执行定期的监控检查。
-
Liveness 探测和 Readiness 探测配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness 探测是重启容器;Readiness 探测则是将容器设置为不可用,不接收 Service 转发的请求。
-
Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。
-
用 Liveness 探测判断容器是否需要重启以实现自愈;用 Readiness 探测判断容器是否已经准备好对外提供服务。Readiness可用于指定容器启动后,判断容器各服务是否已正常启动(如启动脚本执行后写指定内容至特定文件)。
3、实现原理
startupProbe、liveness 和 readiness 的探测都是由kubelet执行。
3.1 exec方式
func (pb *prober) runProbe(p *v1.Probe, Pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID) (probe.Result, string, error) { ..... command := kubecontainer.ExpandContainerCommandOnlyStatic(p.Exec.Command, container.Env) return pb.exec.Probe(pb.newExecInContainer(container, containerID, command, timeout)) ...... func (pb *prober) newExecInContainer(container v1.Container, containerID kubecontainer.ContainerID, cmd []string, timeout time.Duration) exec.Cmd { return execInContainer{func() ([]byte, error) { return pb.runner.RunInContainer(containerID, cmd, timeout) }} } ...... func (m *kubeGenericRuntimeManager) RunInContainer(id kubecontainer.ContainerID, cmd []string, timeout time.Duration) ([]byte, error) { stdout, stderr, err := m.runtimeService.ExecSync(id.ID, cmd, 0) return append(stdout, stderr...), err }
由kubelet,通过CRI接口的ExecSync接口,在对应容器内执行拼装好的cmd命令。获取返回值。
func (pr execProber) Probe(e exec.Cmd) (probe.Result, string, error) { data, err := e.CombinedOutput() glog.V(4).Infof("Exec probe response: %q", string(data)) if err != nil { exit, ok := err.(exec.ExitError) if ok { if exit.ExitStatus() == 0 { return probe.Success, string(data), nil } else { return probe.Failure, string(data), nil } } return probe.Unknown, "", err } return probe.Success, string(data), nil }
kubelet是根据执行命令的退出码来决定是否探测成功。当执行命令的退出码为0时,认为执行成功,否则为执行失败。如果执行超时,则状态为Unknown。
3.2 http探测
func DoHTTPProbe(url *url.URL, headers http.Header, client HTTPGetInterface) (probe.Result, string, error) { req, err := http.NewRequest("GET", url.String(), nil) ...... if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest { glog.V(4).Infof("Probe succeeded for %s, Response: %v", url.String(), *res) return probe.Success, body, nil } ......
http探测是通过kubelet请求容器的指定url,并根据response来进行判断。 当返回的状态码在200到400(不含400)之间时,也就是状态码为2xx和3xx,认为探测成功。否则认为失败。
3.3 tcp探测
func DoTCPProbe(addr string, timeout time.Duration) (probe.Result, string, error) { conn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { // Convert errors to failures to handle timeouts. return probe.Failure, err.Error(), nil } err = conn.Close() if err != nil { glog.Errorf("Unexpected error closing TCP probe socket: %v (%#v)", err, err) } return probe.Success, "", nil } GolangCopy
tcp探测是通过探测指定的端口。如果可以连接,则认为探测成功,否则认为失败。
4、其他
执行命令探测失败的原因主要可能是容器未成功启动,或者执行命令失败。当然也可能docker或者docker-shim存在故障。
由于http和tcp都是从kubelet自node节点上发起的,向容器的ip进行探测。 所以探测失败的原因除了应用容器的问题外,还可能是从node到容器ip的网络不通。
readiness检查结果会通过SetContainerReadiness函数,设置到Pod的status中,从而更新Pod的ready condition。
liveness和readiness除了最终的作用不同,另外一个很大的区别是它们的初始值不同。
switch probeType { case readiness: w.spec = container.ReadinessProbe w.resultsManager = m.readinessManager w.initialValue = results.Failure case liveness: w.spec = container.LivenessProbe w.resultsManager = m.livenessManager w.initialValue = results.Success }
liveness的初始值为成功。这样防止在应用还没有成功启动前,就被误杀。如果在规定时间内还未成功启动,才将其设置为失败,从而触发容器重建。
而readiness的初始值为失败。这样防止应用还没有成功启动前就向应用进行流量的导入。如果在规定时间内启动成功,才将其设置为成功,从而将流量向应用导入。
liveness与readiness二者作用不能相互替代。
例如只配置了liveness,那么在容器启动,应用还没有成功就绪之前,这个时候Pod是ready的(因为容器成功启动了)。那么流量就会被引入到容器的应用中,可能会导致请求失败。虽然在liveness检查失败后,重启容器,此时Pod的ready的condition会变为false。但是前面会有一些流量因为错误状态导入。
当然只配置了readiness是无法触发容器重启的。
因为二者的作用不同,在实际使用中,可以根据实际的需求将二者进行配合使用。
5、最佳实践
对于生产环境中重要的应用都建议配置 Health Check,保证处理客户请求的容器都是准备就绪的 Service backend。如果 Liveness不通过,则应该缩掉异常 Pod,重新启动新 Pod 。
注意事项:
- periodSeconds探测周期不能太短,否则会发送很多请求,也不能太长,否则会导致发现不了异常 Pod
- 合理配置failureThreshold和successThreshold,否则会导致在 ready 和 not ready 直接反复摆动
参考:K8S 中的健康检查机制