数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(一)
背景
这篇文章是写给有缘人的,为什么这么说呢,因为本篇主要讲讲数据库连接池之c3p0-0.9.1.2版本。
年轻的朋友,可能没怎么听过c3p0了,或者也仅限于听说,这都很正常,因为c3p0算是200几年时比较流行的技术,后来,作者消失了好几年,12年重新开始维护,这时候已经出现了很多第二代线程池了,c3p0已经不占优势,就这样,又维护了几年,直到19年彻底停止更新。
看下其版本历史吧,一开始的maven坐标是这样的:
<!-- https://mvnrepository.com/artifact/c3p0/c3p0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
</dependency>
07年发了最后一个版本c3p0-0.9.1.2:
再下一个版本是2012年的0.9.2-pre2-RELEASE,来到了2012年,坐标改成了:
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</dependency>
后续的更新版本如下:
可以看到,维护到15年后,又消失了几年,直到19年又重新维护了一年,然后就再无动静。
所以,为啥我觉得还是可以讲讲c3p0-0.9.1.2这个版本呢,因为据说当年还是比较火的,很多那时候的项目都用了这个版本,然后就一直再没有升级(想升也没得升啊),所以,我估计,如果那些老项目还在维护的话,估计有不少有缘人还在和这个c3p0-0.9.1.2打交道,我,就是其中一个。
在一些求稳的行业,线上能跑的项目,那肯定是没人会去大动的,只会不断地添砖加瓦,而这也导致更难大动,如果没被重构掉的话,就遗留到了现在。
我现在手里的维护的一个项目,就是用的这个框架,而且,它很容易有bug,不信的话,搜索看看:
本文,就打算来讲讲我遇到的问题和这个框架的0.9.1.2版本的大概的源码逻辑。
我遇到的线上问题
我目前手里这套服务的代码框架应该是0几年诞生的,不是市面上曾经流行的框架,如struts、spring mvc那些,而是c++开发的类比netty、servlet容器的东西,在监听端口收到客户端请求后,能根据请求中的功能id来反向调用对应的java代码,还是有点东西的。而java代码里也是一套框架,框架源码还失传了,框架里代码定死了用c3p0这个来创建数据库连接池,导致我想换也不好换,比较费劲。
业务层呢,托了jdbc规范的福,就是只和jdbc的api打交道,比如找datasource拿connection,这个拿,一般也就是从连接池里面取,用完了,再调用connection.close(内部会把连接再还回连接池)。
所以,我们线上到底有啥问题呢?具体表现就是,业务会突然在某个时刻,调用datasource.getConnection的时候,取不到连接,直接超时,而且是全部的业务请求都出这个问题,这时候,服务基本就hang死了,前端一直转圈。
这个是完全随机的,不定时地炸,每次炸了后,就要靠运维同事重启服务,重启后,服务就好了。
下面来说说定位的过程吧,现在其实也没找到根本原因,只是有了解决的办法和一些猜测,可以等下次再出现的时候,验证一下。
定位初始
刚开始的时候,线上服务只有日志,而且只有error日志,那基本看不出个啥,就是大片大片的等待从连接池获取连接,最后直到超时都获取不到的报错。
当时苦于没有其他手段,又是偶现,也看不出个啥,找dba了也看过db,dba表示运行稳定,当然,dba说的也不一定准,反正是没收获。
后来,2月份的时候,搞了个脚本,服务出现问题的时候,先执行下脚本,打印下jstack、jmap、netstat、top等一些东西,而一开始的时候,运维经常忘记执行,直接就重启了,于是只能等下次,直到2月底的某一天吧,总算是执行了下脚本,拿到了jmap等信息。
jmap确认直接原因
查看资源池现状
分析jmap,个人习惯用MAT。MAT支持object query language语言进行堆对象查询,具体语法可以自己学一学。
我就如下图所示,查询连接池的情况,我这边有多数据源,所以有多个连接池,其中有问题的那个连接池,池子里维护的连接有40个:
这里有必要说一下,这个managed:
/* keys are all valid, managed resources, value is a PunchCard */
HashMap managed = new HashMap();
这个hashmap,就是连接池。
初始化连接池--维护managed、unused
那么,它是怎么初始化的呢,以下面的参数举例:
<property name="minPoolSize">10</property>
<property name="maxPoolSize">50</property>
在BasicResourcePool的构造函数中,就会调用如下方法:
//start acquiring our initial resources
ensureStartResources();
private void ensureStartResources()
{ recheckResizePool(); }
具体就会调用:
private void expandPool(int count)
{
for (int i = 0; i < count; ++i)
taskRunner.postRunnable( new AcquireTask() );
}
c3p0会计算出,需要建10个连接出来,上面的count就是10,那么会new 10个runnable,提交给线程池执行,在每个线程执行时:
private void doAcquire() throws Exception
{
// 1 交给具体的manager去获取底层连接
Object resc = mgr.acquireResource();
...
// 2 拿到连接后,维护到池子里
assimilateResource(resc);
}
这里的mgr,负责具体去创建数据库连接,由于涉及到多种数据库,因此mgr就负责具体脏活累活,连接池这边就不和这些脏话累活打交道,就是类似于我们代码分层架构中的,用来操作redis、es、第三方服务等的一个层,相当于把一些通用的业务逻辑下沉。
而上面2处的代码,就负责池子维护:
private void assimilateResource( Object resc ) throws Exception
{
// 1
managed.put(resc, new PunchCard());
// 2
unused.add(0, resc);
// 3
this.notifyAll();
}
这里的1处,就会往managed里面存放连接,key就是创建的连接,那么value是啥呢?
final static class PunchCard
{
long acquisition_time;// 创建时间
long last_checkin_time; //上次归还到连接池的时间
long checkout_time; // 从连接池借出的时间,未借出时,值为-1
Exception checkoutStackTraceException; // 被借出时,该借出线程的堆栈
PunchCard()
{
this.acquisition_time = System.currentTimeMillis();
this.last_checkin_time = acquisition_time;
this.checkout_time = -1;
this.checkoutStackTraceException = null;
}
}
Punchcard这个词,翻译的意思是:穿孔卡(旧时把信息打成一排排的小孔,用以将指令输入计算机等),我这边就理解成这个数据库连接的一些记录出借/归还信息的卡片。
里面有个checkout_time字段,初始化的值是 -1,表示未被出借。
另外,还有个重要字段,unused,这个主要是存放可供出借的连接。
/* all valid, managed resources currently available for checkout */
LinkedList unused = new LinkedList();
上面的2处,就会把新的连接往这里面放,放完后,用notify通知其他消费者线程。
综上所述,刚开始的时候,
<property name="minPoolSize">10</property>
<property name="maxPoolSize">50</property>
managed的size是10,unused也是10
连接池出借连接的逻辑
检查unused是否有空闲连接
private synchronized Object prelimCheckoutResource( long timeout ){
int available = unused.size();
if (available == 0){
// 检查是否可以扩容,可以的话,触发扩容后开始等待。扩容也是异步的,扩容成功的话,unused的size就大于0
...
}
Object resc = unused.get(0);
// 检查连接是否过期了,如果过期了,这个连接不能要,得销毁
if ( shouldExpire( resc ) )
{
removeResource( resc );
ensureMinResources();
return prelimCheckoutResource( timeout );
}
else
{ // 连接可用,那就从unused中摘除本连接并返回
unused.remove(0);
return resc;
}
}
检查连接是否真实有效
boolean refurb = attemptRefurbishResourceOnCheckout( resc );
if (!refurb)
{
removeResource( resc );
ensureMinResources();
resc = null;
}
这个步骤类似于在连接上执行一个select 1,检查连接到底能不能用。不能用的话,销毁连接。
借到连接后,维护出借卡片
PunchCard card = (PunchCard) managed.get( resc );
card.checkout_time = System.currentTimeMillis();
if (debug_store_checkout_exceptions)
card.checkoutStackTraceException = new Exception("DEBUG ONLY: Overdue resource check-out stack trace.");
这里就是,获取到这个连接的punchCard信息卡,然后登记出借的时间为当前时间,那么,是谁借了呢,这里是通过new一个异常的方式,通过这个异常,就能知道当前线程的堆栈。
用完后,归还连接给连接池
if (managed.keySet().contains(resc))
doCheckinManaged( resc );
这个归还呢,如下,也不是直接归还,竟然也是new一个runnable去归还,个人觉得,这个有巨大的隐患,因为线程池是可能会堵的,而这个就极有可能导致还不进去。
private void doCheckinManaged( final Object resc ){
Runnable doMe = new RefurbishCheckinResourceTask();
taskRunner.postRunnable( doMe );
}
class RefurbishCheckinResourceTask implements Runnable
{
public void run()
{
// 1 归还前试着测试下连接是否能用,比如select 1
boolean resc_okay = attemptRefurbishResourceOnCheckin( resc );
// 2 获取卡片并更新卡片
PunchCard card = (PunchCard) managed.get( resc );
if ( resc_okay && card != null)
{
// 3
unused.add(0, resc );
card.last_checkin_time = System.currentTimeMillis();
card.checkout_time = -1;
}
BasicResourcePool.this.notifyAll();
}
}
这里主要就是,归还前先测试下连接是不是好的,免得还个坏的进去;再就是,拿到之前的出借卡,更新归还时间为当前时间、借出时间改为-1;再把连接放回到unused空闲链表。
现状反映出的问题
问题如下,空闲链表为空,连接池被出借一空:
随便找了个连接看出借时间:
这个时间,距离执行jmap的时候,已经过去了一分钟了,而大部分的punchCard都是这样,这说明了什么,说明了这些连接被借出去一分钟了,都还没有归还到unused空闲链表,导致空闲链表size为0,后续的请求在unused上死等也等不到连接(因为managed已经达到池子的最大值了,也没法扩容),于是超时。
问题根因如何找
现在看起来,直接原因是找到了,就是有连接泄露,但是具体是哪里有泄露呢?是不是真的有泄露呢?感觉长路仍漫漫,继续努力吧。
留到下篇继续吧,天也晚了,现在早上上班早,晚上不早点睡真是扛不住。
数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(一)的更多相关文章
- JDBC学习笔记(8)——数据库连接池(dbcp&C3P0)
JDBC数据库连接池的必要性 一.在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤: 1)在主程序(如servlet/beans)中建立数据库连接 2)进行sql操作 3)断开数据库连接 ...
- 【转】JDBC学习笔记(8)——数据库连接池(dbcp&C3P0)
转自:http://www.cnblogs.com/ysw-go/ JDBC数据库连接池的必要性 一.在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤: 1)在主程序(如servlet/ ...
- 主流Java数据库连接池分析(C3P0,DBCP,TomcatPool,BoneCP,Druid)
主流数据库连接池 常用的主流开源数据库连接池有C3P0.DBCP.Tomcat Jdbc Pool.BoneCP.Druid等 C3p0: 开源的JDBC连接池,实现了数据源和JNDI绑定,支持JDB ...
- java攻城狮之路--复习JDBC(数据库连接池 : C3P0、DBCP)
复习数据库连接池 : C3P0.DBCP 1.数据库连接池技术的优点: •资源重用: 由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销.在减少系统消耗的基础上,另一方面也增 ...
- 开源数据库连接池之C3P0
本篇介绍几种开源数据库连接池,同时重点讲述如何使用C3P0数据库连接池. 之前的博客已经重点讲述了使用数据库连接池的好处,即是将多次创建连接转变为一次创建而使用长连接模式.这样能减少数据库创建连接的消 ...
- 数据库连接池(c3p0)
(一)问题的提出: 在使用开发基于数据库的web程序时,传统的数据库使用模式按照以下步骤: 在程序中建立数据库连接 进行sql操作 断开数据库连接 但是,这种模式存在着移动的问题: 传统连接模式每次向 ...
- java之数据库连接池-dbcp&c3p0&dbutils
介绍 因为数据库连接对象的创建比较消耗性能,所以可以在应用程序启动时就在内存中开辟一片空间(集合)存放多个数据库连接对象,后面需要连接时直接从该空间中取而不用新创建:使用完毕后归还连接(将连接重新放回 ...
- 数据库连接池之C3P0
一.C3P0的使用步骤 1:导入相关的依赖jar包 c3p0-0.9.5.2.jar mchange-commons-java-0.2.12.jar 2:代码实现 A:硬编码的实现方式 package ...
- 第14章—数据库连接池(C3P0)
spring boot 系列学习记录:http://www.cnblogs.com/jinxiaohang/p/8111057.html 码云源码地址:https://gitee.com/jinxia ...
- java_JDBC,连接数据库方式,RestSet结果集,Statement,PreparedStatement,事务,批处理,数据库连接池(c3p0和Druid)、Apache-DBUtils、
一.JDBC的概述 1.JDBC为访问不同的数据薛是供了统一的接口,为使用者屏蔽了细节问题.2. Java程序员使用JDBC,可以连接任何提供了JDBC驱动程序的数据库系统,从而完成对数据库的各种操作 ...
随机推荐
- 机器学习(六):回归分析——鸢尾花多变量回归、逻辑回归三分类只用numpy,sigmoid、实现RANSAC 线性拟合
[实验1 回归分析] 一. 预备知识 使用梯度下降法求解多变量回归问题 数据集 Iris 鸢尾花数据集是一个经典数据集,在统计学习和机器学习领域都经常被用作示例.数据集内包含 3 类共 150 条记录 ...
- Mybatis 坑(1)
org.apache.ibatis.executor.ExecutorException: No constructor found in xxxx [Integer,String] 这种情况一般是类 ...
- 深入理解python虚拟机:黑科技的幕后英雄——描述器
深入理解python虚拟机:黑科技的幕后英雄--描述器 在本篇文章当中主要给大家介绍一个我们在使用类的时候经常使用但是却很少在意的黑科技--描述器,在本篇文章当中主要分析描述器的原理,以及介绍使用描述 ...
- 粘包/拆包问题一直都存在,只是到TCP就拆不动了。
OSI open-system-Interconnection TCP/IP 5层协议栈 应用层和操作系统的边界是 系统调用 ,对应到网络编程是socket api TCP/UDP 概况 TCP粘包问 ...
- 【Vue2】编程式路由导航
在Vue Router中,除了使用 创建 a 标签来定义导航链接之外,还可以使用Vue Router通过编写代码来实现导航. 他提供的三个实例方法:router.push.router.replace ...
- 前端Vue项目打包性能优化方案
一.前言 Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM:但 Vue 项目中仍 ...
- 代码打包的可视化数据分析图: webpack-bundle-analyzer 的使用
先看webpack-bundle-analyzer的效果图(官方效果图): 通过使用webpack-bundle-analyzer可以看到项目各模块的大小,可以按需优化 1.先安装 npm insta ...
- Sourcetree 提交顺序
总结:一共5个步骤 1.首先获取git主分支的代码. 2.暂存所需要上传的代码. 3.拉取代码(如发生文件冲突先暂不处理). 4.提交代码,然后再次拉取代码(不显示冲突跳下一步).如果还是显示文件冲突 ...
- 2023-04-02:设计一个仓库管理器,提供如下的方法: 1) void supply(String item, int num, int price) 名字叫item的商品,个数num,价格pri
2023-04-02:设计一个仓库管理器,提供如下的方法: void supply(String item, int num, int price) 名字叫item的商品,个数num,价格price. ...
- 2020-09-19:TCP状态有哪些?
福哥答案2020-09-19:#福大大架构师每日一题# [答案来自此链接](https://www.zhihu.com/question/421833613) 11种状态1.CLOSED状态:初始状态 ...