1. 概述
在Kubernetes中,探针(Probe)是一种监控和诊断容器健康状态的机制。它通过定期执行某种操作来判断容器是否正常运行,从而实现自动化的问题检测和恢复。
探针的引入大大提高了Kubernetes集群管理的智能化水平。通过配置合适的探测方式和参数,我们可以让Kubernetes自动处理一些常见的问题,比如:
- 如果一个容器死锁、崩溃或者没有响应,探针可以感知到问题并自动重启容器,保证服务的持续可用;
- 对于启动时间较长的应用,探针可以避免在应用完全启动之前将流量导入,防止将请求发送到尚未准备好的容器;
- 探针可以判断应用是否已经准备好对外提供服务,帮助实现滚动更新的平滑切换。
Kubernetes中主要提供了三种类型的探针:
- 存活探针(LivenessProbe):用于判断容器是否存活,如果探测失败,Kubernetes会自动重启容器
- 就绪探针(ReadinessProbe):用于判断容器是否已经准备好接收请求,如果探测失败,Kubernetes会将容器从Service的后端Endpoint列表中移除,暂停向其发送请求
- 启动探针(StartupProbe):用于判断容器内的应用是否已经启动完成。启动探针通常用于启动时间较长的应用,可以避免存活探针过早地认为应用无响应
这三种探针可以单独使用,也可以结合使用,以实现对容器状态的全方位监控。我们将在后面的章节中详细介绍每一种探针的原理和使用方法。
2. 探针类型
2.1 存活探针(LivenessProbe)
LivenessProbe用于判断容器是否存活,是Kubernetes中最常用的一种探针。它的基本原理是定期执行一个探测操作,如果探测成功,则认为容器是健康存活的;如果探测失败,则认为容器已经死亡,Kubernetes会根据重启策略自动重启容器。
存活探针的主要作用是及时发现容器的异常状态,防止应用程序进入不可用的状态。例如,如果一个Java应用因为内存泄漏导致无法响应请求,存活探针可以探测到这种情况,然后重启容器,恢复服务。
存活探针支持三种探测方式:
- Exec探针:在容器内执行一个命令,如果命令的退出状态码为0,则认为探测成功
- TCP探针:尝试与容器的指定端口建立TCP连接,如果连接成功,则认为探测成功
- HTTP探针:向容器的指定端口和路径发送一个HTTP GET请求,如果响应的状态码在200到400之间,则认为探测成功
当存活探针连续多次探测失败时,Kubernetes就会认为容器已经死亡,并根据Pod的重启策略进行处理:
- Always(默认):容器会被立即重启,并且将来会一直重启;
- OnFailure:容器会被立即重启,但是在5分钟内最多重启10次;
- Never:容器不会被自动重启,Pod将一直处于Failed状态;
需要注意的是,存活探针只能发现容器是否存活,但不能判断容器是否已经准备好处理请求。如果一个应用需要较长的启动时间,存活探针可能会过早地认为应用已经死亡并触发重启,导致应用反复重启却无法正常提供服务。这种情况下,我们可以使用启动探针(StartupProbe)来延迟存活探针的检查,直到应用完全启动。
此外,如果容器的主进程(PID 1)是一个脚本或者其他非常驻进程,存活探针可能会无法正确判断容器的状态。因为当主进程退出时,容器就会被认为已经死亡,即使其他进程还在运行。对于这种情况,我们可以考虑使用进程管理工具如supervisord来管理容器内的多个进程。
存活探针是实现自愈性(self-healing)应用的关键手段之一。通过恰当地配置探测方式和参数,我们可以让Kubernetes自动处理许多常见的故障,大大减轻了运维的负担。但是探针的配置也需要根据实际情况进行调整,不能完全依赖默认值,否则可能会适得其反。
2.2 就绪探针(ReadinessProbe)
ReadinessProbe用于判断容器是否已经准备好接受请求,是实现平滑滚动更新的关键。与LivenessProbe不同,ReadinessProbe并不会触发容器的重启,而是控制容器是否可以被访问。
当一个Pod中的所有容器都通过了就绪探针的检查,这个Pod就被认为是可用的,Kubernetes会将其加入到对应Service的Endpoint列表中,开始向其发送请求。反之,如果有任何一个容器没有通过就绪探针的检查,这个Pod就会被从Service的Endpoint列表中移除,暂停接收请求。
就绪探针的主要作用是防止将请求发送到还没有准备好的容器,特别是在滚动更新的场景下。例如,当我们使用Deployment更新一个应用的版本时,新版本的Pod在启动初期可能还无法立即处理请求,如果直接将流量切换到新Pod,可能会导致请求失败。这时,就绪探针可以检查新Pod是否已经完全启动并准备好接收请求,只有通过检查的Pod才会被加入到Service的Endpoint中,从而实现平滑的流量切换。
就绪探针支持与存活探针相同的三种探测方式:Exec、TCP和HTTP GET。但是,就绪探针的判断标准通常与存活探针不同。存活探针关注的是容器的运行状态,而就绪探针关注的是应用的业务可用性。例如,一个Web服务器的存活探针可能只检查服务进程是否存在,但是就绪探针可能要检查服务是否已经加载完配置、连接上数据库、可以正常响应请求等。
下面我们通过对比,进一步理解就绪探针与存活探针的区别:
LivenessProbe |
ReadinessProbe | |
作用 | 判断容器是否存活 | 判断容器是否就绪 |
探测失败后果 | 重启容器 | 将容器从Service的Endpoint中移除 |
典型用途 | 发现容器运行时故障,自动恢复可用性 | 控制哪些Pod可以被访问,实现平滑上线 |
探测标准 | 容器是否在运行 | 应用是否可以正常处理请求 |
虽然存活探针和就绪探针的作用不同,但它们可以结合使用,以实现更全面的健康检查。例如,在滚动更新时,我们可以先用就绪探针控制流量切换,等新版本的Pod完全就绪后,再用存活探针监控其运行状态,确保整个更新过程的平稳进行。
此外,并非所有的容器都需要配置就绪探针。对于一些无状态的服务,或者启动时间非常短的应用,存活探针可能就已经足够。而对于有状态服务、启动时间较长的应用,以及需要平滑上线的场景,就绪探针就显得非常必要了。
就绪探针为Kubernetes提供了一种更细粒度的流量控制机制,是实现滚动更新、蓝绿部署等高级发布策略的基础。合理利用就绪探针,可以大大提高应用上线的平稳性和整个系统的稳定性。
2.3 启动探针(StartupProbe)
StartupProbe是Kubernetes 1.16版本引入的一种新的探针类型,它的主要作用是判断容器内的应用是否已经启动完成。与存活探针和就绪探针不同,启动探针专门用于处理启动时间较长的应用场景。
在某些情况下,应用程序可能需要较长的时间来初始化和启动,例如加载大量的配置文件、预热缓存、建立数据库连接等。如果我们直接使用存活探针来检查这类应用,就可能会遇到一个问题:
存活探针可能会在应用完全启动之前多次检查失败,从而触发容器的重启。这样不仅会影响应用的可用性,还可能加重系统的负载。
启动探针就是为了解决这个问题而设计的。它的基本原理是:
在容器启动之后的一段时间内,只执行启动探针,而不执行存活探针和就绪探针。只有当启动探针检查成功后,才会开始执行存活探针和就绪探针。
这样,我们就可以给应用一个宽限期,让它有足够的时间完成启动,而不会被过早地判定为失败。
启动探针支持与存活探针和就绪探针相同的三种探测方式:Exec、TCP和HTTP GET。但是,启动探针有一些特殊的配置参数:
failureThreshold:启动探针的失败阈值,默认为3。即启动探针最多可以连续失败3次,超过这个次数,容器就会被重启。
periodSeconds:启动探针的执行周期,默认为10秒。即每隔10秒执行一次启动探针。
timeoutSeconds:启动探针的超时时间,默认为1秒。即启动探针的执行时间不能超过1秒,否则会被视为失败。
下面我们通过一个例子来说明启动探针与其他两种探针的关系:
假设我们有一个应用需要2分钟的时间来完成启动。如果我们只配置了存活探针,并将其初始延迟(initialDelaySeconds)设置为30秒,那么在应用启动的前2分钟内,存活探针会连续多次检查失败,导致容器被反复重启。
现在,我们可以添加一个启动探针,并将其失败阈值设置为12。这意味着启动探针最多可以连续失败12次,即允许应用最多花费2分钟(12 *10秒)来完成启动。在这2分钟内,只会执行启动探针,而不会执行存活探针。只有当启动探针检查成功后,才会开始执行存活探针,此时应用已经完全启动,可以正常处理请求了。
可见,启动探针提供了一种延迟存活探针的机制,让应用有充足的时间来完成启动,避免了过早的重启。同时,启动探针本身也会检查应用的启动状态,如果应用确实无法在指定时间内启动,启动探针最终还是会触发容器的重启,以恢复应用的可用性。
启动探针是对存活探针的一种补充,适用于启动时间较长、启动过程复杂的应用场景。它与就绪探针的关系相对独立,主要影响的是存活探针的行为。在实际使用时,我们可以根据应用的特点,灵活地组合使用这三种探针,以实现更精细的健康检查和状态管理。
3. 探测方式
3.1 ExecAction
ExecAction是Kubernetes探针的一种探测方式,它通过在容器内执行一个命令来判断容器的健康状态。ExecAction的原理是:
- Kubelet根据探针的配置,定期在容器内执行一个命令。这个命令通常是一个用于健康检查的脚本或者工具,例如针对Web服务器的curl命令,或者针对数据库的连接测试命令等;
- 命令在容器内运行,并返回一个退出状态码。如果退出状态码为0,则表示命令执行成功,Kubelet认为容器是健康的;如果退出状态码非0,则表示命令执行失败,Kubelet认为容器是不健康的;
- Kubelet根据探测结果决定后续的操作:
- 对于LivenessProbe,如果探测失败,Kubelet会根据重启策略重启容器;
- 对于ReadinessProbe,如果探测失败,Kubelet会将容器从Service的Endpoint列表中移除;
- 对于StartupProbe,如果探测失败,Kubelet会根据失败阈值决定是否重启容器。
可以看到,ExecAction通过在容器内运行命令,将容器的健康状态转化为命令的退出状态码,从而实现了对容器的健康检查。这种方式具有以下优点:
- 灵活性高:可以根据应用的特点,自定义健康检查的逻辑和标准。例如,可以检查进程是否存在、端口是否可用、日志是否包含错误关键字等;
- 适用性广:可以适用于各种类型的应用,不限于Web服务。只要能找到一种方式将应用的健康状态转化为命令的退出码,就可以使用ExecAction进行检查;
- 与应用解耦:ExecAction只需要关注命令的退出码,而不需要了解应用的内部实现细节。这样可以将健康检查的逻辑与应用代码分离,提高可维护性。
当然,ExecAction也有一些局限性:
- 性能开销:每次探测都需要在容器内启动一个新的进程来执行命令,这会消耗一定的CPU和内存资源。如果探测频率较高,可能会对应用性能产生影响;
- 安全风险:由于需要在容器内执行命令,因此必须谨慎地控制命令的权限和范围。如果使用了不安全的命令或者参数,可能会被攻击者利用,从而威胁到整个系统的安全;
- 故障诊断:当ExecAction探测失败时,我们只能知道命令的退出码,但无法知道具体的失败原因。这会给问题诊断和修复带来一定的难度。
3.2 TCPSocketAction
TCPSocketAction是Kubernetes探针的一种探测方式,它通过尝试与容器的指定端口建立TCP连接来判断容器是否健康。如果能够成功建立连接,就认为容器是正常的;如果无法建立连接,就认为容器是异常的。
TCPSocketAction的工作原理是:
- Kubelet根据探针的配置,周期性地尝试与容器的指定端口建立TCP连接;
- 如果连接成功建立,Kubelet就认为本次探测成功,并记录探测结果;
- 如果连接建立失败(比如端口未监听、拒绝连接、超时等),Kubelet就认为本次探测失败,并记录探测结果;
- Kubelet会根据连续成功或失败的次数,来决定将容器判定为正常还是异常。
相比于ExecAction,TCPSocketAction的优点是:
- 更轻量级:不需要在容器内执行命令,对容器的影响更小;
- 更通用:不依赖于容器内的任何程序,适用于所有基于TCP的应用;
- 更安全:不需要在容器内执行任意命令,降低了安全风险。
但是,TCPSocketAction也有一些局限性:
- 只能检测端口的可达性,无法判断应用的实际状态;
- 对于HTTP等基于TCP的应用层协议,无法检测具体的HTTP状态码、响应内容等;
- 如果容器内的进程绑定了本地回环地址(127.0.0.1),则TCPSocketAction可能会误判;
TCPSocketAction仍然是一种非常实用的探测方式。特别是对于一些无状态的服务(如Redis、Memcached等),我们通常只需要确保服务端口可达,而不需要关心更多的细节,这时使用TCPSocketAction就非常方便。
在实际使用中,我们需要根据应用的特点,合理地配置TCPSocketAction的参数,主要包括:
参数名称 | 描述 | 默认值 |
port |
要探测的容器端口号。HTTPGetAction会向该端口发送HTTP GET请求。 | - |
host |
要连接的主机名或IP地址。默认为Pod的IP,也可以指定为一个域名。 | Pod的IP |
scheme |
连接使用的协议,必须是HTTP或HTTPS。 | HTTP |
path |
访问的HTTP服务器的路径。 | / |
httpHeaders |
请求中自定义的HTTP头。这可以用于传递一些特殊的参数,如身份验证信息等。 | - |
initialDelaySeconds |
容器启动后到开始执行探测的延迟时间,单位为秒。这个参数可以用于给应用程序一个初始化的时间,避免过早地开始探测。 | 0 |
periodSeconds |
执行探测的频率,单位为秒。 | 10 |
timeoutSeconds |
探测的超时时间,单位为秒。如果在指定时间内没有收到响应,则认为探测失败。 | 1 |
successThreshold |
探测失败后认为成功的最小连续成功次数。 | 1 |
failureThreshold |
探测成功后认为失败的最小连续失败次数。如果连续失败达到指定次数,就认为容器已经不健康,需要重启或移除。 | 3 |
通过调整这些参数,我们可以控制探测的时机、频率和灵敏度,以适应不同应用的需求。同时,我们还要注意设置合理的初始延迟和超时时间,以避免因为应用启动慢或者网络延迟而导致探测失败。
3.3 HTTPGetAction
HTTPGetAction是Kubernetes探针的一种探测方式,它通过向容器发送HTTP GET请求来判断容器的健康状态。与ExecAction和TCPSocketAction不同,HTTPGetAction在应用层面检查容器的状态,因此可以执行更加精细和定制化的健康检查逻辑。
HTTPGetAction的工作原理是:
- Kubelet根据探针的配置,定期向容器的指定端口发送一个HTTP GET请求。请求的路径、端口、HTTP头等参数都可以在探针中自定义;
- 容器收到请求后,由应用程序根据自身的业务逻辑进行处理,并返回一个HTTP响应;
- Kubelet接收到HTTP响应后,根据响应的状态码来判断探测是否成功。如果状态码在200到400之间(不包括400),则认为探测成功,否则认为探测失败;
- 如果探测成功,Kubelet会认为容器是健康的,继续执行下一次探测。如果探测失败,Kubelet会根据探针的类型(存活、就绪、启动)和重试次数等参数,决定是否重启容器或者将容器从Service的后端中移除。
可以看到,HTTPGetAction将健康检查的主动权交给了应用程序自身。这意味着我们可以在应用程序中实现任意的健康检查逻辑,例如:
- 检查应用程序的关键组件(如数据库连接、缓存服务等)是否正常工作
- 检查应用程序的内部状态和性能指标是否正常
- 根据业务规则动态调整应用程序的健康状态
- 提供一个专门用于健康检查的API接口,与应用程序的业务接口分离
与ExecAction和TCPSocketAction相比,HTTPGetAction的优点在于:
- 更加灵活和可定制。我们可以在应用程序中自由地实现健康检查逻辑,而不仅仅局限于进程状态和端口连通性;
- 与应用程序的业务逻辑更加贴近。通过在应用层面进行健康检查,我们可以更全面地评估应用程序的真实状态,而不是仅仅依赖底层的运行状态;
- 可以与应用程序的其他管理功能相结合,例如监控、告警、自愈等,构建一个完整的应用程序管理体系。
当然,HTTPGetAction也有一些需要注意的地方:
- 应用程序需要提供一个专门用于健康检查的API接口,这会增加开发和维护的工作量;
- 健康检查接口的实现需要谨慎,不能影响应用程序的正常服务,也不能占用过多的系统资源;
- 健康检查接口本身的可用性和正确性也需要保证。如果接口本身出现问题,会影响到整个健康检查机制的可靠性。
4. 探针通用参数
在Kubernetes中,不同类型的探针都支持一些通用的配置参数,通过这些参数我们可以精细地控制探针的行为。下面我们重点介绍几个关键的参数:
initialDelaySeconds: 容器启动后到开始执行探测的延迟时间,单位为秒。默认为0,即容器启动后立即开始探测。但在实际场景中,许多应用在刚启动时还无法正常提供服务,如果立即开始探测可能会产生大量的失败记录,甚至导致容器被误杀。通过设置一个合理的initialDelaySeconds,我们可以给应用一个启动的缓冲期,避免过早探测引起的问题。
periodSeconds: 执行探测的频率,单位为秒。默认为10秒,即每10秒执行一次探测。选择探测频率需要权衡及时发现故障和减少探测开销两个因素:
频率太低可能会延迟故障的发现,影响应用的可用性;
频率太高又会增加探测对系统资源的消耗,尤其是使用Exec和HTTP探测方式时。
通常建议根据应用的重要程度和状态变化频率来设置,核心应用可以设置得更频繁一些。
timeoutSeconds: 探测的超时时间,单位为秒。默认为1秒,即在1秒内如果没有得到探测结果,就认为本次探测超时失败。超时时间的设置需要考虑到容器内进程的响应时间、网络延迟等因素。
如果超时时间太短,正常的探测可能会因为偶尔的延迟而失败;
如果超时时间太长,故障的发现时间就会被延后。
通常建议根据应用的实际响应时间来设置,留出一定的冗余度。
successThreshold: 探测失败后认为成功的最小连续成功次数。默认为1,即探测一次成功就认为容器恢复正常。
在实际使用中,有时为了防止探测结果的抖动,我们会通过successThreshold要求连续多次探测成功才认为恢复正常。例如设置为3,就是要求连续3次探测都成功,才最终判定为成功。
这个值不宜设置得过大,否则会延长故障状态到恢复状态的判定时间。
failureThreshold: 探测成功后认为失败的最小连续失败次数。默认为3,即连续3次探测失败才最终判定为失败。
failureThreshold的作用是防止偶发的探测失败导致容器被频繁重启。
只有连续多次失败,才认为容器的确出现了故障,需要重启或移除流量。
和successThreshold类似,这个值也不宜设置得过大,以免延长故障的发现时间。
5. 探针的应用
5.1 在Deployment中使用存活探针和就绪探针
首先,我们创建一个名为liveness-readiness-probe.yaml
的文件,内容如下:
apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: my-app:v1 ports: - containerPort: 8080 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 # 容器启动后30秒开始执行存活探针 periodSeconds: 10 # 每10秒执行一次存活探针 timeoutSeconds: 5 # 每次探针超时时间为5秒 failureThreshold: 3 # 连续3次探测失败则重启容器 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 10 # 容器启动后10秒开始执行就绪探针 periodSeconds: 5 # 每5秒执行一次就绪探针 timeoutSeconds: 2 # 每次探针超时时间为2秒 successThreshold: 3 # 连续3次探测成功则将Pod标记为就绪
在这个例子中,我们定义了一个Deployment,它管理着3个副本的my-app应用。在template部分,我们定义了容器的规格,包括存活探针(livenessProbe)和就绪探针(readinessProbe)。
对于存活探针,我们使用了httpGet方式,它会向容器的8080端口发送一个GET请求,请求路径为/healthz。我们设置initialDelaySeconds为30秒,给应用一个启动的缓冲期。之后每隔10秒(periodSeconds)执行一次探测,如果连续3次(failureThreshold)探测失败,每次探测超过5秒(timeoutSeconds)未响应,就会重启容器。
对于就绪探针,我们也使用了httpGet方式,但是请求路径为/ready。就绪探针的initialDelaySeconds设置为10秒,比存活探针更短,因为通常应用可以更快地准备好接受请求。之后每隔5秒执行一次探测,如果连续3次(successThreshold)探测成功,每次探测超过2秒未响应,就会将Pod标记为就绪,加入到Service的后端。
现在,我们应用这个Deployment:
kubectl apply -f liveness-readiness-probe.yaml
然后我们可以观察Pod的状态变化:
kubectl get pods -w
输出结果类似如下:
NAME READY STATUS RESTARTS AGE my-app-6c5f79d9f8-7bglx 0/1 Running 0 5s my-app-6c5f79d9f8-7bglx 0/1 Running 1 36s my-app-6c5f79d9f8-7bglx 1/1 Running 1 46s my-app-6c5f79d9f8-kdg9p 0/1 Running 0 5s my-app-6c5f79d9f8-kdg9p 0/1 Running 1 36s my-app-6c5f79d9f8-kd
我们可以看到,Pod刚创建时,READY列为0/1,表示还没有通过就绪探针。30秒后,存活探针开始执行,如果/healthz接口没有正确响应,Pod会被重启,RESTARTS列会增加。当/ready接口正确响应后,Pod最终变为就绪状态,READY列变为1/1。
如果我们删除存活探针和就绪探针的定义,再次应用这个Deployment,会发现Pod的行为有所不同:
没有了存活探针,即使应用程序无响应,Pod也不会被自动重启,可能会长时间处于不可用状态;
没有了就绪探针,Pod会立即被标记为就绪,加入到Service的后端,但此时应用程序可能还没准备好处理请求,导致请求失败。
通过这个例子,我们可以直观地感受到存活探针和就绪探针在保障应用可用性方面的重要作用。合理地使用探针,配置适当的参数,可以让Kubernetes更智能地管理应用程序,提高整个系统的稳定性和恢复能力。
当然,探针的定义还需要根据实际的应用特点来调整。我们需要提供相应的/healthz和/ready接口,合理设置初始延迟和超时时间等参数。
5.2 为有启动时间的应用配置启动探针
在实际生产环境中,我们经常会遇到一些启动时间较长的应用,比如需要加载大量配置、初始化缓存、建立外部连接等。这些应用在刚启动时,虽然进程已经运行,但是还无法正常提供服务。如果我们直接使用存活探针来检测这类应用,就很可能会出现存活探针在应用完全启动之前,连续多次探测失败,导致容器被反复重启,影响服务的可用性。
这时,启动探针就可以帮助我们解决这个问题。我们可以在应用的容器规范中同时定义启动探针和存活探针:
启动探针负责检查应用是否已经启动完成,如果检查失败,会继续等待下一次检查,直到达到最大失败次数(failureThreshold)才会重启容器。
存活探针负责检查应用是否健康运行,如果检查失败,会立即重启容器。
关键的是,在启动探针检查成功之前,存活探针是不会执行的。这就给了应用一个"启动保护期",避免了存活探针过早介入而引发的问题。
下面是一个具体的例子,假设我们有一个Java应用需要60秒的时间来完成初始化,我们可以为它配置如下的启动探针和存活探针:
apiVersion: v1 kind: Pod metadata: name: my-app spec: containers: - name: my-app image: my-app:v1.0 startupProbe: httpGet: path: /healthz port: 8080 failureThreshold: 6 # 最多允许失败6次,即60秒的启动时间 periodSeconds: 10 # 每10秒执行一次 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 60 # 启动60秒后开始执行存活探针 periodSeconds: 5 # 每5秒执行一次
在这个例子中:
startupProbe使用HTTP GET请求访问容器的/healthz接口,如果返回2xx或3xx的状态码则认为成功。startupProbe每10秒执行一次(periodSeconds: 10),最多允许失败6次(failureThreshold: 6),即容器有60秒的时间来完成启动。
livenessProbe同样使用HTTP GET请求访问/healthz接口,不同的是它要求容器启动60秒后才开始第一次检查(initialDelaySeconds: 60),然后每5秒执行一次(periodSeconds: 5)。
这样,在容器启动的前60秒内,只有startupProbe在执行,而livenessProbe不会干扰应用的启动。一旦startupProbe检查通过,说明应用已经完全启动,这时livenessProbe才开始工作,监控应用的运行健康状态。
如果应用在启动过程中出现了问题,无法在60秒内完成启动,那么startupProbe会在重试6次后重启容器,而不是让应用一直处于中间状态。
可见,启动探针提供了一种"延时"机制,推迟了存活探针的介入时间,避免了存活探针因为应用启动慢而过早判定失败的问题。同时,启动探针本身也会检查应用的启动状态,确保应用可以在有限的时间内完成启动,如果启动失败,则会通过重启容器来恢复。
5.3 使用HTTP探针实现业务层面的健康检查
在前面的章节中,我们介绍了存活探针和就绪探针主要关注容器的运行状态,比如进程是否存在、端口是否可达等。但是有时候,我们还需要在更高的层次上对应用的健康状态进行检查,比如:
应用的关键依赖(如数据库、缓存等)是否正常可用;
应用的关键流程(如登录、下单等)是否能够正常执行;
应用的性能指标(如响应时间、错误率等)是否在正常范围内。
这就需要我们在应用程序内部实现一个专门用于健康检查的接口,通过这个接口暴露应用的内部状态,然后在Kubernetes中用HTTP探针来访问该接口,以判断应用的实际健康情况。
假设我们有一个Django应用,它有两个关键依赖:一个MySQL数据库和一个Redis缓存。
我们希望在健康检查中判断这两个依赖是否可用,同时还要检查Django应用自身是否能够正常处理请求。
首先,我们在Django应用中实现一个/health
接口:
urlpatterns = [ ... path('health/', views.health), ... ]
from django.http import HttpResponse from django.db import connections from django.db.utils import OperationalError from redis import Redis, ConnectionError def health(request): try: # 检查MySQL连接 cursor = connections['default'].cursor() cursor.execute("SELECT 1") cursor.fetchone() # 检查Redis连接 redis = Redis(host='redis', port=6379) redis.ping() # 检查Django应用 return HttpResponse("OK", status=200) except OperationalError: return HttpResponse("MySQL is not available", status=500) except ConnectionError: return HttpResponse("Redis is not available", status=500) except Exception as e: return HttpResponse(str(e), status=500)
通过这个/health接口,我们可以检查Django应用的关键依赖,如数据库连接、缓存连接等是否正常。这比仅仅检查进程是否存在、端口是否可达更进一步,能够反映应用的真实健康状态。
这个/health接口中,我们分别检查了MySQL连接、Redis连接和Django应用本身的可用性:
对于MySQL,我们通过Django的connections对象获取到默认的数据库连接,然后执行一个简单的SELECT 1查询。如果查询成功,说明MySQL连接是正常的;
对于Redis,我们直接创建了一个Redis客户端,然后调用ping()方法。如果连接正常,该方法会返回True,否则会抛出ConnectionError异常;
对于Django应用,我们直接返回了一个200状态码的HttpResponse,表示应用是健康的。如果在处理请求的过程中发生了任何异常,Django会自动返回500错误;
如果所有检查都通过,/health接口会返回字符串’**OK’**和状态码200,表示应用是健康的;
如果MySQL连接失败,就会返回**‘MySQL is not available’**和状态码500;
如果Redis连接失败,就会返回**‘Redis is not available’**和状态码500;
如果发生了其他异常,就会返回错误信息和状态码500。
有了这个/health
接口,我们就可以在Kubernetes中使用HTTP探针来访问它,以实现业务层面的健康检查。下面是一个Deployment的示例配置:
apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: my-app:v1 ports: - containerPort: 8000 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 # 容器启动30秒后开始存活性探测 periodSeconds: 10 # 每10秒执行一次存活性探测 readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 10 # 容器启动10秒后开始就绪性探测 periodSeconds: 5 # 每5秒执行一次就绪性探测
在这个Deployment中,我们为容器定义了存活探针(livenessProbe)和就绪探针(readinessProbe),它们都使用HTTP GET方法访问容器的/health接口,以判断应用的健康状态。
对于存活探针,我们设置了initialDelaySeconds: 30,即容器启动30秒后才开始探测,以留出时间让应用完成初始化。之后每隔10秒(periodSeconds: 10)执行一次探测。
对于就绪探针,我们设置了initialDelaySeconds: 10,即容器启动10秒后就开始探测,以尽快将健康的Pod加入到Service中。之后每隔5秒(periodSeconds: 5)执行一次探测。
通过这样的配置,Kubernetes就可以根据/health接口返回的状态来判断应用的真实健康状况:
如果/health返回200,说明MySQL、Redis、Django应用都是正常的,Kubernetes会认为容器是健康的,不会重启容器,并且会将其加入到Service的后端。
如果/health返回500,说明某个依赖出现了问题,Kubernetes会认为容器是不健康的,会根据存活探针的重启策略重启容器,并根据就绪探针的结果将其从Service的后端移除,避免将流量导入异常的Pod。
这样,我们就通过一个简单的/health接口和HTTP探针,实现了对应用内部状态的全面检查。这种方式相比于仅检查进程和端口,能够更加准确地反映应用的实际健康状况,帮助我们及时发现和解决问题。
当然,/health接口的实现需要根据应用的实际架构和依赖来设计,上面的例子只是一个简单的演示。在实际项目中,我们可能需要检查更多的依赖和服务,判断更复杂的业务逻辑,甚至根据不同的严重程度返回不同的状态码。但是无论如何,遵循"在应用层面暴露健康状态,在Kubernetes层面通过HTTP探针来检查"的基本思路,我们就可以灵活地实现自己的健康检查策略,让Kubernetes更好地管理我们的应用。
除了在Deployment中单独使用,HTTP探针还可以与其他Kubernetes特性结合,发挥更大的作用。比如在滚动更新时,我们可以通过就绪探针来控制新旧版本的切换时机,确保应用在整个更新过程中保持可用。这部分内容我们将在后面的"5.4 利用探针实现零停机滚动更新"一节中详细介绍。
5.4 利用探针实现零停机滚动更新
在Kubernetes中,我们通常使用Deployment来管理应用的多个副本,并通过更新Deployment的方式来实现应用的滚动更新。滚动更新的基本过程是:
- 创建一个新版本的Pod;
- 等待新Pod就绪;
- 将流量切换到新Pod;
- 删除旧版本的Pod。
在这个过程中,如果新Pod在就绪之前就开始接收流量,就可能会导致请求失败,影响服务的可用性。这时,就绪探针就可以发挥作用了。
我们可以在Deployment中为新版本的Pod模板定义一个就绪探针,例如:
apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: my-app:v2 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 # 容器启动10秒后开始就绪探测 periodSeconds: 5 # 每5秒执行一次就绪探测 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 # 滚动更新时最多有1个Pod不可用 maxSurge: 1 # 滚动更新时最多可以超出期望副本数1个Pod
在这个例子中,我们为新版本的Pod模板定义了一个基于HTTP GET的就绪探针:
- 探针会向Pod的8080端口发送一个GET请求,请求路径为
/healthz
; - 探针会在容器启动10秒后开始执行(initialDelaySeconds),然后每5秒执行一次(periodSeconds);
- 如果探针请求的HTTP状态码在200到400之间,则认为Pod已经就绪,可以接收流量;否则,认为Pod尚未就绪,不会将其加入到Service的Endpoint中;
我们还通过.spec.strategy
字段设置了滚动更新的策略:
maxUnavailable
: 1表示在滚动更新过程中,最多可以有1个Pod处于不可用状态;maxSurge
: 1表示在滚动更新过程中,最多可以超出期望副本数1个Pod;
这样可以更精细地控制滚动更新的过程,避免一次性创建或删除过多的Pod,从而影响服务的可用性。
现在,当我们更新这个Deployment时(比如修改Pod模板中的镜像版本),Kubernetes就会执行滚动更新:
- Kubernetes会先创建一个新版本的Pod;
- 新Pod启动后,就绪探针会开始探测其是否已经就绪;
- 只有当新Pod通过就绪探针检查后,Kubernetes才会将其加入到Service的Endpoint中,开始向其发送流量;
- 与此同时,Kubernetes会逐渐减少旧版本Pod的数量,直到所有旧Pod都被替换为止。
在整个滚动更新过程中,就绪探针起到了这些作用:
- 它确保只有就绪的新Pod才会被加入到Service中,接收流量。这避免了将请求发送到尚未准备好的Pod,保证了服务的连续可用;
- 它与Kubernetes的滚动更新策略配合,实现了平滑的版本切换。在任何时刻,总有一定数量的Pod(新版本+旧版本)可以处理请求,从而实现了零停机;
假设我们有一个简单的Web应用,提供一个/接口返回一个问候语,一个/healthz
接口用于健康检查:
const express = require('express'); const app = express(); // 应用版本 const version = process.env.APP_VERSION || 'v1'; // 问候接口 app.get('/', (req, res) => { res.send(`Hello, world! This is ${version}.`); }); // 健康检查接口 app.get('/healthz', (req, res) => { // 检查应用的关键依赖,比如数据库连接 const dbConnected = checkDatabaseConnection(); if (dbConnected) { res.sendStatus(200); } else { res.sendStatus(500); } }); // 启动服务器 const port = process.env.PORT || 8080; app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`); }); // 检查数据库连接的函数(这里只是一个示例,实际情况根据应用的架构而定) function checkDatabaseConnection() { // ... return true; // 假设数据库连接正常 }
我们将这个应用打包成两个版本的Docker镜像:myapp:v1
和myapp:v2
,它们的唯一区别是环境变量APP_VERSION
的值不同。
然后,我们创建一个Deployment来运行这个应用,并设置就绪探针:
apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myapp:v1 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: myapp spec: selector: app: myapp ports: - port: 80 targetPort: 8080 type: LoadBalancer # 使用LoadBalancer类型,便于从集群外部访问
我们同时创建了一个Service,将流量导向带有app: myapp
标签的Pod。当我们应用这些资源时,Kubernetes会创建3个myapp:v1
的Pod。
一旦这些Pod通过就绪探针检查,它们就会被添加到myapp
服务的Endpoint中,开始接收流量。
现在,我们可以通过服务的外部IP来访问应用:
kubectl get svc myapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp LoadBalancer 10.98.219.86 123.45.67.890 80:31000/TCP 5m
curl http://123.45.67.890
Hello, world! This is v1.
接下来,我们将Deployment的YAML文件中的镜像更新为myapp:v2
,然后用kubectl apply
来应用这个更改,模拟一次应用升级:
kubectl apply -f myapp-deployment.yaml
deployment.apps/myapp configured
这时,Kubernetes会开始执行滚动更新:
- 创建一个新的
myapp:v2
的Pod; - 等待新Pod通过就绪探针检查;
- 将新Pod添加到服务的Endpoint中,开始向其发送流量;
- 逐渐减少
myapp:v1
的Pod数量,直到全部替换为myapp:v2
。
在这个过程中,我们可以不断地访问服务,观察其返回结果的变化:
while true; do curl http://123.45.67.890; sleep 1; done
Hello, world! This is v1. Hello, world! This is v1. Hello, world! This is v2. Hello, world! This is v1. Hello, world! This is v2. Hello, world! This is v2. ...
可以看到,在滚动更新的过程中,服务会逐渐将流量从v1
切换到v2
,但在任何时刻,服务都是可用的,请求不会失败。这就实现了零停机的滚动更新。
如果我们在更新过程中描述Deployment的状态,就可以清楚地看到整个过程:
kubectl describe deploy myapp
... Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable Progressing True ReplicaSetUpdated OldReplicaSets: myapp-5f88c99fb8 (1/1 replicas created) NewReplicaSet: myapp-658d4f5d67 (2/2 replicas created) ...