概述
健康检查(Health Check)用于检测您的应用实例是否正常工作,是保障业务可用性的一种传统机制,一般用于负载均衡下的业务,如果实例的状态不符合预期,将会把该实例“摘除”,不承担业务流量。
Kubernetes中的健康检查使用存活性探针(liveness probes)和就绪性探针(readiness probes)来实现,service即为负载均衡,k8s保证 service 后面的 pod 都可用,是k8s中自愈能力的主要手段,基于这两种探测机制,可以实现如下需求:
- 异常实例自动剔除,并重启新实例
- 多种类型探针检测,保证异常pod不接入流量
- 不停机部署,更安全的滚动升级
目前支持的探测方式包括:
- HTTP
- TCP
- Exec命令
k8s 中的 示例配置如下:
探针类型
默认机制:
如果把 k8s 对 pod 的crash 状态判断也能称之为“健康检查”的话,那算是默认的健康检查机制了,
每个容器启动时都会执行一个主进程,如果进程退出返回码不是0,则认为容器异常,即pod异常,k8s 会根据restartPolicy策略选择是否杀掉 pod,再重新启动一个。
restartPolicy分为三种:
- Always:当容器终止退出后,总是重启容器,默认策略。
- Onfailure:当容器异常退出(退出码非0)时,才重启容器。
- Never:当容器终止退出时,才不重启容器。
存活探针
上面的默认机制中,容器进程返回值非0则认为容器发生故障,需要重启。但很多情况下服务出现问题,进程却没有退出,如系统超载 5xx 错误,资源死锁等。这种情况下就需要健康检查机制出场了
存活探针(Liveness probe):让Kubernetes知道你的应用程序是否健康,如果你的应用程序不健康,Kubernetes将删除Pod并启动一个新的替换它。这里的“健康”不再是进程状态,而是用户自定义探测方式:HTTP、TCP、Exec
举例说明:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness
spec:
restartPolicy: OnFailure
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -fr /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
启动进程首先创建文件 /tmp/healthy,30 秒后删除,在我们的设定中,如果 /tmp/healthy 文件存在,则认为容器处于正常状态,反正则发生故障。
livenessProbe 部分定义如何执行 Liveness 探测:
探测的方法是:通过 cat 命令检查 /tmp/healthy 文件是否存在。如果命令执行成功,返回值为0,Kubernetes 则认为本次 Liveness 探测成功;如果命令返回值非0,本次 Liveness 探测失败。
initialDelaySeconds: 10,指定容器启动 10s 之后开始执行 Liveness 探测,我们一般会根据应用启动的准备时间来设置。比如某个应用正常启动要花 30 秒,那么 initialDelaySeconds 的值就应该大于 30。
periodSeconds: 5, 指定每 5 秒执行一次 Liveness 探测。Kubernetes 如果连续执行 3 次 Liveness 探测均失败,则会杀掉并重启容器。3次是可以配置的,参数为failureThreshold,含义后面解释
使用上面的 yaml 创建 pod:
刚开始的 30s,健康检查能通过。
此时的events 显示正常
30s 后,日志会显示 /tmp/healthy 已经不存在,Liveness 探测失败。再过几十秒,几次探测都失败后,容器会被重启。events 中可以看到重试 了 3次探测,每次间隔 10s,单次探测的超时时间为 1s。
liveness 的配置来自 v1中的Probe资源,所有属性含义如下:
- httpGet:对应HTTPGetAction对象,属性包括:host、httpHeaders、path、port、scheme
- initialDelaySeconds:容器启动后开始探测之前需要等多少秒,如应用启动一般30s的话,就设置为 30s
- periodSeconds:执行探测的频率(多少秒执行一次)。默认为10秒。最小值为1。
- successThreshold:探针失败后,最少连续成功多少次才视为成功。默认值为1。最小值为1。
- failureThreshold:最少连续多少次失败才视为失败。默认值为3。最小值为1。
- timeoutSeconds:探测的超时时间,默认 1s,最小 1s
- tcpSocket:对应TCPSocketAction对象,TCPSocket指定端口。尚不支持TCP hook
- exec:对应ExecAction对象,需要执行的内容
动图说明:
探针执行方式
HTTP
HTTP探针可能是最常见的自定义Liveness探针类型。 即使您的应用程序不是HTTP服务,您也可以在应用程序内创建轻量级HTTP服务以响应Liveness探针。 Kubernetes去访问一个路径,如果它得到的是200或300范围内的HTTP响应,它会将应用程序标记为健康。 否则它被标记为不健康。
httpGet配置项:
- host:连接的主机名,默认连接到pod的IP。你可能想在http header中设置"Host"而不是使用IP。
- scheme:连接使用的schema,默认HTTP。
- path: 访问的HTTP server的path。
- httpHeaders:自定义请求的header。HTTP运行重复的header。
- port:访问的容器的端口名字或者端口号。端口号必须介于1和65535之间。
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- name: liveness
image: k8s.gcr.io/liveness
args:
- /server
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
Exec
对于Exec探针,Kubernetes则只是在容器内运行命令。 如果命令以退出代码0返回,则容器标记为健康。 否则,它被标记为不健康。 当您不能或不想运行HTTP服务时,此类型的探针则很有用,但是必须是运行可以检查您的应用程序是否健康的命令。
Exec 的配置项(exec):
- command:需要执行的命令,需要符合命令的格式
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: k8s.gcr.io/busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
TCP
最后一种类型的探针是TCP探针,Kubernetes尝试在指定端口上建立TCP连接。 如果它可以建立连接,则容器被认为是健康的;否则被认为是不健康的。
如果您有HTTP探针或Command探针不能正常工作的情况,TCP探测器会派上用场。 例如,gRPC或FTP服务是此类探测的主要候选者。
TCP 的配置项(tcpSocket):
- host:探测的主机,默认为本pod ip
- port:端口,1到65535
apiVersion: v1
kind: Pod
metadata:
name: goproxy
labels:
app: goproxy
spec:
containers:
- name: goproxy
image: k8s.gcr.io/goproxy:0.1
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
就绪探针
就绪探针(Readiness probe):让Kubernetes知道您的应用是否准备好其流量服务。 Kubernetes确保Readiness探针检测通过,然后允许服务将流量发送到Pod。 如果Readiness探针开始失败,Kubernetes将停止向该容器发送流量,直到它通过。 判断容器是否处于可用Ready状态, 达到ready状态表示pod可以接受请求, 如果不健康, 从service的后端endpoint列表中把pod隔离出去
用户通过 Liveness 探测可以告诉 Kubernetes 什么时候通过重启容器实现自愈;而就绪探针Readiness则是告诉 Kubernetes 什么时候可以将容器加入到 Service 负载均衡中,对外提供服务。
Readiness 探测的配置语法与 Liveness 探测完全一样,这里不再赘述。
如果连续 n 次 Readiness 探测均失败后,READY被设置为不可用。
status 为 running,但是 ready 数为 0/1
动图示例:
两种探针对比
Liveness 探测和 Readiness 探测是两种 Health Check 机制,如果不特意配置,Kubernetes 将对两种探测采取相同的默认行为,即通过判断容器启动进程的返回值是否为零来判断探测是否成功。
两种探测的配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness 探测是重启容器;Readiness 探测则是将容器设置为不可用,不接收 Service 转发的请求。
Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。
用 Liveness 探测判断容器是否需要重启以实现自愈;用 Readiness 探测判断容器是否已经准备好对外提供服务。Readiness可用于指定容器启动后,判断容器各服务是否已正常启动(如启动脚本执行后写指定内容至特定文件)
使用场景
扩缩容
对于生产环境中重要的应用都建议配置 Health Check,保证处理客户请求的容器都是准备就绪的 Service backend。如果 Liveness不通过,则应该缩掉异常 pod,重新启动新 pod
示例:
对于 http://[container_ip]:8080/healthy,应用则可以实现自己的判断逻辑,比如检查所依赖的数据库是否就绪,示例代码如下:
健康检查的步骤为:
- 容器启动 10 秒之后开始探测。
- 如果 http://[container_ip]:8080/healthy 返回代码不是 200-400,表示容器没有就绪,不接收 Service web-svc 的请求。
- 每隔 5 秒再探测一次。
- 直到返回代码为 200-400,表明容器已经就绪,然后将其加入到 web-svc 的负责均衡中,开始处理客户请求。
- 探测会继续以 5 秒的间隔执行,如果连续发生 3 次失败,容器又会从负载均衡中移除,直到下次探测成功重新加入。
滚动更新
现有一个正常运行的多副本应用,接下来对应用进行更新(比如使用更高版本的 image),Kubernetes 会启动新副本,然后发生了如下事件:
- 正常情况下新副本需要 10 秒钟完成准备工作,在此之前无法响应业务请求。
- 但由于人为配置错误,副本始终无法完成准备工作(比如无法连接后端数据库)。
如果没有配置健康检查,则有问题的新副本将替换老副本,导致集群服务异常。
如果正确配置了 Health Check,新副本只有通过了 Readiness 探测,才会被添加到 Service;如果没有通过探测,现有副本不会被全部替换,业务仍然正常进行。
实现原理
liveness 和 readiness 的探测都是由kubelet执行。
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。
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,认为探测成功。否则认为失败。
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
}
tcp探测是通过探测指定的端口。如果可以连接,则认为探测成功,否则认为失败。
其他
执行命令探测失败的原因主要可能是容器未成功启动,或者执行命令失败。当然也可能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是无法触发容器重启的。
因为二者的作用不同,在实际使用中,可以根据实际的需求将二者进行配合使用。
新探针:启动探针
目的:
对于慢启动容器来说,现有的健康检查机制不太好用
慢启动容器:指需要大量时间(一到几分钟)启动的容器。启动缓慢的原因可能有多种:
- 长时间的数据初始化:只有第一次启动会花费很多时间
- 负载很高:每次启动都花费很多时间
- 节点资源不足/过载:即容器启动时间取决于外部因素
这种容器的主要问题在于,在livenessProbe失败之前,应该给它们足够的时间来启动它们。对于这种问题,现有的机制的处理方式为:
- 方法一:livenessProbe中把
延迟初始时间initialDelaySeconds
设置的很长,以允许容器启动(即initialDelaySeconds大于平均启动时间)。虽然这样可以确保livenessProbe不会检测失败,但是不知道initialDelaySeconds应该配置为多少,启动时间不是一个固定值。另外,因为livenessProbe在启动过程还没运行,因此pod 得不到反馈,events 看不到内容,如果你initialDelaySeconds是 10 分钟,那这 10 分钟内你不知道在发生什么。 - 方法二:增加livenessProbe的失败次数。即failureThreshold*periodSeconds的乘积足够大,简单粗暴,同时容器在初次成功启动后,就算死锁或以其他方式挂起,livenessProbe也会不断探测
方法二可以解决这个问题,但不够优雅。
因为livenessProbe的设计是为了在 pod 启动成功后进行健康探测,最好前提是 pod 已经启动成功,否则启动阶段的多次失败是没有意义的,因此官方提出了一种新的探针:即startupProbe,startupProbe并不是一种新的数据结构,他完全复用了livenessProbe,只是名字改了下,多了一种概念,关于这个 probe 的提议讨论可以参考issue
使用方式:startup-probes
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
这个配置的含义是:
startupProbe首先检测,该应用程序最多有5分钟(30 * 10 = 300s)完成启动。一旦startupProbe成功一次,livenessProbe将接管,以对后续运行过程中容器死锁提供快速响应。如果startupProbe从未成功,则容器将在300秒后被杀死。
k8s 1.16 才开始支持startupProbe这个特性
最佳实践
上述的扩缩容和滚动升级场景都需要用户对应用的健康检查足够了解,并且配置合适的策略。主要工作是:
- 给用户程序开发一个/healthy 接口,来获取世界可用状态(仅仅是 http服务)
- 定义合理的健康检查组合
注意事项:
- periodSeconds探测周期不能太短,否则会发送很多请求,也不能太长,否则会导致发现不了异常 pod
- 合理配置failureThreshold和successThreshold,否则会导致在 ready 和 not ready 直接反复摆动
改造服务
如果你的服务无法提供 http 的健康检查接口,可能需要修改你的业务代码:如 grpc 服务,后面会提到
sidecar 形式做健康检查
如果你不想更改你的业务逻辑,您的应用程序容器优没有公开HTTP接口以进行健康检查,那么您可以将另一个容器部署在pod内并调用您的应用程序的,即sidecar模式
之所以可行,是因为Pod的所有容器都在同一个环回接口(localhost)中。您无需将此端口暴露给外界。
Cli 工具
你的应用虽然不提供端点,但是可以通过exec 的方式执行容器内预装的 cli 工具来实现健康检查,其实就是脚本形式,变相的 exec
系统探针
你的应用不提供端点,但是可以侧面显示应用的使用状态,如 cpu内存使用率,通过系统指标来侧面反映服务的状态,如机器学习作业会使GPU升温,从而导致计算速度变慢。检测不通过,将作业移到其他节点可以解决此问题。
如何支持 gprc 的健康检查
GRPC正在成为云原生微服务之间通信的通用语言。如果您今天要将gRPC应用程序部署到Kubernetes,您可能想知道配置运行状况检查的最佳方法。在本文中,我们将讨论grpc-health-probe,一种Kubernetes本地健康检查gRPC应用程序的方法。
kubernetes本身不支持gRPC健康检查。这使得gRPC开发人员在部署到Kubernetes时有以下三种方法:
- httpGet probe: 不能与gRPC原生使用。您需要重构您的应用程序以同时提供gRPC和HTTP / 1.1协议(在不同的端口号上)。
- tcpSocket probe: 打开套接字到gRPC服务器是没有意义的,因为它无法读取响应正文。
- exec probe: 这会定期调用容器生态系统中的程序。对于gRPC,这意味着您自己实现健康RPC,然后使用编写客户端工具,并将客户端工具与容器打包到一起。
为了标准化上面提到的“exec探针”方法,我们需要:
- 标准的健康检查“协议”,可以轻松地在任何gRPC服务器中实现。
- 标准的健康检查“工具”,可以轻松查询健康协议。
得庆幸的是,gRPC有一个标准的健康检查协议。它可以从任何语言轻松使用。生成的代码和用于设置运行状况的实用程序几乎都在gRPC的所有语言实现中提供。
如果在gRPC应用程序中实现此运行状况检查协议,则可以使用标准/通用工具调用此Check()方法来确定服务器状态。
下面你需要的是“标准工具”,它是grpc-health-probe。
使用此工具,您可以在所有gRPC应用程序中使用相同的运行状况检查配置。这种方法需要你:
- 选择您喜欢的语言找到gRPC“health”模块并开始使用它(例如Go库)。
- 将grpc_health_probe二进制文件打到容器中。
- 配置Kubernetes“exec”探针以调用容器中的“grpc_health_probe”工具。
或者参考:https://github.com/americanexpress/grpc-k8s-health-check
参考
- https://www.youtube.com/watch?v=mxEvAPQRwhw
- https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-setting-up-health-checks-with-readiness-and-liveness-probes
- https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#execaction-v1-core
- https://xuxinkun.github.io/2019/10/28/liveness-readiness/
- https://yq.aliyun.com/articles/68567
- https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- https://www.ianlewis.org/en/kubernetes-health-checks-django
- https://ahmet.im/blog/advanced-kubernetes-health-checks/