从零开始入门-K8s-可观测性你的应用健康吗

80次阅读

共计 9473 个字符,预计需要花费 24 分钟才能阅读完成。

作者 | 莫源 阿里巴巴技术专家

一、需求来源

首先来看一下,整个需求的来源:当把应用迁移到 Kubernetes 之后,要如何去保障应用的健康与稳定呢?其实很简单,可以从两个方面来进行增强:

首先是提高应用的可观测性;

第二是提高应用的可恢复能力。

从可观测性上来讲,可以在三个方面来去做增强:

首先是应用的健康状态上面,可以实时地进行观测;

第二个是可以获取应用的资源使用情况;

第三个是可以拿到应用的实时日志,进行问题的诊断与分析。

当出现了问题之后,首先要做的事情是要降低影响的范围,进行问题的调试与诊断。最后当出现问题的时候,理想的状况是:可以通过和 K8s 集成的自愈机制进行完整的恢复。

二、Liveness 与 Readiness

本小节为大家介绍 Liveness probe 和 eadiness probe。

应用健康状态 - 初识 Liveness 与 Readiness

Liveness probe 也叫就绪指针,用来判断一个 pod 是否处在就绪状态。当一个 pod 处在就绪状态的时候,它才能够对外提供相应的服务,也就是说接入层的流量才能打到相应的 pod。当这个 pod 不处在就绪状态的时候,接入层会把相应的流量从这个 pod 上面进行摘除。

来看一下简单的一个例子:

如下图其实就是一个 Readiness 就绪的一个例子:

当这个 pod 指针判断一直处在失败状态的时候,其实接入层的流量不会打到现在这个 pod 上。

当这个 pod 的状态从 FAIL 的状态转换成 success 的状态时,它才能够真实地承载这个流量。

Liveness 指针也是类似的,它是存活指针,用来判断一个 pod 是否处在存活状态。当一个 pod 处在不存活状态的时候,会出现什么事情呢?

这个时候会由上层的判断机制来判断这个 pod 是否需要被重新拉起。那如果上层配置的重启策略是 restart always 的话,那么此时这个 pod 会直接被重新拉起。

应用健康状态 - 使用方式

接下来看一下 Liveness 指针和 Readiness 指针的具体的用法。

探测方式

Liveness 指针和 Readiness 指针支持三种不同的探测方式:

第一种是 httpGet。它是通过发送 http Get 请求来进行判断的,当返回码是 200-399 之间的状态码时,标识这个应用是健康的;

第二种探测方式是 Exec。它是通过执行容器中的一个命令来判断当前的服务是否是正常的,当命令行的返回结果是 0,则标识容器是健康的;

第三种探测方式是 tcpSocket。它是通过探测容器的 IP 和 Port 进行 TCP 健康检查,如果这个 TCP 的链接能够正常被建立,那么标识当前这个容器是健康的。

探测结果

从探测结果来讲主要分为三种:

第一种是 success,当状态是 success 的时候,表示 container 通过了健康检查,也就是 Liveness probe 或 Readiness probe 是正常的一个状态;

第二种是 Failure,Failure 表示的是这个 container 没有通过健康检查,如果没有通过健康检查的话,那么此时就会进行相应的一个处理,那在 Readiness 处理的一个方式就是通过 service。service 层将没有通过 Readiness 的 pod 进行摘除,而 Liveness 就是将这个 pod 进行重新拉起,或者是删除。

第三种状态是 Unknown,Unknown 是表示说当前的执行的机制没有进行完整的一个执行,可能是因为类似像超时或者像一些脚本没有及时返回,那么此时 Readiness-probe 或 Liveness-probe 会不做任何的一个操作,会等待下一次的机制来进行检验。

那在 kubelet 里面有一个叫 ProbeManager 的组件,这个组件里面会包含 Liveness-probe 或 Readiness-probe,这两个 probe 会将相应的 Liveness 诊断和 Readiness 诊断作用在 pod 之上,来实现一个具体的判断。

应用健康状态 -Pod Probe Spec

下面介绍这三种方式不同的检测方式的一个 yaml 文件的使用。

首先先看一下 exec,exec 的使用其实非常简单。如下图所示,大家可以看到这是一个 Liveness probe,它里面配置了一个 exec 的一个诊断。接下来,它又配置了一个 command 的字段,这个 command 字段里面通过 cat 一个具体的文件来判断当前 Liveness probe 的状态,当这个文件里面返回的结果是 0 时,或者说这个命令返回是 0 时,它会认为此时这个 pod 是处在健康的一个状态。

那再来看一下这个 httpGet,httpGet 里面有一个字段是路径,第二个字段是 port,第三个是 headers。这个地方有时需要通过类似像 header 头的一个机制做 health 的一个判断时,需要配置这个 header,通常情况下,可能只需要通过 health 和 port 的方式就可以了。

第三种是 tcpSocket,tcpSocket 的使用方式其实也比较简单,你只需要设置一个检测的端口,像这个例子里面使用的是 8080 端口,当这个 8080 端口 tcp connect 审核正常被建立的时候,那 tecSocket,Probe 会认为是健康的一个状态。

此外还有如下的五个参数,是 Global 的参数。

第一个参数叫 initialDelaySeconds,它表示的是说这个 pod 启动延迟多久进行一次检查,比如说现在有一个 Java 的应用,它启动的时间可能会比较长,因为涉及到 jvm 的启动,包括 Java 自身 jar 的加载。所以前期,可能有一段时间是没有办法被检测的,而这个时间又是可预期的,那这时可能要设置一下 initialDelaySeconds;

第二个是 periodSeconds,它表示的是检测的时间间隔,正常默认的这个值是 10 秒;

第三个字段是 timeoutSeconds,它表示的是检测的超时时间,当超时时间之内没有检测成功,那它会认为是失败的一个状态;

第四个是 successThreshold,它表示的是:当这个 pod 从探测失败到再一次判断探测成功,所需要的阈值次数,默认情况下是 1 次,表示原本是失败的,那接下来探测这一次成功了,就会认为这个 pod 是处在一个探针状态正常的一个状态;

最后一个参数是 failureThreshold,它表示的是探测失败的重试次数,默认值是 3,表示的是当从一个健康的状态连续探测 3 次失败,那此时会判断当前这个 pod 的状态处在一个失败的状态。

应用健康状态 -Liveness 与 Readiness 总结

接下来对 Liveness 指针和 Readiness 指针进行一个简单的总结。

介绍

Liveness 指针是存活指针,它用来判断容器是否存活、判断 pod 是否 running。如果 Liveness 指针判断容器不健康,此时会通过 kubelet 杀掉相应的 pod,并根据重启策略来判断是否重启这个容器。如果默认不配置 Liveness 指针,则默认情况下认为它这个探测默认返回是成功的。

Readiness 指针用来判断这个容器是否启动完成,即 pod 的 condition 是否 ready。如果探测的一个结果是不成功,那么此时它会从 pod 上 Endpoint 上移除,也就是说从接入层上面会把前一个 pod 进行摘除,直到下一次判断成功,这个 pod 才会再次挂到相应的 endpoint 之上。

检测失败

对于检测失败上面来讲 Liveness 指针是直接杀掉这个 pod,而 Readiness 指针是切掉 endpoint 到这个 pod 之间的关联关系,也就是说它把这个流量从这个 pod 上面进行切掉。

适用场景

Liveness 指针适用场景是支持那些可以重新拉起的应用,而 Readiness 指针主要应对的是启动之后无法立即对外提供服务的这些应用。

注意事项

在使用 Liveness 指针和 Readiness 指针的时候有一些注意事项。因为不论是 Liveness 指针还是 Readiness 指针都需要配置合适的探测方式,以免被误操作。

第一个是调大超时的阈值,因为在容器里面执行一个 shell 脚本,它的执行时长是非常长的,平时在一台 ecs 或者在一台 vm 上执行,可能 3 秒钟返回的一个脚本在容器里面需要 30 秒钟。所以这个时间是需要在容器里面事先进行一个判断的,那如果可以调大超时阈值的方式,来防止由于容器压力比较大的时候出现偶发的超时;

第二个是调整判断的一个次数,3 次的默认值其实在比较短周期的判断周期之下,不一定是最佳实践,适当调整一下判断的次数也是一个比较好的方式;

