线上问题总结-获取不到连接池(logback 配置+代码问题)

原文链接

出错问题复盘

1、问题回顾

早上 6:00 多开始报警,数据库连接池获取不到连接,日志的报错如下:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
### The error may exist in class path resource [com/ xxx/xxxMapper.xml]
### The error may involve xxx.getListByOrgIdOrRealName
### The error occurred while executing a query
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms  

导致的结果就是:

40 多笔提现失败,因为那个时候刚好有客户要提现

2、定位问题

2.1 首先看应用日志

看应用日志,最主要的信息就是 Failed to obtain JDBC Connection、HikariPool-1 - Connection is not available,request timed out after 30000ms
1、看这个信息的第一反应就是数据库连接是不是被打满了,先去看了数据库监控,发现连接数很平稳,没有太大的变化(zabbix 的监控)。

2、然后数据库连接没有很多的话,第 2 步去看看 Hikari 连接池的配置,配置如下:

spring.datasource.hikari.connectionTimeout = 100000

spring.datasource.hikari.validationTimeout = 300000

spring.datasource.hikari.maximum-pool-size = 200

spring.datasource.hikari.minimum-idle = 50

3、当时是想着把 maximum-pool-size=500(就是 200 调整为 500),当然此时还没有改,也就没有发布、重启

2.2 看 jstack 日志
2.1 jsatck 日志如下
"xxxx_Worker-1" #206 prio=5 os_prio=0 tid=0x00007f508f6ab000 nid=0x1705 waiting on condition [0x00007f4f61818000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)

parking to wait for <0x0000000720ab5208> (a java.util.concurrent.locks.AbstractQueuedSynchronizerC o n d i t i o n O b j e c t ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . L o c k S u p p o r t . p a r k ( L o c k S u p p o r t . j a v a : 175 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . A b s t r a c t Q u e u e d S y n c h r o n i z e r ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:175)atjava.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
at ch.qos.logback.core.AsyncAppenderBase.putUninterruptibly(AsyncAppenderBase.java:181)
at ch.qos.logback.core.AsyncAppenderBase.puto r i g i n a l originaloriginalXaWJEOe0(AsyncAppenderBase.java:172)
at ch.qos.logback.core.AsyncAppenderBase.puto r i g i n a l originaloriginalXaWJEOe0$accessor67 X f D E R 7 ( A s y n c A p p e n d e r B a s e . j a v a ) a t c h . q o s . l o g b a c k . c o r e . A s y n c A p p e n d e r B a s e 67XfDER7(AsyncAppenderBase.java) at ch.qos.logback.core.AsyncAppenderBase67XfDER7(AsyncAppenderBase.java)atch.qos.logback.core.AsyncAppenderBaseauxiliary$fPj3kqdc.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
2.2 观察 jstack 日志分析

通过看 jstack 日志发现,有几百个 parking to wait for <0x0000000720ab5208> 这个日志,而且下面也是相同的输出,调用栈是在 logback 的一个 putUniteruptibly 方法上,然后去追踪应用里的调用栈,发现都是卡在了打印日志的输出上(log.info("{}", xxx))。

2.3 猜测
2.3.1 看日志配置

因为都是卡在打印日志上,所以想打印日志是不是有什么问题。然后就想到了前一天刚上线了应用日志的异步打印配置,配置如下 :

<!--设置为异步打印-->
<appender name="debug-async-appender" class="ch.qos.logback.classic.AsyncAppender">
  <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
  <queueSize>512</queueSize>
  <appender-ref ref="debugAppender"/>
</appender>

<!--设置为异步打印-->
<appender name="error-async-appender" class="ch.qos.logback.classic.AsyncAppender">
  <discardingThreshold >0</discardingThreshold>
  <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
  <queueSize>512</queueSize>
  <appender-ref ref="errorAppender"/>
</appender>
2.3.2 追踪 logback 异步打印方法

因为看 jstack 日志的时候发现调用栈是:

at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)

at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)

at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)

at ch.qos.logback.core.AsyncAppenderBase.putUninterruptibly(AsyncAppenderBase.java:181)

可以看到是有一个阻塞队列在阻塞排队。我们直接看源码

2.3.3 看源码
//这个就是阻塞队列的声明。
BlockingQueue<E> blockingQueue;

