Kubernetes 中的健康检查机制

发布时间 2023-07-03 07:37:25作者: 人艰不拆_zmc

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)》这篇博文。

动图说明:https://storage.googleapis.com/gweb-cloudblog-publish/original_images/google-kubernetes-probe-livenessae14.GIF

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)》这篇博文。

动图说明: https://storage.googleapis.com/gweb-cloudblog-publish/original_images/google-kubernetes-probe-readiness6ktf.GIF

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 启动探针、存活探针和就绪探针对比

  1. 如果不特意配置这三种健康检查机制,Kubernetes 将采取的默认机制,即通过判断容器启动主进程的返回值是否为零来判断探测是否成功。
  2. startupProbe 完全复用了 livenessProbe,只是名字改了下,多了一种概念,startupProbe常用于慢启动程序,启动探针只在容器启动期间起作用,在容器成功启动后不会继续执行定期的监控检查。
  3. Liveness 探测和 Readiness 探测配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness 探测是重启容器;Readiness 探测则是将容器设置为不可用,不接收 Service 转发的请求。

  4. Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。

  5. 用 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 中的健康检查机制