第三个是 exec,如果是使用 shell 脚本的这个判断,调用时间会比较长,比较建议大家可以使用类似像一些编译性的脚本 Golang 或者一些 C 语言、C++ 编译出来的这个二进制的 binary 进行判断,那这种通常会比 shell 脚本的执行效率高 30% 到 50%;

第四个是如果使用 tcpSocket 方式进行判断的时候,如果遇到了 TLS 的服务,那可能会造成后边 TLS 里面有很多这种未健全的 tcp connection,那这个时候需要自己对业务场景上来判断,这种的链接是否会对业务造成影响。

三、问题诊断

接下来给大家讲解一下在 K8s 中常见的问题诊断。

应用故障排查 - 了解状态机制

首先要了解一下 K8s 中的一个设计理念,就是这个状态机制。因为 K8s 是整个的一个设计是面向状态机的,它里面通过 yaml 的方式来定义的是一个期望到达的一个状态,而真正这个 yaml 在执行过程中会由各种各样的 controller 来负责整体的状态之间的一个转换。

比如说上面的图,实际上是一个 Pod 的一个生命周期。刚开始它处在一个 pending 的状态,那接下来可能会转换到类似像 running,也可能转换到 Unknown,甚至可以转换到 failed。然后,当 running 执行了一段时间之后,它可以转换到类似像 successded 或者是 failed,然后当出现在 unknown 这个状态时,可能由于一些状态的恢复,它会重新恢复到 running 或者 successded 或者是 failed。

其实 K8s 整体的一个状态就是基于这种类似像状态机的一个机制进行转换的,而不同状态之间的转化都会在相应的 K8s 对象上面留下来类似像 Status 或者像 Conditions 的一些字段来进行表示。

像下面这张图其实表示的就是说在一个 Pod 上面一些状态位的一些展现。

比如说在 Pod 上面有一个字段叫 Status,这个 Status 表示的是 Pod 的一个聚合状态,在这个里面,这个聚合状态处在一个 pending 状态。

然后再往下看,因为一个 pod 里面有多个 container,每个 container 上面又会有一个字段叫 State,然后 State 的状态表示当前这个 container 的一个聚合状态。那在这个例子里面,这个聚合状态处在的是 waiting 的状态,那具体的原因是因为什么呢?是因为它的镜像没有拉下来,所以处在 waiting 的状态,是在等待这个镜像拉取。然后这个 ready 的部分呢,目前是 false,因为它这个进行目前没有拉取下来,所以这个 pod 不能够正常对外服务,所以此时 ready 的状态是未知的,定义为 false。如果上层的 endpoint 发现底层这个 ready 不是 true 的话,那么此时这个服务是没有办法对外服务的。

再往下是 condition,condition 这个机制表示是说:在 K8s 里面有很多这种比较小的这个状态,而这个状态之间的聚合会变成上层的这个 Status。那在这个例子里面有几个状态,第一个是 Initialized,表示是不是已经初始化完成?那在这个例子里面已经是初始化完成的,那它走的是第二个阶段,是在这个 ready 的状态。因为上面几个 container 没有拉取下来相应的镜像,所以 ready 的状态是 false。

然后再往下可以看到这个 container 是否 ready,这里可以看到是 false,而这个状态是 PodScheduled,表示说当前这个 pod 是否是处在一个已经被调度的状态,它已经 bound 在现在这个 node 之上了,所以这个状态也是 true。

那可以通过相应的 condition 是 true 还是 false 来判断整体上方的这个状态是否是正常的一个状态。而在 K8s 里面不同的状态之间的这个转换都会发生相应的事件,而事件分为两种:一种叫做 normal 的事件,一种是 warning 事件。大家可以看见在这第一条的事件是有个 normal 事件,然后它相应的 reason 是 scheduler,表示说这个 pod 已经被默认的调度器调度到相应的一个节点之上,然后这个节点是 cn-beijing192.168.3.167 这个节点之上。

再接下来,又是一个 normal 的事件,表示说当前的这个镜像在 pull 相应的这个 image。然后再往下是一个 warning 事件,这个 warning 事件表示说 pull 这个镜像失败了。

以此类推,这个地方表示的一个状态就是说在 K8s 里面这个状态机制之间这个状态转换会产生相应的事件,而这个事件又通过类似像 normal 或者是 warning 的方式进行暴露。开发者可以通过类似像通过这个事件的机制,可以通过上层 condition Status 相应的一系列的这个字段来判断当前这个应用的具体的状态以及进行一系列的诊断。

应用故障排查 - 常见应用异常

本小节介绍一下常见应用的一些异常。首先是 pod 上面,pod 上面可能会停留几个常见的状态。

Pod 停留在 Pending

第一个就是 pending 状态,pending 表示调度器没有进行介入。此时可以通过 kubectl describe pod 来查看相应的事件,如果由于资源或者说端口占用,或者是由于 node selector 造成 pod 无法调度的时候,可以在相应的事件里面看到相应的结果,这个结果里面会表示说有多少个不满足的 node,有多少是因为 CPU 不满足,有多少是由于 node 不满足,有多少是由于 tag 打标造成的不满足。

Pod 停留在 waiting

那第二个状态就是 pod 可能会停留在 waiting 的状态,pod 的 states 处在 waiting 的时候,通常表示说这个 pod 的镜像没有正常拉取,原因可能是由于这个镜像是私有镜像,但是没有配置 Pod secret;那第二种是说可能由于这个镜像地址是不存在的,造成这个镜像拉取不下来;还有一个是说这个镜像可能是一个公网的镜像,造成镜像的拉取失败。

Pod 不断被拉取并且可以看到 crashing

第三种是 pod 不断被拉起,而且可以看到类似像 backoff。这个通常表示说 pod 已经被调度完成了,但是启动失败,那这个时候通常要关注的应该是这个应用自身的一个状态,并不是说配置是否正确、权限是否正确,此时需要查看的应该是 pod 的具体日志。

Pod 处在 Runing 但是没有正常工作

第四种 pod 处在 running 状态,但是没有正常对外服务。那此时比较常见的一个点就可能是由于一些非常细碎的配置,类似像有一些字段可能拼写错误,造成了 yaml 下发下去了,但是有一段没有正常地生效,从而使得这个 pod 处在 running 的状态没有对外服务,那此时可以通过 apply-validate-f pod.yaml 的方式来进行判断当前 yaml 是否是正常的,如果 yaml 没有问题,那么接下来可能要诊断配置的端口是否是正常的,以及 Liveness 或 Readiness 是否已经配置正确。

Service 无法正常的工作

最后一种就是 service 无法正常工作的时候,该怎么去判断呢?那比较常见的 service 出现问题的时候,是自己的使用上面出现了问题。因为 service 和底层的 pod 之间的关联关系是通过 selector 的方式来匹配的,也就是说 pod 上面配置了一些 label,然后 service 通过 match label 的方式和这个 pod 进行相互关联。如果这个 label 配置的有问题,可能会造成这个 service 无法找到后面的 endpoint,从而造成相应的 service 没有办法对外提供服务,那如果 service 出现异常的时候,第一个要看的是这个 service 后面是不是有一个真正的 endpoint,其次来看这个 endpoint 是否可以对外提供正常的服务。

四、应用远程调试

本节讲解的是在 K8s 里面如何进行应用的远程调试,远程调试主要分为 pod 的远程调试以及 service 的远程调试。还有就是针对一些性能优化的远程调试。

应用远程调试 – Pod 远程调试

首先把一个应用部署到集群里面的时候,发现问题的时候,需要进行快速验证,或者说修改的时候,可能需要类似像登陆进这个容器来进行一些诊断。

比如说可以通过 exec 的方式进入一个 pod。像这条命令里面,通过 kubectl exec-it pod-name 后面再填写一个相应的命令,比如说 /bin/bash,表示希望到这个 pod 里面进入一个交互式的一个 bash。然后在 bash 里面可以做一些相应的命令,比如说修改一些配置,通过 supervisor 去重新拉起这个应用,都是可以的。

那如果指定这一个 pod 里面可能包含着多个 container,这个时候该怎么办呢?怎么通过 pod 来指定 container 呢?其实这个时候有一个参数叫做 -c,如上图下方的命令所示。-c 后面是一个 container-name,可以通过 pod 在指定 -c 到这个 container-name,具体指定要进入哪个 container,后面再跟上相应的具体的命令,通过这种方式来实现一个多容器的命令的一个进入,从而实现多容器的一个远程调试。