private void putUninterruptibly(E eventObject) {
    boolean interrupted = false;

  //可以看到这里就是一个 while true 的死循环,然后不断的往 blockingQueue 中放对象
  try {
    while(true) {
      try {
        this.blockingQueue.put(eventObject);
        return;
      } catch (InterruptedException var7) {
        interrupted = true;
      }
    }
  } finally {
    if (interrupted) {
      Thread.currentThread().interrupt();
    }

  }
}

//再看一下 ArrayBlockingQueue 的 put 操作
public void put(E e) throws InterruptedException {
  Objects.requireNonNull(e);
  //可以看到这里就是一个锁操作,加锁
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    while (count == items.length)
      notFull.await();//一直阻塞等待
    enqueue(e);
  } finally {
    lock.unlock();
  }
}

由此可以看出,因为配置的是有界队列,512,然后此时可能满了,然后新来的对象就要阻塞等待出队,然后自己才能入队。所以才会在 jstack 日志中看到 400 多个 wait for 同一个对象的日志

2.4 尝试解决
2.4.1 修改 logback 配置
<!--设置为异步打印-->
<appender name="debug-async-appender" class="ch.qos.logback.classic.AsyncAppender">
  <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
  <queueSize>512</queueSize>
  <appender-ref ref="debugAppender"/>
  <neverBlock>true</neverBlock>
</appender>
<!--设置为异步打印-->
<appender name="error-async-appender" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold >0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<appender-ref ref="errorAppender"/>
<neverBlock>true</neverBlock>
</appender>

添加 neverBlock 的配置,添加这个配置后,队列默认走的不是 put 文件,是 offer 方法

//AsyncAppenderBase 的方法
private void put(E eventObject) {
  //配置了之后,就走 offer 方法
  if (this.neverBlock) {
    this.blockingQueue.offer(eventObject);
  } else {
    this.putUninterruptibly(eventObject);
  }
}

//看一下 offer 方法
public boolean offer(E e) {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果列队满了,直接返回 false
if (count == items.length)
return false;
else {
//否则入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}

也就是说,设置了 neverBlock 之后,就会把这些丢弃掉,不入队,也就是说可能会丢日志

2.5 测试环境还原

测试环境还原的时候,因为猜测是数据库连接池的问题,所以修改了连接池配置,最小、最大设置为 5,去压测,还是没有还原这个情况。然后继续看应用日志有新发现

2.6 日志大小跟其他同区间的比很大

相同的日志切割,发现 6 点的那个时间点特别大,其他的就几十 M,但是 6 点区间的有 100 多 M,快 200M了,所以就看看到底是啥情况。然后就发现了,有一个身份证认证的异常信息,返回的异常信息特别大,导致了 log 的时候卡了。

就是说,本来 log.error("", e),这个最多几十 ms 左右就左右了,但是呢,这个对象特别大,然后就导致最终 io 写入日志信息的时候耗时太长。

2.7 找到原因,分析验证
2.7.1 找出代码中这种情况的日志打印,然后把这段代码去除
2.7.2 分析

1)、因为这个是调用第 3 方的人脸识别的接口,接口返回的 Base64 的照片信息,这个信息很大,导致 logback 打印日志写这个日志的时候很耗时。
2)、因为有些类、方法上加了 @Transactional,导致在 logback 执行的时候(logger.info/logger.error 在 finally 中执行),连接没有被释放,所以其他的请求执行的时候获取不到连接。

2.8 发布上线

上线之后,第 2 天相同时间发现没有再出现这样的问题,至此解决

2.9 模拟那种场景,还原异常
2.9.1 代码
1.application.properties
#这里为了模拟极限场景,直接将连接数置为 1 
spring.datasource.hikari.connectionTimeout = 10000
spring.datasource.hikari.validationTimeout = 300000
spring.datasource.hikari.maximum-pool-size = 1
spring.datasource.hikari.minimum-idle = 1
2.logback-spring.xml
<appender name="error-async-appender" class="ch.qos.logback.classic.AsyncAppender">
    <discardingThreshold >0</discardingThreshold>
    <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
    <queueSize>512</queueSize>
    <appender-ref ref="errorAppender"/>
