今天在维护预生产环境的数据库的时候,发生了一个灾难性的故障(还好不是生产环境),集群中除了eureka和zuul的其他服务全部springboot服务都变成了不可用状态,容器在不停的重启中,出现这种情况,一般是Liveness和Readiness健康检查失败了。
为什么会出现这样雪崩式的故障?接下来阿汤博客就kubernetes自愈和springboot健康检查分享下具体原因。
一、Kubernetes自愈
Kubernetes强大的自愈能力毋容置疑,如何自愈?默认实现方式是自动重启发生故障的容器,在非kubernetes集群中,要实现自愈是非常困难的一件事情。
那怎么判定容器发生了故障呢?kubernetes提供了Liveness和Readiness 探测机制,检测容器的健康度。
Liveness 探测让用户可以自定义判断容器是否健康的条件。如果探测失败,Kubernetes 就会重启容器,实现自愈。
Readiness 探测则是告诉 Kubernetes 什么时候可以将容器加入到 Service 负载均衡池中,对外提供服务。
Liveness VS Readiness 探测
1、Liveness 探测和 Readiness 探测是两种 Health Check 机制,如果不特意配置,Kubernetes 将对两种探测采取相同的默认行为,即通过判断容器启动进程的返回值是否为零来判断探测是否成功。
2、两种探测的配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness 探测是重启容器;Readiness 探测则是将容器设置为不可用,不接收 Service 转发的请求。
3、Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。用 Liveness 探测判断容器是否需要重启以实现自愈;用 Readiness 探测判断容器是否已经准备好对外提供服务。
我们项目健康探测配置:
readinessProbe: initialDelaySeconds: 10 periodSeconds: 12 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 httpGet: path: /health port: 8080 livenessProbe: initialDelaySeconds: 300 periodSeconds: 12 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 httpGet: path: /health port: 8080
二、SpringBoot的spring boot actuator中的监控检查机制
springboot服务中引用了依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
以后,对应的服务就提供了端点/health。
Actuator 的 health 端点是主要用来检查应用的运行状态,我们项目在kubernetes集群中也是使用此端点对springboot服务做的健康状态检测。
由于今天之前一直没对Actuator 的 health 深入研究,导致出现了今天的雪崩故障。这也是为什么我维护数据库的时候,导致所有服务都出现了不可用,无限重启的情况。
故障一开始虽然知道是健康检查失败了,但还是充满了疑惑,为什么失败呢?因为这期间除了维护了数据库,没做其他操作。
于是停掉了开发环境的数据库,马上去查看开发环境springboot服务pod的状态,没过多久服务全部为不可用状态,然后就开始重启容器尝试恢复。
经过反复测试,发现只要数据库一停,服务的/health端点,返回的就是DOWN状态。
我去查阅了关于spring boot actuator健康检查相关的文章,原来默认情况下/health端点会检测数据库连接、Redis链接、RocketMQ、diskSpace等等这些和服务相关的中间组件的状态,只要有一个不正常,则健康检查为失败。
名称 | 键 | 描述 |
---|---|---|
ApplicationHealthIndicator | none | 永远为up |
DataSourceHealthIndicator | db | 如果数据库能连上,则内容是up和数据类型,否则为DOWN |
DiskSpaceHealthIndicator | diskSpace | 如果可用空间大于阈值,则内容为UP和可用磁盘空间,如果空间不足则为DOWN |
JmsHealthIndicator | jms | 如果能连上消息代理,则内容是up和JMS提供方的名称,否则为DOWN |
MailHealthIndicator | 如果能连上邮件服务器,则内容是up和邮件服务器主机和端口,否则为DOWN | |
MongoHealthIndicator | mongo | 如果能连上MongoDB服务器,则内容是up和MongoDB服务器版本,否则为DOWN |
RabbitHealthIndicator | rabbit | 如果能连上RabbitMQ服务器,则内容是up和版本号,否则为DOWN |
RedisHealthIndicator | redis | 如果能连上服务器,则内容是up和Redis服务器版本,否则为DOWN |
SolrHealthIndicator | solr | 如果能连上solr服务器,则内容是up,否则为DOWN |
因为我Liveness和Readiness探测都采用的spring boot actuator的/health端点,所以当我停掉mysql以后,所有使用了mysql的服务监控检查全部为DOWN状态,导致触发了kubernetes的重启自愈。
这样就导致所有服务都会不停的重启,导致服务器CPU一直处于满负载,然后就形成了连锁反应,可能会导致其他pod因为健康检查超时,出现失败,然后又重启的情况。
有了上面的问题,就需要对当前的健康检测配置进行优化调整。
优化思路:
当redis、mysql、MQ等中间服务出现短暂的网络超时或者不可用时,此时服务只是无法正常处理请求,本身并没有出现致命故障(比如:OOM),重启服务也无法恢复正常。
所以此时不应该让kubernetes去重启容器,只需要让他把容器标记为不可用即可,等网络、或者相关中间件恢复正常再标记为可用状态,提供正常服务即可。
优化以后:
readinessProbe: initialDelaySeconds: 10 periodSeconds: 12 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 httpGet: path: /health port: 8080 livenessProbe: initialDelaySeconds: 300 periodSeconds: 12 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 tcpSocket: #或者使用httpGet path为/info port: 8080
由于我们的springboot服务并不是通过kubernetes的service去提供对外服务,标记为不可用以后没有什么实际意义,eureka server本身自己会对注册中的服务做健康状态检测,剔除不健康的服务。
这里需要注意,默认情况下eureka-server并不是通过spring boot actuator的/health端点进行健康状态探测,而是探测eureka client进程是否正常运行。
所以数据库、redis等出现故障,默认情况eureka 并不会把服务标记为down,我们需要在服务配置中添加eureka.client.healthcheck.enabled=true让eureka使用spring boot actuator的/health端点来对服务做健康探测。