应用远程调试 – Servic 远程调试

那么 service 的远程调试该怎么做呢?service 的远程调试其实分为两个部分:

第一个部分是说我想将一个服务暴露到远程的一个集群之内,让远程集群内的一些应用来去调用本地的一个服务,这是一条反向的一个链路;

还有一种方式是我想让这个本地服务能够去调远程的服务,那么这是一条正向的链路。

在反向列入上面有这样一个开源组件,叫做 Telepresence,它可以将本地的应用代理到远程集群中的一个 service 上面,使用它的方式非常简单。

首先先将 Telepresence 的一个 Proxy 应用部署到远程的 K8s 集群里面。然后将远程单一个 deployment swap 到本地的一个 application,使用的命令就是 Telepresence-swap-deployment 然后以及远程的 DEPLOYMENT_NAME。通过这种方式就可以将本地一个 application 代理到远程的 service 之上、可以将应用在远程集群里面进行本地调试,这个有兴趣的同学可以到 GitHub 上面来看一下这个插件的使用的方式。

第二个是如果本地应用需要调用远程集群的服务时候,可以通过 port-forward 的方式将远程的应用调用到本地的端口之上。比如说现在远程的里面有一个 API server,这个 API server 提供了一些端口,本地在调试 Code 时候,想要直接调用这个 API server,那么这时,比较简单的一个方式就是通过 port-forward 的方式。

它的使用方式是 kubectl port-forward,然后 service 加上远程的 service name,再加上相应的 namespace,后面还可以加上一些额外的参数,比如说端口的一个映射,通过这种机制就可以把远程的一个应用代理到本地的端口之上,此时通过访问本地端口就可以访问远程的服务。

开源的调试工具 – kubectl-debug

最后再给大家介绍一个开源的调试工具,它也是 kubectl 的一个插件,叫 kubectl-debug。我们知道在 K8s 里面,底层的容器 runtime 比较常见的就是类似像 docker 或者是 containerd,不论是 docker 还是 containerd,它们使用的一个机制都是基于 Linux namespace 的一个方式进行虚拟化和隔离的。

通常情况下,并不会在镜像里面带特别多的调试工具,类似像 netstat telnet 等等这些,因为这个会造成应用整体非常冗余。那么如果想要调试的时候该怎么做呢?其实这个时候就可以依赖类似于像 kubectl-debug 这样一个工具。

kubectl-debug 这个工具是依赖于 Linux namespace 的方式来去做的,它可以 datash 一个 Linux namespace 到一个额外的 container,然后在这个 container 里面执行任何的 debug 动作,其实和直接去 debug 这个 Linux namespace 是一致的。这里有一个简单的操作,给大家来介绍一下:

这个地方其实已经安装好了 kubectl-debug,它是 kubectl 的一个插件。所以这个时候,你可以直接通过 kubectl-debug 这条命令来去诊断远程的一个 pod。像这个例子里面,当执行 debug 的时候,实际上它首先会先拉取一些镜像,这个镜像里面实际上会默认带一些诊断的工具。当这个镜像启用的时候,它会把这个 debug container 进行启动。与此同时会把这个 container 和相应的你要诊断的这个 container 的 namespace 进行挂靠,也就说此时这个 container 和你是同 namespace 的,类似像网络站,或者是类似像内核的一些参数,其实都可以在这个 debug container 里面实时地进行查看。

像这个例子里面,去查看类似像 hostname、进程、netstat 等等,这些其实都是和这个需要 debug 的 pod 是在同一个环境里面的,所以你之前这三条命令可以看到里面相关的信息。

如果此时进行 logout 的话,相当于会把相应的这个 debug pod 杀掉,然后进行退出,此时对应用实际上是没有任何的影响的。那么通过这种方式可以不介入到容器里面,就可以实现相应的一个诊断。

此外它还支持额外的一些机制,比如说我给设定一些 image,然后类似像这里面安装了的是 htop,然后开发者可以通过这个机制来定义自己需要的这个命令行的工具,并且通过这种 image 的方式设置进来。那么这个时候就可以通过这种机制来调试远程的一个 pod。

本节总结

关于 Liveness 和 Readiness 的指针。Liveness probe 就是保活指针,它是用来看 pod 是否存活的,而 Readiness probe 是就绪指针,它是判断这个 pod 是否就绪的,如果就绪了,就可以对外提供服务。这个就是 Liveness 和 Readiness 需要记住的部分;

应用诊断的三个步骤:首先 describe 相应的一个状态;然后提供状态来排查具体的一个诊断方向;最后来查看相应对象的一个 event 获取更详细的一个信息;

提供 pod 一个日志来定位应用的自身的一个状态;

远程调试的一个策略,如果想把本地的应用代理到远程集群,此时可以通过 Telepresence 这样的工具来实现,如果想把远程的应用代理到本地,然后在本地进行调用或者是调试,可以用类似像 port-forward 这种机制来实现。

“阿里巴巴云原生微信公众号(ID:Alicloudnative)关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术公众号。”

** 作者:一绿舟
**

原文链接:https://yq.aliyun.com/articles/720438?utm_content=g_1000080953

本文为云栖社区原创内容,未经允许不得转载。

正文完
 0

从零开始入门-K8s-可观测性你的应用健康吗

80次阅读

共计 9810 个字符,预计需要花费 25 分钟才能阅读完成。

一、基本知识

存储快照产生背景

在使用存储时,为了提高数据操作的容错性,我们通常有需要对线上数据进行 snapshot,以及能快速 restore 的能力。另外,当需要对线上数据进行快速的复制以及迁移等动作,如进行环境的复制、数据开发等功能时,都可以通过存储快照来满足需求,而 K8s 中通过 CSI Snapshotter controller 来实现存储快照的功能。

存储快照用户接口 -Snapshot

我们知道,K8s 中通过 pvc 以及 pv 的设计体系来简化用户对存储的使用,而存储快照的设计其实是仿照  pvc & pv 体系的设计思想。当用户需要存储快照的功能时,可以通过 VolumeSnapshot 对象来声明,并指定相应的 VolumeSnapshotClass 对象,之后由集群中的相关组件动态生成存储快照以及存储快照对应的对象 VolumeSnapshotContent。如下对比图所示,动态生成 VolumeSnapshotContent 和动态生成 pv 的流程是非常相似的。

存储快照用户接口 -Restore

有了存储快照之后,如何将快照数据快速恢复过来呢?如下图所示:

如上所示的流程,可以借助 PVC 对象将其的 dataSource 字段指定为 VolumeSnapshot 对象。这样当 PVC 提交之后,会由集群中的相关组件找到 dataSource 所指向的存储快照数据,然后新创建对应的存储以及 pv 对象,将存储快照数据恢复到新的 pv 中,这样数据就恢复回来了,这就是存储快照的 restore 用法。

Topolopy- 含义

首先了解一下拓扑是什么意思:这里所说的拓扑是 K8s 集群中为管理的 nodes 划分的一种“位置”关系,意思为:可以通过在 node 的 labels 信息里面填写某一个 node 属于某一个拓扑。
 
常见的有三种,这三种在使用时经常会遇到的:

  • 第一种,在使用云存储服务的时候,经常会遇到 region,也就是地区的概念,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区;
  • 第二种,比较常用的是可用区,也就是 available zone,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区;
  • 第三种,是 hostname,就是单机维度,是拓扑域为 node 范围,在 K8s 中常通过 label kubernetes.io/hostname 来标识,这个在文章的最后讲 local pv 的时候,会再详细描述。

上面讲到的三个拓扑是比较常用的,而拓扑其实是可以自己定义的。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的值其实就是拓扑域下不同的拓扑位置。

举个例子:可以用 rack,也就是机房中的机架这个纬度来做一个拓扑域。这样就可以将不同机架 (rack) 上面的机器标记为不同的拓扑位置,也就是说可以将不同机架上机器的位置关系通过 rack 这个纬度来标识。属于 rack1 上的机器,node label 中都添加 rack 的标识,它的 value 就标识成 rack1,即 rack=rack1;另外一组机架上的机器可以标识为 rack=rack2,这样就可以通过机架的纬度就来区分来 K8s 中的 node 所处的位置。