</appender>
3.controller/service/mapper
//Controller
@Slf4j
@RequestMapping("/hikari-demo/")
@RestController
public class TestController {
@Resource
private TaskOrderService taskOrderService;

@GetMapping("list")
public List<TaskOrder> getList() {
return taskOrderService.selectByAll();
}

@GetMapping("order")
public TaskOrder selectById() {
return taskOrderService.selectById();
}
}

@Service
@Slf4j
public class TaskOrderServiceImpl implements TaskOrderService {
@Resource
private OrderMapper orderMapper;

@Transactional(readOnly = true)
@Override
public List<TaskOrder> selectByAll() {
List<TaskOrder> taskOrders = orderMapper.selectAll();
try {
log.info("{}", 1);
//这里就是模拟日志写很大文件的操作(大于数据库连接池的连接超时时间)
TimeUnit.SECONDS.sleep(15);
} catch (Exception e) {
} finally {
}
return taskOrders;
}

@Override
public TaskOrder selectById() {
Long id = RandomUtil.randomLong(1000);
TaskOrder taskOrder = orderMapper.selectById(id);
try {
log.info("{}", id);
TimeUnit.SECONDS.sleep(15);
} catch (Exception e) {
} finally {
}
return taskOrder;
}
}

//Mapper/Mapper.xml
public interface OrderMapper {
  List<TaskOrder> selectAll();

  TaskOrder selectById(Long id);
}
<mapper namespace="com.bamboo.hikaridemo.mapper.OrderMapper">
    <resultMap id="allMap" type="com.bamboo.hikaridemo.TaskOrder">
        <id column="id" property="id"/>
        <result column="create_date" property="createDate"/>
    </resultMap>

    <select id="selectAll" resultMap="allMap">
        select *
        from task_order
        limit 10
    </select>

    <select id="selectById" resultMap="allMap">
        select *
        from task_order
        WHERE id = #{id}
    </select>
</mapper>
2.9.2 压测
bash 复制代码
--同时用10 个用户 100 个请求去测试
ab -c 10 -n 100 http://localhost:8089/hikari-demo/order ab -c 10 -n 100 http://localhost:8089/hikari-demo/list
2.9.3 场景还原

2021-07-15 11:25:34.915 ERROR 31485 --- [nio-8089-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:

Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 10004ms.

The error may exist in file [/Users/zhuzi/Desktop/hikari-demo/target/classes/mapper/OrderMapper.xml]

The error may involve com.bamboo.hikaridemo.mapper.OrderMapper.selectById

The error occurred while executing a query

Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 10004ms.] with root cause

java.sql.SQLTransientConnectionException**: HikariPool-1 - Connection is not available, request timed out after 10004ms**.
可以看到压测开始 1 次执行后,第 2 次的时候就报了连接超时,获取不到连接的错误

2.10 总结

至此,这个问题解决了,然后也还原了问题发出的场景,所以以后还是要注意引入一个技术的时候要多了解更多的核心原理。

2.10.1 打印日志一定要注意,不能打印太大的信息

尤其是调用第 3 方的接口里,一定要搞清楚他的返回信息,再抛出相应的信息,要不出现这种的就可能会出问题。

2.10.2 引入新的东西后,一定要多测试,多考虑极端情况
相关推荐
play_big_knife4 分钟前
鸿蒙项目云捐助第二十八讲云捐助项目首页组件云数据库加载轮播图
数据库·华为·harmonyos·鸿蒙·云开发·鸿蒙开发·鸿蒙技术
qq_321665331 小时前
mysql 数据库迁移到达梦数据库
数据库·mysql
Hello.Reader2 小时前
Redis大Key问题全解析
数据库·redis·bootstrap
靖顺4 小时前
【OceanBase 诊断调优】—— packet fly cost too much time 的根因分析
数据库·oceanbase
liuxin334455664 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
yuanbenshidiaos7 小时前
C++--------------树
java·数据库·c++
dengjiayue8 小时前
MySQL 查询大偏移量(LIMIT)问题分析
数据库·mysql
言之。8 小时前
【MySQL】在MySQL中如何定位慢查询?
数据库·mysql
DashVector8 小时前
如何通过HTTP API插入Doc
数据库·人工智能·http·阿里云·向量检索
DashVector9 小时前
如何通过HTTP API分组检索Doc
服务器·数据库·http·数据库开发·数据库架构