likes
comments
collection
share

排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题

作者站长头像
站长
· 阅读数 22

1. 异常问题场景

  • 凌晨2点二十分左右,生产环境base基础服务有两个节点不能提供服务了(consul健康检查超时导致),经过我们排查,初步定位到这个时间点xxx业务那边修改了消息订阅的回调地址,触发了“xxxx”这个产品下所有项目的消息重新订阅,由于xxxx这个产品下项目有1000多个,base基础服务这边又是开多线程去处理的,怀疑是这个操作导致base基础服务的资源被过度占用导致健康检查超时😬。

下面是当时在网上看到的健康检查接口超时原因猜测

  1. 健康检查接口检查某个组件的时候被阻塞住了。比如说数据库如果卡住,数据库健康检查线程会超时没有返回成功响应。 (先说结果 还真的是数据库卡住了,连接数量被消耗完了😍)
  2. 另一个猜测是Http线程池没来得及处理健康检查请求,请求就超时了,业务在启动过程中,会概率性出现大量线程阻塞,导致可对外提供服务的HTTP线程非常少,流量进来以后马上出现HTTP线程耗尽,健康检查接口请求失败,服务被K8s杀死。(这也是一个排查方向,但我们业务不是因为这个出现异常)

2. 问题复现,排查过程

  1. 首先向运维那边要要了那个时间点左右的资源监控,当时 FullGC信息查看和CPU资源看起来都没什么问题。
  2. 然后在xn环境将涉及到的服务进行pod单节点部署(方便查看日志), 在Business服务那边有个post接口使用mock接口进行延时200ms模拟(根据线上延时来设定)。
  3. 查看相关接口的逻辑,将产品添加到1500个左右,并且根据代码逻辑修改数据库的相关属性,使它走向update更新消息订阅接口,然后调用消息订阅接口进行触发,Consul上出现节点健康检查超时,K8s有自愈能力,过了一会自动恢复,去看consul日志,也只是一行报连接超时(没什么作用),再去排查CPU资源和内存利用率,使用jstat gcutils 查看FullGC的次数和时间(没什么问题)。
  4. 最后在手动调用更新消息订阅接口的时候,在Linux服务器上使用curl 调用该pod的健康检查接口,发现接口健康线程会被阻塞,超过配置的10s,将会导致失去连接,然后再去排查线程这一块,怀疑是业务用的undertow多线程和健康检查的线程是不是冲突了,后面发现使用的@Async注解没有使用自定义线程池,而是使用Spring自带的SimpleAsyncTask线程池去处理,而这个线程池不会对线程进行复用,而是来一个任务创建一个新的线程(看源码),后面将@Async改成自定义线程池,还是会出现健康检查超时问题
  5. 后面发现,在xn环境上base开8以上个异步线程去调用business模块会阻塞健康检查接口,使用自定义线程池,核心线程池大小设置为7能解决健康检查阻塞问题,但是后续产品数量上去了,并且线程的核心大小设置为7也会业务的处理时间问题。(默认线程池核心大小=7没问题 等于8有问题 那瓶颈就是剩下的这1个线程在帮忙处理一些任务,当任务过多一个任务撑不住)。
  • 排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题
  1. 继续排查在Consul上掉线原因排查,发现业务里面使用两个@Async异步注解嵌套异步注解使用会导致http健康检查线程阻塞,解决方法就是将两个异步方法合成为一个异步方法。(验证失败,错误猜测)

  2. dump线程栈⽂件,观察线程的运⾏情况。 发现部分异步线程都在TIME_WAITING状态异步线程和Consul健康检查中的数据库健康检查线程,居然都在等待获取数据库连接。什么原因导致 数据库连接被耗尽? ⽽且我代码⾥没有进⾏任何的数据库操作,数据库连接到底被谁用完了?

使用Spring Admin 监控线程状态

  • 排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题
  1. 查看代码,发现base基本服务有全局的事务控制(通过切⾯实现),当我每次的使用@Async异步的时候,都会开启⼀个事务控制(@Async和@Transaction注解均采⽤切⾯实现,两个注解同时存在时,由于@Async注解实现了@Order 来控制Bean的执行顺序优先级,Aop切⾯的时候会第⼀个执⾏@Async,所以会先开启异步线程,再在异步线程⾥开启事务@Transaction)。当base基础服务开启了10多个线程的时候,数据库连接就被利用完了。

写个单元测试 对base基础服务里面的Druid连接配置进行监控

@Autowired
private DataSource dataSource;

@Test
public void testDataSource() throws InterruptedException {
  DruidDataSource druidDataSource = (DruidDataSource) dataSource;
  while (true){
    System.out.println("druid active connection : " + ((DruidDataSource) dataSource).getActiveCount() + ", total connection : " + druidDataSource.getMaxActive()
        + ",init connection: " + druidDataSource.getInitialSize());
    Thread.sleep(1000);
  }
}

排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题

  • 发现Druid数据库连接池最⼤连接数为8,在application.yml配置的最大连接数是100,那说明druid配置没有生效 所以连接数很容易耗尽,导致数据库健康检查阻塞, 我擦😭 终于找到原因了 历尽千辛万苦。
# Mysql数据库配置
datasource:
  url: ${MYSQL_URL}
  username: ${MYSQL_USER:root}
  password: ${MYSQL_PASSWORD:root@B0ngm1}
  driver-class-name: com.mysql.cj.jdbc.Driver
  # 设置类型为 DruidDataSource
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
    # 连接池初始化大小
    initial-size: 10
    # 最小空闲连接数
    min-idle: 20
    # 最大连接数
    max-active: 100
    # 获取连接等待超时时间
    max-wait: 5000

排查发现,缺少 关键jar包,参数读取相关的配置

  • 缺少的jar包如下:
<!-- 集成druid连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.9</version>
</dependency>
  • 缺少的配置如下:
package cn.lollypop.www.lollypopv2mallserver.config;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DruidConfig {
  @ConfigurationProperties(prefix = "spring.datasource.druid")
  @Bean
  public DataSource druidDataSource() {
    return new DruidDataSource();
  }
}

排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题

添加配置jar包 进行Bean注入 再次进行单元测试 最大连接大小恢复到100

排查Java服务在consul健康检查超时💥(Druid数据库参数未生效)—线上问题

  • 最后赶紧拉分支, 修改相关代码, 测试环境->预发环境->性能环境->生产环境 完美修复 ✌️。

3. 总结

  • Druid数据库连接池参数配置未⽣效,默认最⼤连接数为8。(修复代码正确读取Druid参数,使yml配置⽣效)

  • 存在全局事务切⾯, 每个请求⾄少会开启⼀个事务,占⽤⼀个数据库连接。存在全局事务切⾯, @Async注解每次开启异步线程时,也会同时开启⼀个事务,占⽤⼀个数据库连接。

  • 取消@Async注解的使⽤,使⽤ybase⾃定义的线程池,控制异步线程的数量,避免起过多的异步线程,导致事务过多,数据库连接被耗尽;避免SimpleAsyncTaskExecutor 线程池⽆限制的创建线程添加到等待队列,导致OOM。

  • 在这次排查错误中,学到的东西很多,比如更加熟练的使用k8s进行操作,对@Async异步处理的方式,多线程理解层面也提升了,也发现软技能非常重要,比如:沟通能力,表达能力。

4. 参考

转载自:https://juejin.cn/post/7376940200915042342
评论
请登录