接下来就一起来看看拓扑在 K8s 存储中的使用。

存储拓扑调度产生背景

上一节课我们说过,K8s 中通过 PV 的 PVC 体系将存储资源和计算资源分开管理了。如果创建出来的 PV 有 ” 访问位置 ” 的限制,也就是说,它通过 nodeAffinity 来指定哪些 node 可以访问这个 PV。为什么会有这个访问位置的限制?

因为在 K8s 中创建 pod 的流程和创建 PV 的流程,其实可以认为是并行进行的,这样的话,就没有办法来保证 pod 最终运行的 node 是能访问到 有位置限制的 PV 对应的存储,最终导致 pod 没法正常运行。这里来举两个经典的例子:

首先来看一下 Local PV 的例子,Local PV 是将一个 node 上的本地存储封装为 PV,通过使用 PV 的方式来访问本地存储。为什么会有 Local PV 的需求呢?简单来说,刚开始使用 PV 或 PVC 体系的时候,主要是用来针对分布式存储的,分布式存储依赖于网络,如果某些业务对 I/O 的性能要求非常高,可能通过网络访问分布式存储没办法满足它的性能需求。这个时候需要使用本地存储,刨除了网络的 overhead,性能往往会比较高。但是用本地存储也是有坏处的!分布式存储可以通过多副本来保证高可用,但本地存储就需要业务自己用类似 Raft 协议来实现多副本高可用。

接下来看一下 Local PV 场景可能如果没有对 PV 做“访问位置”的限制会遇到什么问题?

当用户在提交完 PVC 的时候,K8s PV controller 可能绑定的是 node2 上面的 PV。但是,真正使用这个 PV 的 pod,在被调度的时候,有可能调度在 node1 上,最终导致这个 pod 在起来的时候没办法去使用这块存储,因为 pod 真实情况是要使用 node2 上面的存储。

第二个 (如果不对 PV 做“访问位置”的限制会出问题的) 场景:

如果搭建的 K8s 集群管理的 nodes 分布在单个区域多个可用区内。在创建动态存储的时候,创建出来的存储属于可用区 2,但之后在提交使用该存储的 pod,它可能会被调度到可用区 1 了,那就可能没办法使用这块存储。因此像阿里云的云盘,也就是块存储,当前不能跨可用区使用,如果创建的存储其实属于可用区 2,但是 pod 运行在可用区 1,就没办法使用这块存储,这是第二个常见的问题场景。

接下来我们来看看 K8s 中如何通过存储拓扑调度来解决上面的问题的。

存储拓扑调度

首先总结一下之前的两个问题,它们都是 PV 在给 PVC 绑定或者动态生成 PV 的时候,我并不知道后面将使用它的 pod 将调度在哪些 node 上。但 PV 本身的使用,是对 pod 所在的 node 有拓扑位置的限制的,如 Local PV 场景是我要调度在指定的 node 上我才能使用那块 PV,而对第二个问题场景就是说跨可用区的话,必须要在将使用该 PV 的 pod 调度到同一个可用区的 node 上才能使用阿里云云盘服务,那 K8s 中怎样去解决这个问题呢?

简单来说,在 K8s 中将 PV 和 PVC 的 binding 操作和动态创建 PV 的操作做了 delay,delay 到 pod 调度结果出来之后,再去做这两个操作。这样的话有什么好处?

  • 首先,如果要是所要使用的 PV 是预分配的,如 Local PV,其实使用这块 PV 的 pod 它对应的 PVC 其实还没有做绑定,就可以通过调度器在调度的过程中,结合 pod 的计算资源需求(如 cpu/mem) 以及 pod 的 PVC 需求,选择的 node 既要满足计算资源的需求又要 pod 使用的 pvc 要能 binding 的 pv 的 nodeaffinity 限制;
  • 其次对动态生成 PV 的场景其实就相当于是如果知道 pod 运行的 node 之后,就可以根据 node 上记录的拓扑信息来动态的创建这个 PV,也就是保证新创建出来的 PV 的拓扑位置与运行的 node 所在的拓扑位置是一致的,如上面所述的阿里云云盘的例子,既然知道 pod 要运行到可用区 1,那之后创建存储时指定在可用区 1 创建即可。

为了实现上面所说的延迟绑定和延迟创建 PV,需要在 K8s 中的改动涉及到的相关组件有三个:

  • PV Controller 也就是 persistent volume controller,它需要支持延迟 Binding 这个操作。
  • 另一个是动态生成 PV 的组件,如果 pod 调度结果出来之后,它要根据 pod 的拓扑信息来去动态的创建 PV。
  • 第三组件,也是最重要的一个改动点就是 kube-scheduler。在为 pod 选择 node 节点的时候,它不仅要考虑 pod 对 CPU/MEM 的计算资源的需求,它还要考虑这个 pod 对存储的需求,也就是根据它的 PVC,它要先去看一下当前要选择的 node,能否满足能和这个 PVC 能匹配的 PV 的 nodeAffinity;或者是动态生成 PV 的过程,它要根据 StorageClass 中指定的拓扑限制来 check 当前的 node 是不是满足这个拓扑限制,这样就能保证调度器最终选择出来的 node 就能满足存储本身对拓扑的限制。

这就是存储拓扑调度的相关知识。

二、用例解读

接下来通过 yaml 用例来解读一下第一部分的基本知识。

Volume Snapshot/Restore 示例

下面来看一下存储快照如何使用:首先需要集群管理员,在集群中创建 VolumeSnapshotClass 对象,VolumeSnapshotClass 中一个重要字段就是 Snapshot,它是指定真正创建存储快照所使用的卷插件,这个卷插件是需要提前部署的,稍后再说这个卷插件。

接下来用户他如果要做真正的存储快照,需要声明一个 VolumeSnapshotClass,VolumeSnapshotClass 首先它要指定的是 VolumeSnapshotClassName,接着它要指定的一个非常重要的字段就是 source,这个 source 其实就是指定快照的数据源是啥。这个地方指定 name 为 disk-pvc,也就是说通过这个 pvc 对象来创建存储快照。提交这个 VolumeSnapshot 对象之后,集群中的相关组件它会找到这个 PVC 对应的 PV 存储,对这个 PV 存储做一次快照。

有了存储快照之后,那接下来怎么去用存储快照恢复数据呢?这个其实也很简单,通过声明一个新的 PVC 对象并在它的 spec 下面的 DataSource 中来声明我的数据源来自于哪个 VolumeSnapshot,这里指定的是 disk-snapshot 对象,当我这个 PVC 提交之后,集群中的相关组件,它会动态生成新的 PV 存储,这个新的 PV 存储中的数据就来源于这个 Snapshot 之前做的存储快照。

Local PV 的示例

如下图看一下 Local PV 的 yaml 示例:

Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明 PV 对象,既然 Local PV 只能是本地访问,就需要在声明 PV 对象的,在 PV 对象中通过 nodeAffinity 来限制我这个 PV 只能在单 node 上访问,也就是给这个 PV 加上拓扑限制。如上图拓扑的 key 用 kubernetes.io/hostname 来做标记,也就是只能在 node1 访问。如果想用这个 PV,你的 pod 必须要调度到 node1 上。

既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding,这个时候你就要通过一种手段来告诉 PV controller,什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 no-provisioner,其实就是相当于告诉 K8s 它不会去动态创建 PV,它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer,可以先简单地认为它是延迟绑定。

当用户开始提交 PVC 的时候,pv controller 在看到这个 pvc 的时候,它会找到相应的 storageClass,发现这个 BindingMode 是延迟绑定,它就不会做任何事情。

之后当真正使用这个 pvc 的 pod,在调度的时候,当它恰好调度在符合 pv nodeaffinity 的 node 的上面后,这个 pod 里面所使用的 PVC 才会真正地与 PV 做绑定,这样就保证我 pod 调度到这台 node 上之后,这个 PVC 才与这个 PV 绑定,最终保证的是创建出来的 pod 能访问这块 Local PV,也就是静态 Provisioning 场景下怎么去满足 PV 的拓扑限制。

限制 Dynamic Provisioning PV 拓扑示例

再看一下动态 Provisioning PV 的时候,怎么去做拓扑限制的?

动态就是指动态创建 PV 就有拓扑位置的限制,那怎么去指定?

首先在 storageclass 还是需要指定 BindingMode,就是 WaitForFirstConsumer,就是延迟绑定。

其次特别重要的一个字段就是 allowedTopologies,限制就在这个地方。上图中可以看到拓扑限制是可用区的级别,这里其实有两层意思:

  1. 第一层意思就是说我在动态创建 PV 的时候,创建出来的 PV 必须是在这个可用区可以访问的;
  2. 第二层含义是因为声明的是延迟绑定,那调度器在发现使用它的 PVC 正好对应的是该 storageclass 的时候,调度 pod 就要选择位于该可用区的 nodes。

总之,就是要从两方面保证,一是动态创建出来的存储时要能被这个可用区访问的,二是我调度器在选择 node 的时候,要落在这个可用区内的,这样的话就保证我的存储和我要使用存储的这个 pod 它所对应的 node,它们之间的拓扑域是在同一个拓扑域,用户在写 PVC 文件的时候,写法是跟以前的写法是一样的,主要是在 storageclass 中要做一些拓扑限制。

三、操作演示

本节将在线上环境来演示一下前面讲解的内容。

首先来看一下我的阿里云服务器上搭建的 K8s 服务。总共有 3 个 node 节点。一个 master 节点,两个 node。其中 master 节点是不能调度 pod 的。

再看一下,我已经提前把我需要的插件已经布好了,一个是 snapshot 插件  (csi-external-snapshot_),一个是动态云盘的插件  (csi-disk_)。

现在开始 snapshot 的演示。首先去动态创建云盘,然后才能做 snapshot。动态创建云盘需要先创建  storageclass,然后去根据 PVC 动态创建 PV,然后再创建一个使用它的 pod 了。

有个以上对象,现在就可以做 snapshot 了,首先看一下做 snapshot 需要的第一个配置文件:snapshotclass.yaml。

其实里面就是指定了在做存储快照的时候需要使用的插件,这个插件刚才演示了已经部署好了,就是 csi-external-snapshot-0 这个插件。

接下来创建 volume-snapshotclass 文件,创建完之后就开始了 snapshot。

然后看 snapshot.yaml,Volumesnapshot 声明创建存储快照了,这个地方就指定刚才创建的那个 PVC 来做的数据源来做 snapshot,那我们开始创建。

我们看一下 Snapshot 有没有创建好,如下图所示,content 已经在 11 秒之前创建好了。

可以看一下它里面的内容,主要看 volumesnapshotcontent 记录的一些信息,这个是我 snapshot 出来之后,它记录的就是云存储厂商那边返回给我的 snapshot 的 ID。然后是这个 snapshot 数据源,也就是刚才指定的 PVC,可以通过它会找到对应的 PV。

snapshot 的演示大概就是这样,把刚才创建的 snapshot 删掉,还是通过 volumesnapshot 来删掉。然后看一下,动态创建的这个 volumesnapshotcontent 也被删掉。

接下来看一下动态 PV 创建的过程加上一些拓扑限制,首先将的 storageclass 创建出来,然后再看一下 storageclass 里面做的限制,storageclass 首先还是指定它的 BindingMode 为 WaitForFirstConsumer,也就是做延迟绑定,然后是对它的拓扑限制,我这里面在 allowedTopologies 字段中配置了一个可用区级别的限制。

来尝试创建一下的 PVC,PVC 创建出来之后,理论上它应该处在 pending 状态。看一下,它现在因为它要做延迟绑定,由于现在没有使用它的 pod,暂时没办法去做绑定,也没办法去动态创建新的 PV。

接下来创建使用该 pvc 的 pod 看会有什么效果,看一下 pod,pod 也处在 pending 了。

那来看一下 pod 为啥处在 pending 状态,可以看一下是调度失败了,调度失败原因:一个 node 由于 taint 不能调度,这个其实是 master,另外两个 node 也是没有说是可绑定的 PV。

为什么会有两个 node 出现没有可绑定的 pv 的错误?不是动态创建么?

我们来仔细看看 storageclass 中的拓扑限制,通过上面的讲解我们知道,这里限制使用该 storageclass 创建的 PV 存储必须在可用区 cn-hangzhou-d 可访问的,而使用该存储的 pod 也必须调度到 cn-hangzhou-d 的 node 上。

那就来看一下 node 节点上有没有这个拓扑信息,如果它没有当然是不行了。

看一下第一个 node 的全量信息吧,主要找它的 labels 里面的信息,看 lables 里面的确有一个这样的 key。也就是说有一个这样的拓扑,但是这指定是 cn-hangzhou-b,刚才 storageclass 里面指定的是 cn-hangzhou-d。

那看一下另外的一个 node 上的这个拓扑信息写的也是 hangzhou-b,但是我们那个 storageclass 里面限制是 d。

这就导致最终没办法将 pod 调度在这两个 node 上。现在我们修改一下 storageclass 中的拓扑限制,将 cn-hangzhou-d 改为 cn-hangzhou-b。

改完之后再看一下,其实就是说我动态创建出来的 PV 要能被 hangzhou-b 这个可用区访问的,使用该存储的 pod 要调度到该可用区的 node 上。把之前的 pod 删掉,让它重新被调度看一下有什么结果,好,现在这个已经调度成功了,就是已经在启动容器阶段。

说明刚才把 storageclass 它里面的对可用区的限制从 hangzhou-d 改为 hangzhou-b 之后,集群中就有两个 node,它的拓扑关系是和 storageclass 里要求的拓扑关系是相匹配的,这样的话它就能保证它的 pod 是有 node 节点可调度的。上图中最后一点 Pod 已经 Running 了,说明刚才的拓扑限制改动之后可以 work 了。

四、处理流程

kubernetes 对 Volume Snapshot/Restore 处理流程

接下来看一下 K8s 中对存储快照与拓扑调度的具体处理流程。如下图所示:

首先来看一下存储快照的处理流程,这里来首先解释一下 csi 部分。K8s 中对存储的扩展功能都是推荐通过 csi out-of-tree 的方式来实现的。

csi 实现存储扩展主要包含两部分:

  • 第一部分是由 K8s 社区推动实现的 csi controller 部分,也就是这里的 csi-snapshottor controller 以及 csi-provisioner controller,这些主要是通用的 controller 部分;
  • 另外一部分是由特定的云存储厂商用自身 OpenAPI 实现的不同的 csi-plugin 部分,也叫存储的 driver 部分。

两部分部件通过 unix domain socket 通信连接到一起。有这两部分,才能形成一个真正的存储扩展功能。

如上图所示,当用户提交 VolumeSnapshot 对象之后,会被 csi-snapshottor controller watch 到。之后它会通过 GPPC 调用到 csi-plugin,csi-plugin 通过 OpenAPI 来真正实现存储快照的动作,等存储快照已经生成之后,会返回到 csi-snapshottor controller 中,csi-snapshottor controller 会将存储快照生成的相关信息放到 VolumeSnapshotContent 对象中并将用户提交的 VolumeSnapshot 做 bound。这个 bound 其实就有点类似 PV 和 PVC 的 bound 一样。

有了存储快照,如何去使用存储快照恢复之前的数据呢?前面也说过,通过声明一个新的 PVC 对象,并且指定他的 dataSource 为 Snapshot 对象,当提交 PVC 的时候会被 csi-provisioner watch 到,之后会通过 GRPC 去创建存储。这里创建存储跟之前讲解的 csi-provisioner 有一个不太一样的地方,就是它里面还指定了 Snapshot 的 ID,当去云厂商创建存储时,需要多做一步操作,即将之前的快照数据恢复到新创建的存储中。之后流程返回到 csi-provisioner,它会将新创建的存储的相关信息写到一个新的 PV 对象中,新的 PV 对象被 PV controller watch 到它会将用户提交的 PVC 与 PV 做一个 bound,之后 pod 就可以通过 PVC 来使用 Restore 出来的数据了。这是 K8s 中对存储快照的处理流程。

kubernetes 对 Volume Topology-aware Scheduling 处理流程

接下来看一下存储拓扑调度的处理流程:

第一个步骤 其实就是要去声明延迟绑定,这个通过 StorageClass 来做的,上面已经阐述过,这里就不做详细描述了。

接下来看一下调度器,上图中红色部分就是调度器新加的存储拓扑调度逻辑,我们先来看一下不加红色部分时调度器的为一个 pod 选择 node 时,它的大概流程:

  • 首先用户提交完 pod 之后,会被调度器 watch 到,它就会去首先做预选,预选就是说它会将集群中的所有 node 都来与这个 pod 它需要的资源做匹配;
  • 如果匹配上,就相当于这个 node 可以使用,当然可能不止一个 node 可以使用,最终会选出来一批 node;
  • 然后再经过第二个阶段优选,优选就相当于我要对这些 node 做一个打分的过程,通过打分找到最匹配的一个 node;
  • 之后调度器将调度结果写到 pod 里面的 spec.nodeName 字段里面,然后会被相应的 node 上面的 kubelet watch 到,最后就开始创建 pod 的整个流程。

那现在看一下加上卷相关的调度的时候,筛选 node(第二个步骤 ) 又是怎么做的?

  • 先就要找到 pod 中使用的所有 PVC,找到已经 bound 的 PVC,以及需要延迟绑定的这些 PVC;
  • 对于已经 bound 的 PVC,要 check 一下它对应的 PV 里面的 nodeAffinity 与当前 node 的拓扑是否匹配。如果不匹配,就说明这个 node 不能被调度。如果匹配,继续往下走,就要去看一下需要延迟绑定的 PVC;
  • 对于需要延迟绑定的 PVC。先去获取集群中存量的 PV,满足 PVC 需求的,先把它全部捞出来,然后再将它们一一与当前的 node labels 上的拓扑做匹配,如果它们 (存量的 PV) 都不匹配,那就说明当前的存量的 PV 不能满足需求,就要进一步去看一下如果要动态创建 PV 当前 node 是否满足拓扑限制,也就是还要进一步去 check StorageClass 中的拓扑限制,如果 StorageClass 中声明的拓扑限制与当前的 node 上面已经有的 labels 里面的拓扑是相匹配的,那其实这个 node 就可以使用,如果不匹配,说明该 node 就不能被调度。

经过这上面步骤之后,就找到了所有即满足 pod 计算资源需求又满足 pod 存储资源需求的所有 nodes。
 
当 node 选出来之后,第三个步骤 就是调度器内部做的一个优化。这里简单过一下,就是更新经过预选和优选之后,pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中做的一些 cache 信息。

第四个步骤 也是重要的一步,已经选择出来 node 的 Pod,不管其使用的 PVC 是要 binding 已经存在的 PV,还是要做动态创建 PV,这时就可以开始做。由调度器来触发,调度器它就会去更新 PVC 对象和 PV 对象里面的相关信息,然后去触发 PV controller 去做 binding 操作,或者是由 csi-provisioner 去做动态创建流程。

总结

  1. 通过对比 PVC&PV 体系讲解了存储快照的相关 K8s 资源对象以及使用方法;
  2. 通过两个实际场景遇到的问题引出存储拓扑调度功能必要性,以及 K8s 中如何通过拓扑调度来解决这些问题;
  3. 通过剖析 K8s 中存储快照和存储拓扑调度内部运行机制,深入理解该部分功能的工作原理。

本文作者:至天 阿里巴巴高级研发工程师

原文链接

本文为云栖社区原创内容,未经允许不得转载。

正文完
 0

从零开始入门-K8s-可观测性你的应用健康吗

81次阅读

共计 9283 个字符,预计需要花费 24 分钟才能阅读完成。

一、需求来源

首先来看一下,整个需求的来源:当把应用迁移到 Kubernetes 之后,要如何去保障应用的健康与稳定呢?其实很简单,可以从两个方面来进行增强:

  1. 首先是提高应用的可观测性;
  2. 第二是提高应用的可恢复能力。

从可观测性上来讲,可以在三个方面来去做增强:

  1. 首先是应用的健康状态上面,可以实时地进行观测;
  2. 第二个是可以获取应用的资源使用情况;
  3. 第三个是可以拿到应用的实时日志,进行问题的诊断与分析。

当出现了问题之后,首先要做的事情是要降低影响的范围,进行问题的调试与诊断。最后当出现问题的时候,理想的状况是:可以通过和 K8s 集成的自愈机制进行完整的恢复。

二、Liveness 与 Readiness

本小节为大家介绍 Liveness probe 和 eadiness probe。

应用健康状态 - 初识 Liveness 与 Readiness

Liveness probe 也叫就绪指针,用来判断一个 pod 是否处在就绪状态。当一个 pod 处在就绪状态的时候,它才能够对外提供相应的服务,也就是说接入层的流量才能打到相应的 pod。当这个 pod 不处在就绪状态的时候,接入层会把相应的流量从这个 pod 上面进行摘除。

来看一下简单的一个例子:

如下图其实就是一个 Readiness 就绪的一个例子:

当这个 pod 指针判断一直处在失败状态的时候,其实接入层的流量不会打到现在这个 pod 上。

当这个 pod 的状态从 FAIL 的状态转换成 success 的状态时,它才能够真实地承载这个流量。

Liveness 指针也是类似的,它是存活指针,用来判断一个 pod 是否处在存活状态。当一个 pod 处在不存活状态的时候,会出现什么事情呢?

这个时候会由上层的判断机制来判断这个 pod 是否需要被重新拉起。那如果上层配置的重启策略是 restart always 的话,那么此时这个 pod 会直接被重新拉起。

应用健康状态 - 使用方式

接下来看一下 Liveness 指针和 Readiness 指针的具体的用法。

探测方式

Liveness 指针和 Readiness 指针支持三种不同的探测方式:

  1. 第一种是 httpGet。它是通过发送 http Get 请求来进行判断的,当返回码是 200-399 之间的状态码时,标识这个应用是健康的;
  2. 第二种探测方式是 Exec。它是通过执行容器中的一个命令来判断当前的服务是否是正常的,当命令行的返回结果是 0,则标识容器是健康的;
  3. 第三种探测方式是 tcpSocket。它是通过探测容器的 IP 和 Port 进行 TCP 健康检查,如果这个 TCP 的链接能够正常被建立,那么标识当前这个容器是健康的。

探测结果

从探测结果来讲主要分为三种:

  • 第一种是 success,当状态是 success 的时候,表示 container 通过了健康检查,也就是 Liveness probe 或 Readiness probe 是正常的一个状态;
  • 第二种是 Failure,Failure 表示的是这个 container 没有通过健康检查,如果没有通过健康检查的话,那么此时就会进行相应的一个处理,那在 Readiness 处理的一个方式就是通过 service。service 层将没有通过 Readiness 的 pod 进行摘除,而 Liveness 就是将这个 pod 进行重新拉起,或者是删除。
  • 第三种状态是 Unknown,Unknown 是表示说当前的执行的机制没有进行完整的一个执行,可能是因为类似像超时或者像一些脚本没有及时返回,那么此时 Readiness-probe 或 Liveness-probe 会不做任何的一个操作,会等待下一次的机制来进行检验。

那在 kubelet 里面有一个叫 ProbeManager 的组件,这个组件里面会包含 Liveness-probe 或 Readiness-probe,这两个 probe 会将相应的 Liveness 诊断和 Readiness 诊断作用在 pod 之上,来实现一个具体的判断。

应用健康状态 -Pod Probe Spec

下面介绍这三种方式不同的检测方式的一个 yaml 文件的使用。

首先先看一下 exec,exec 的使用其实非常简单。如下图所示,大家可以看到这是一个 Liveness probe,它里面配置了一个 exec 的一个诊断。接下来,它又配置了一个 command 的字段,这个 command 字段里面通过 cat 一个具体的文件来判断当前 Liveness probe 的状态,当这个文件里面返回的结果是 0 时,或者说这个命令返回是 0 时,它会认为此时这个 pod 是处在健康的一个状态。

那再来看一下这个 httpGet,httpGet 里面有一个字段是路径,第二个字段是 port,第三个是 headers。这个地方有时需要通过类似像 header 头的一个机制做 health 的一个判断时,需要配置这个 header,通常情况下,可能只需要通过 health 和 port 的方式就可以了。

第三种是 tcpSocket,tcpSocket 的使用方式其实也比较简单,你只需要设置一个检测的端口,像这个例子里面使用的是 8080 端口,当这个 8080 端口 tcp connect 审核正常被建立的时候,那 tecSocket,Probe 会认为是健康的一个状态。

此外还有如下的五个参数,是 Global 的参数。

  • 第一个参数叫 initialDelaySeconds,它表示的是说这个 pod 启动延迟多久进行一次检查,比如说现在有一个 Java 的应用,它启动的时间可能会比较长,因为涉及到 jvm 的启动,包括 Java 自身 jar 的加载。所以前期,可能有一段时间是没有办法被检测的,而这个时间又是可预期的,那这时可能要设置一下 initialDelaySeconds;
  • 第二个是 periodSeconds,它表示的是检测的时间间隔,正常默认的这个值是 10 秒;
  • 第三个字段是 timeoutSeconds,它表示的是检测的超时时间,当超时时间之内没有检测成功,那它会认为是失败的一个状态;
  • 第四个是 successThreshold,它表示的是:当这个 pod 从探测失败到再一次判断探测成功,所需要的阈值次数,默认情况下是 1 次,表示原本是失败的,那接下来探测这一次成功了,就会认为这个 pod 是处在一个探针状态正常的一个状态;
  • 最后一个参数是 failureThreshold,它表示的是探测失败的重试次数,默认值是 3,表示的是当从一个健康的状态连续探测 3 次失败,那此时会判断当前这个 pod 的状态处在一个失败的状态。

应用健康状态 -Liveness 与 Readiness 总结

接下来对 Liveness 指针和 Readiness 指针进行一个简单的总结。

介绍

Liveness 指针是存活指针,它用来判断容器是否存活、判断 pod 是否 running。如果 Liveness 指针判断容器不健康,此时会通过 kubelet 杀掉相应的 pod,并根据重启策略来判断是否重启这个容器。如果默认不配置 Liveness 指针,则默认情况下认为它这个探测默认返回是成功的。

Readiness 指针用来判断这个容器是否启动完成,即 pod 的 condition 是否 ready。如果探测的一个结果是不成功,那么此时它会从 pod 上 Endpoint 上移除,也就是说从接入层上面会把前一个 pod 进行摘除,直到下一次判断成功,这个 pod 才会再次挂到相应的 endpoint 之上。

检测失败

对于检测失败上面来讲 Liveness 指针是直接杀掉这个 pod,而 Readiness 指针是切掉 endpoint 到这个 pod 之间的关联关系,也就是说它把这个流量从这个 pod 上面进行切掉。

适用场景

Liveness 指针适用场景是支持那些可以重新拉起的应用,而 Readiness 指针主要应对的是启动之后无法立即对外提供服务的这些应用。

注意事项

在使用 Liveness 指针和 Readiness 指针的时候有一些注意事项。因为不论是 Liveness 指针还是 Readiness 指针都需要配置合适的探测方式,以免被误操作。

  • 第一个是调大超时的阈值,因为在容器里面执行一个 shell 脚本,它的执行时长是非常长的,平时在一台 ecs 或者在一台 vm 上执行,可能 3 秒钟返回的一个脚本在容器里面需要 30 秒钟。所以这个时间是需要在容器里面事先进行一个判断的,那如果可以调大超时阈值的方式,来防止由于容器压力比较大的时候出现偶发的超时;
  • 第二个是调整判断的一个次数,3 次的默认值其实在比较短周期的判断周期之下,不一定是最佳实践,适当调整一下判断的次数也是一个比较好的方式;
  • 第三个是 exec,如果是使用 shell 脚本的这个判断,调用时间会比较长,比较建议大家可以使用类似像一些编译性的脚本 Golang 或者一些 C 语言、C++ 编译出来的这个二进制的 binary 进行判断,那这种通常会比 shell 脚本的执行效率高 30% 到 50%;
  • 第四个是如果使用 tcpSocket 方式进行判断的时候,如果遇到了 TLS 的服务,那可能会造成后边 TLS 里面有很多这种未健全的 tcp connection,那这个时候需要自己对业务场景上来判断,这种的链接是否会对业务造成影响。

三、问题诊断

接下来给大家讲解一下在 K8s 中常见的问题诊断。

应用故障排查 - 了解状态机制

首先要了解一下 K8s 中的一个设计理念,就是这个状态机制。因为 K8s 是整个的一个设计是面向状态机的,它里面通过 yaml 的方式来定义的是一个期望到达的一个状态,而真正这个 yaml 在执行过程中会由各种各样的 controller 来负责整体的状态之间的一个转换。

比如说上面的图,实际上是一个 Pod 的一个生命周期。刚开始它处在一个 pending 的状态,那接下来可能会转换到类似像 running,也可能转换到 Unknown,甚至可以转换到 failed。然后,当 running 执行了一段时间之后,它可以转换到类似像 successded 或者是 failed,然后当出现在 unknown 这个状态时,可能由于一些状态的恢复,它会重新恢复到 running 或者 successded 或者是 failed。

其实 K8s 整体的一个状态就是基于这种类似像状态机的一个机制进行转换的,而不同状态之间的转化都会在相应的 K8s 对象上面留下来类似像 Status 或者像 Conditions 的一些字段来进行表示。

像下面这张图其实表示的就是说在一个 Pod 上面一些状态位的一些展现。

比如说在 Pod 上面有一个字段叫 Status,这个 Status 表示的是 Pod 的一个聚合状态,在这个里面,这个聚合状态处在一个 pending 状态。

然后再往下看,因为一个 pod 里面有多个 container,每个 container 上面又会有一个字段叫 State,然后 State 的状态表示当前这个 container 的一个聚合状态。那在这个例子里面,这个聚合状态处在的是 waiting 的状态,那具体的原因是因为什么呢?是因为它的镜像没有拉下来,所以处在 waiting 的状态,是在等待这个镜像拉取。然后这个 ready 的部分呢,目前是 false,因为它这个进行目前没有拉取下来,所以这个 pod 不能够正常对外服务,所以此时 ready 的状态是未知的,定义为 false。如果上层的 endpoint 发现底层这个 ready 不是 true 的话,那么此时这个服务是没有办法对外服务的。

再往下是 condition,condition 这个机制表示是说:在 K8s 里面有很多这种比较小的这个状态,而这个状态之间的聚合会变成上层的这个 Status。那在这个例子里面有几个状态,第一个是 Initialized,表示是不是已经初始化完成?那在这个例子里面已经是初始化完成的,那它走的是第二个阶段,是在这个 ready 的状态。因为上面几个 container 没有拉取下来相应的镜像,所以 ready 的状态是 false。

然后再往下可以看到这个 container 是否 ready,这里可以看到是 false,而这个状态是 PodScheduled,表示说当前这个 pod 是否是处在一个已经被调度的状态,它已经 bound 在现在这个 node 之上了,所以这个状态也是 true。

那可以通过相应的 condition 是 true 还是 false 来判断整体上方的这个状态是否是正常的一个状态。而在 K8s 里面不同的状态之间的这个转换都会发生相应的事件,而事件分为两种:一种叫做 normal 的事件,一种是 warning 事件。大家可以看见在这第一条的事件是有个 normal 事件,然后它相应的 reason 是 scheduler,表示说这个 pod 已经被默认的调度器调度到相应的一个节点之上,然后这个节点是 cn-beijing192.168.3.167 这个节点之上。

再接下来,又是一个 normal 的事件,表示说当前的这个镜像在 pull 相应的这个 image。然后再往下是一个 warning 事件,这个 warning 事件表示说 pull 这个镜像失败了。

以此类推,这个地方表示的一个状态就是说在 K8s 里面这个状态机制之间这个状态转换会产生相应的事件,而这个事件又通过类似像 normal 或者是 warning 的方式进行暴露。开发者可以通过类似像通过这个事件的机制,可以通过上层 condition Status 相应的一系列的这个字段来判断当前这个应用的具体的状态以及进行一系列的诊断。

应用故障排查 - 常见应用异常

本小节介绍一下常见应用的一些异常。首先是 pod 上面,pod 上面可能会停留几个常见的状态。

Pod 停留在 Pending

第一个就是 pending 状态,pending 表示调度器没有进行介入。此时可以通过 kubectl describe pod 来查看相应的事件,如果由于资源或者说端口占用,或者是由于 node selector 造成 pod 无法调度的时候,可以在相应的事件里面看到相应的结果,这个结果里面会表示说有多少个不满足的 node,有多少是因为 CPU 不满足,有多少是由于 node 不满足,有多少是由于 tag 打标造成的不满足。

Pod 停留在 waiting

那第二个状态就是 pod 可能会停留在 waiting 的状态,pod 的 states 处在 waiting 的时候,通常表示说这个 pod 的镜像没有正常拉取,原因可能是由于这个镜像是私有镜像,但是没有配置 Pod secret;那第二种是说可能由于这个镜像地址是不存在的,造成这个镜像拉取不下来;还有一个是说这个镜像可能是一个公网的镜像,造成镜像的拉取失败。

Pod 不断被拉取并且可以看到 crashing

第三种是 pod 不断被拉起,而且可以看到类似像 backoff。这个通常表示说 pod 已经被调度完成了,但是启动失败,那这个时候通常要关注的应该是这个应用自身的一个状态,并不是说配置是否正确、权限是否正确,此时需要查看的应该是 pod 的具体日志。

Pod 处在 Runing 但是没有正常工作

第四种 pod 处在 running 状态,但是没有正常对外服务。那此时比较常见的一个点就可能是由于一些非常细碎的配置,类似像有一些字段可能拼写错误,造成了 yaml 下发下去了,但是有一段没有正常地生效,从而使得这个 pod 处在 running 的状态没有对外服务,那此时可以通过 apply-validate-f pod.yaml 的方式来进行判断当前 yaml 是否是正常的,如果 yaml 没有问题,那么接下来可能要诊断配置的端口是否是正常的,以及 Liveness 或 Readiness 是否已经配置正确。

Service 无法正常的工作

最后一种就是 service 无法正常工作的时候,该怎么去判断呢?那比较常见的 service 出现问题的时候,是自己的使用上面出现了问题。因为 service 和底层的 pod 之间的关联关系是通过 selector 的方式来匹配的,也就是说 pod 上面配置了一些 label,然后 service 通过 match label 的方式和这个 pod 进行相互关联。如果这个 label 配置的有问题,可能会造成这个 service 无法找到后面的 endpoint,从而造成相应的 service 没有办法对外提供服务,那如果 service 出现异常的时候,第一个要看的是这个 service 后面是不是有一个真正的 endpoint,其次来看这个 endpoint 是否可以对外提供正常的服务。

四、应用远程调试

本节讲解的是在 K8s 里面如何进行应用的远程调试,远程调试主要分为 pod 的远程调试以及 service 的远程调试。还有就是针对一些性能优化的远程调试。

应用远程调试 – Pod 远程调试

首先把一个应用部署到集群里面的时候,发现问题的时候,需要进行快速验证,或者说修改的时候,可能需要类似像登陆进这个容器来进行一些诊断。

比如说可以通过 exec 的方式进入一个 pod。像这条命令里面,通过 kubectl exec-it pod-name 后面再填写一个相应的命令,比如说 /bin/bash,表示希望到这个 pod 里面进入一个交互式的一个 bash。然后在 bash 里面可以做一些相应的命令,比如说修改一些配置,通过 supervisor 去重新拉起这个应用,都是可以的。

那如果指定这一个 pod 里面可能包含着多个 container,这个时候该怎么办呢?怎么通过 pod 来指定 container 呢?其实这个时候有一个参数叫做 -c,如上图下方的命令所示。-c 后面是一个 container-name,可以通过 pod 在指定 -c 到这个 container-name,具体指定要进入哪个 container,后面再跟上相应的具体的命令,通过这种方式来实现一个多容器的命令的一个进入,从而实现多容器的一个远程调试。

应用远程调试 – Servic 远程调试

那么 service 的远程调试该怎么做呢?service 的远程调试其实分为两个部分:

  • 第一个部分是说我想将一个服务暴露到远程的一个集群之内,让远程集群内的一些应用来去调用本地的一个服务,这是一条反向的一个链路;
  • 还有一种方式是我想让这个本地服务能够去调远程的服务,那么这是一条正向的链路。

在反向列入上面有这样一个开源组件,叫做 Telepresence,它可以将本地的应用代理到远程集群中的一个 service 上面,使用它的方式非常简单。

首先先将 Telepresence 的一个 Proxy 应用部署到远程的 K8s 集群里面。然后将远程单一个 deployment swap 到本地的一个 application,使用的命令就是 Telepresence-swap-deployment 然后以及远程的 DEPLOYMENT_NAME。通过这种方式就可以将本地一个 application 代理到远程的 service 之上、可以将应用在远程集群里面进行本地调试,这个有兴趣的同学可以到 GitHub 上面来看一下这个插件的使用的方式。

第二个是如果本地应用需要调用远程集群的服务时候,可以通过 port-forward 的方式将远程的应用调用到本地的端口之上。比如说现在远程的里面有一个 API server,这个 API server 提供了一些端口,本地在调试 Code 时候,想要直接调用这个 API server,那么这时,比较简单的一个方式就是通过 port-forward 的方式。

它的使用方式是 kubectl port-forward,然后 service 加上远程的 service name,再加上相应的 namespace,后面还可以加上一些额外的参数,比如说端口的一个映射,通过这种机制就可以把远程的一个应用代理到本地的端口之上,此时通过访问本地端口就可以访问远程的服务。

开源的调试工具 – kubectl-debug

最后再给大家介绍一个开源的调试工具,它也是 kubectl 的一个插件,叫 kubectl-debug。我们知道在 K8s 里面,底层的容器 runtime 比较常见的就是类似像 docker 或者是 containerd,不论是 docker 还是 containerd,它们使用的一个机制都是基于 Linux namespace 的一个方式进行虚拟化和隔离的。

通常情况下,并不会在镜像里面带特别多的调试工具,类似像 netstat telnet 等等这些,因为这个会造成应用整体非常冗余。那么如果想要调试的时候该怎么做呢?其实这个时候就可以依赖类似于像 kubectl-debug 这样一个工具。

kubectl-debug 这个工具是依赖于 Linux namespace 的方式来去做的,它可以 datash 一个 Linux namespace 到一个额外的 container,然后在这个 container 里面执行任何的 debug 动作,其实和直接去 debug 这个 Linux namespace 是一致的。这里有一个简单的操作,给大家来介绍一下:

这个地方其实已经安装好了 kubectl-debug,它是 kubectl 的一个插件。所以这个时候,你可以直接通过 kubectl-debug 这条命令来去诊断远程的一个 pod。像这个例子里面,当执行 debug 的时候,实际上它首先会先拉取一些镜像,这个镜像里面实际上会默认带一些诊断的工具。当这个镜像启用的时候,它会把这个 debug container 进行启动。与此同时会把这个 container 和相应的你要诊断的这个 container 的 namespace 进行挂靠,也就说此时这个 container 和你是同 namespace 的,类似像网络站,或者是类似像内核的一些参数,其实都可以在这个 debug container 里面实时地进行查看。

像这个例子里面,去查看类似像 hostname、进程、netstat 等等,这些其实都是和这个需要 debug 的 pod 是在同一个环境里面的,所以你之前这三条命令可以看到里面相关的信息。

如果此时进行 logout 的话,相当于会把相应的这个 debug pod 杀掉,然后进行退出,此时对应用实际上是没有任何的影响的。那么通过这种方式可以不介入到容器里面,就可以实现相应的一个诊断。

此外它还支持额外的一些机制,比如说我给设定一些 image,然后类似像这里面安装了的是 htop,然后开发者可以通过这个机制来定义自己需要的这个命令行的工具,并且通过这种 image 的方式设置进来。那么这个时候就可以通过这种机制来调试远程的一个 pod。

本节总结

  • 关于 Liveness 和 Readiness 的指针。Liveness probe 就是保活指针,它是用来看 pod 是否存活的,而 Readiness probe 是就绪指针,它是判断这个 pod 是否就绪的,如果就绪了,就可以对外提供服务。这个就是 Liveness 和 Readiness 需要记住的部分;
  • 应用诊断的三个步骤:首先 describe 相应的一个状态;然后提供状态来排查具体的一个诊断方向;最后来查看相应对象的一个 event 获取更详细的一个信息;
  • 提供 pod 一个日志来定位应用的自身的一个状态;
  • 远程调试的一个策略,如果想把本地的应用代理到远程集群,此时可以通过 Telepresence 这样的工具来实现,如果想把远程的应用代理到本地,然后在本地进行调用或者是调试,可以用类似像 port-forward 这种机制来实现。

本文作者:莫源

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

正文完
 0