22. 在销毁空闲的连接(IdleRemover)和无效的连接(ConnectionValidator)后,都会执行一个
prefill 的操作,将连接池中的连接数填充到 min 值,所以,对于连接池 min 需要合理的进行
设置,如果 min 设置过大,JBOSS 会将连接不断的进行销毁->创建->销毁->创建…(idle
线程对空闲连接销毁,销毁后小于 min 值,然后马上又创建, 新创建的连接处于空闲状态,
于是又被销毁…)
总之,我们需要合理的设置连接池 min 值,对于某些系统来说,数据库的连接资源是
很昂贵的。
前段日子在公司的核心系统上优化的一个连接池,主要也是对于 min 值的优化:一个
生产库的 JBOSS 连接池调整优化及分析。对于连接数的调优,后面会专门整理一篇文章。
24. {
//判断连接池是否已经被 shutdown,如果被 shutdown,
//则抛出异常"The pool has been shutdown",并释放信号量
if (shutdown.get())
{
permits.release();
throw new ResourceException("The pool has been shutdown");
}
//如果可用的连接事件监听的 arraylist 大于 0, 则从 arraylist 的尾部取一
个连接
if (cls.size() > 0)
{
cl = (ConnectionListener) cls.remove(cls.size() - 1);
//将 arraylist 中获取的连接加到到 checkdout 的一个 hash 结构中.
checkedOut.add(cl);
//计算已经使用的连接数
int size = (int) (maxSize - permits.permits());
//更新当前已经使用的最大连接数
if (size > maxUsedConnections)
maxUsedConnections = size;
}
}
//如果已经从连接事件监听数组中获取到了连接
if (cl != null)
{
//我们从一个 pool 取一个 ManagedConnection,并检查它是否符合要求?
try
{
Object matchedMC = mcf.matchManagedConnections
(Collections.singleton(cl.getManagedConnection())
,subject, cri);
if (matchedMC != null)
{
if (trace)
log.trace("supplying ManagedConnection from pool: " + cl);
//通知 connection listener 是否它拥有权限
cl.grantPermit(true);
//返回连接,结束
return cl;
}
/*
* 匹配不成功,并且没有异常信息抛出。
25. * 在检查的时候,要么我们匹配错误,要么连接已经死亡
我们需要去辨别这些场景,但是现在,不管怎么样,我们都销毁连
接。
*/
log.warn("Destroying connection that could not be
successfully matched: " + cl);
synchronized (cls)
{
//从 checkout 的 hashset 中删除已经获取的连接。
checkedOut.remove(cl);
}
//销毁连接
doDestroy(cl);
cl = null;
}
//不管发生任何事,都销毁连接
catch (Throwable t)
{
log.warn("Throwable while trying to match ManagedConnection,
destroying connection: " + cl, t);
synchronized (cls)
{
checkedOut.remove(cl);
}
doDestroy(cl);
cl = null;
}
//如果发生意外,我们应该决定是否应该继续尝试去建立连接,
//由 jboss 配置文件中的的 useFastFail 参数来决定,默认为 false。
//这个 useFastFail 设置为 true,则立刻跳出 get connection,并报错。
if(poolParams.useFastFail)
{
log.trace("Fast failing for connection attempt.
No more attempts will be made to acquire connection from pool
and a new connection will be created immeadiately");
break;
}
}
}
//当连接监听队列>0,即还有可用的连接监听器
while (cls.size() > 0);//end of do loop
26. //OK, 我们不能够找到一个可以使用的连接,则新建一个
try
{
//创建一个新的连接。这里不需要判断是否已经到达 max 连接数
//因为前面已经获取了信号量,所以肯定可以创建连接。
cl = createConnectionEventListener(subject, cri);
synchronized (cls)
{ //将创建的连接加入到 checkout 数组中。
checkedOut.add(cl);
int size = (int) (maxSize - permits.permits());
//更新当前已经使用的最大连接数
if (size > maxUsedConnections)
maxUsedConnections = size;
}
//如果连接池还没有启动,则初始化连接池,并设置值 started 为 true。
//这里连接池可能被启用多次(因为非线程安全),但是这里没有危害。
if (started == false)
{
started = true;
if (poolParams.minSize > 0)
PoolFiller.fillPool(this);
}
if (trace)
log.trace("supplying new ManagedConnection: " + cl);
//通知 connection listener 是否它拥有权限
cl.grantPermit(true);
return cl;
}
catch (Throwable t)
{
log.warn("Throwable while attempting to get a new connection: " + cl, t);
//return permit and rethrow
synchronized (cls)
{
checkedOut.remove(cl);
}
permits.release();
JBossResourceException.rethrowAsResourceException(
"Unexpected throwable while trying to create a connection: " + cl, t);
throw new UnreachableStatementException();
}
27. }
//这里的 else 操作,不能获取信号量,则抛出异常,报错连接池超时。
else
{
// we timed out
throw new ResourceException("No ManagedConnections available
within configured blocking timeout ( "
+ poolParams.blockingTimeout + " [ms] )");
}
}
catch (InterruptedException ie)
{
long end = System.currentTimeMillis() - startWait;
connectionCounter.updateBlockTime(end);
throw new ResourceException("Interrupted while requesting permit!
Waited " + end + " ms");
}
}
29. 关于 getConnetion 的几点说明
1.blockingTimeout 是一个 jboss 的参数:
<blocking-timeout-millis >5000</blocking-timeout-millis >,它是一个获取信号量的超
时时间,更确切的说,是从连接池中获取一个连接的超时时间。如果超过 5000ms,不
能够获取到信号量(连接),则 jboss 会抛出异常: “can not get connection,No
ManagedConnections available within configured blocking timeout [xx] ms”。当然,
只要当前正在使用的连接数没有到达 MAX 值,这个信号量一定能够被获取到。因为信
号量一共有 MAX 值个,如果连接池中当前的连接不够用时,在获取信号量之后,新创
建一个连接即可。建议可以设置成可以设成 500ms 左右,不需要设计过大,当应用中
存在多个数据源时,可以防止因 DB 异常线程池带来的阻塞。如果网络环境不好的话,
可以设置的更高一点。
2.连接池内部就是一个连接监听队列,每次都从队列的尾部获取连接。而 IdleRemove
线程,则是从这个队列头部开始进行清理。
3.连接池的获取,销毁,创建,这三个操作都是线程安全的操作。
4.业务在使用连接的过程中,会一直占有这个信号量,在 returnConnection 或者发生
异常时释放信号量。能够获取信号量,则意味着肯定可以获取到连接。第二节中,我们讲到
JBOSS 在启动时会初始化一个信号量数组,长度为连接池的 max 参数。
当业务系统使用完连接后,需要把连接放回到连接池中它的主要见下图,源代码如下:
public void returnConnection(ConnectionListener cl, boolean kill) {
synchronized (cls) {
/*
* 判断连接是否已经被 DESTROYED?
* 可能有其它的线程如 background-validation 及 shuwdown
* 标记这个连接临听器为 DESTORYED 状态。
*
*/
if (cl.getState() == ConnectionListener.DESTROYED) {
if (trace)
log
.trace("ManagedConnection is being returned after it was destroyed"
+ cl);
//释放信号量,并直接返回
if (cl.hasPermit()) {
// release semaphore
cl.grantPermit(false);
permits.release();
}
return;
}
}
if (trace)
30. log.trace("putting ManagedConnection back into pool kill=" + kill
+ " cl=" + cl);
try {
//前台应用强制清理连接。
cl.getManagedConnection().cleanup();
} catch (ResourceException re) {
log.warn("ResourceException cleaning up ManagedConnection: " + cl,
re);
//清理失败,抛出异常,清理失败。
kill = true;
}
synchronized (cls) {
// 连接监听的状态为 DESTROY 或者 DESTROYED,则设置 kill 为 true
if (cl.getState() == ConnectionListener.DESTROY
|| cl.getState() == ConnectionListener.DESTROYED)
kill = true;
//checkedOut 队列中移除连接监听器。
checkedOut.remove(cl);
//如果 kill==false, 并且连接数>=最大连接 max 值, 说明异常发生, 再次设置 kill=true
if (kill == false && cls.size() >= poolParams.maxSize) {
log
.warn("Destroying returned connection, maximum pool size exceeded "
+ cl);
kill = true;
}
//kill 连接
if (kill) {
// Adrian Brock: A resource adapter can asynchronously notify us
// that
// a connection error occurred.
// This could happen while the connection is not checked out.
// e.g. JMS can do this via an ExceptionListener on the
// connection.
// I have twice had to reinstate this line of code, PLEASE DO
// NOT REMOVE IT!
cls.remove(cl);
}
//如果 kill==false
else {
cl.used();
//这个连接监听不属于连接监听队列,则加入。
31. if (cls.contains(cl) == false)
cls.add(cl);
else
log.warn("Attempt to return connection twice (ignored): "
+ cl, new Throwable("STACKTRACE"));
}
if (cl.hasPermit()) {
//释放信号量
cl.grantPermit(false);
permits.release();
}
}
if (kill) {
if (trace)
log.trace("Destroying returned connection " + cl);
//销毁连接。
doDestroy(cl);
}
}
40. prepareStatement 对象。 PreparedStatementCache 使用了一个本地缓存的 LRU 链表来减少
SQL 的预编译,减少 SQL 的预编译,意味着可以减少一次网络的交互和数据库的解析(有
可能在 session cursor cache hits 中命中,也可能是 share pool 中命中),这对应用的 DAO
响应延时是很大的提升。
下面是 JBOSS 源代码中对于 prepareStatementCache 的实现。
BaseWrapperManagedConnection 类是 JBOSS 连接管理最基本的一个抽象类,有两个类继
续自这个抽象类,分别为:
1.LocalManagedConnection 本地事务的连接管理。(一般我们都使用这个类)
2.XAManagedConnection 分布式事务的连接管理
BaseWrapperManagedConnection 类的构造函数中, PreparedStatementCache 的初始化。
有
(即在连接被创建的时候,即初始化 PreparedStatementCache),初始化代码如下:
if (psCacheSize > 0)
psCache = new PreparedStatementCache(psCacheSize);
而 PreparedStatementCache 这个类继承自 LRUCachePolicy 类,如下:
public class PreparedStatementCache extends LRUCachePolicy
public PreparedStatementCache(int max)
{
//max 值即为 JBOSS 连接池配置文件定义的 psCacheSize
super(2, max);
create();
}
其中 super 如下:
//根据指定的最小值和最大值,创建 LRU cache 管理方案。
public LRUCachePolicy(int min, int max)
{
if (min < 2 || min > max)
{throw new IllegalArgumentException("Illegal cache capacities");}
m_minCapacity = min;
m_maxCapacity = max;
}
public void create()
{
m_map = new HashMap();
m_list = createList();
m_list.m_maxCapacity = m_maxCapacity;
m_list.m_minCapacity = m_minCapacity;
41. m_list.m_capacity = m_maxCapacity;
}
PreparedStatementCache 对象其实是一个是一个 LRU List,即它使用了一个双向链表
来存储 PreparedStatement 的值,这个 LRU 链表的数据结构如下:
public class LRUList
{
/** The maximum capacity of the cache list */
public int m_maxCapacity;
/** The minimum capacity of the cache list */
public int m_minCapacity;
/** The current capacity of the cache list */
public int m_capacity;
/** The number of cached objects */
public int m_count;
/** The head of the double linked list */
public LRUCacheEntry m_head;
/** The tail of the double linked list */
public LRUCacheEntry m_tail;
/** The cache misses happened */
public int m_cacheMiss;
/**
* Creates a new double queued list.
*/
protected LRUList()
{
m_head = null;
m_tail = null;
m_count = 0;
}
在 BaseWrapperManagedConnection 被实例化的时候,即连接创建的时候,会初始化
一个最小值为 2,最大值为 max 的一个 LRU 双向链表(max 值在 jboss 连接池中通过<
prepared-statement-cache-size> 50< /prepared-statement-cache-size>参数来设置)。
BaseWrapperManagedConnection 方法的 prepareStatement 方法,源代码如下:
PreparedStatement prepareStatement(String sql, int resultSetType,
int resultSetConcurrency) throws SQLException
{
if (psCache != null)
{
//实例化 KEY 类。
PreparedStatementCache.Key key = new PreparedStatementCache.Key(sql,
52. 调优篇-inlist 查询优化
在前面一节 JBOSS 连接池调优 2-合理的设置 PreparedStatementCache 中我们已经学
会了使用 Memory Analyzer (MAT)工具来查看 JVM 的内存,并且看到了由于 inlist sql 的存
在,导致系统 PreparedStatementCache 中 SQL 的命中率仅为 40%左右。之前我们已经介
绍了 PreparedStatementCache 的作用,因此,命中率低可能引起的问题也显而易见(不清
楚的同学请参考前一章节的内容)。
部分同学可能不明白什么是 in list 的 SQL,这里先说明一下,所谓 in list 的 SQL 就是指
使用了 in 来进行查询,绑定变量个数不确认的 SQL,如:
select * from test where id in (:1,:2,:3)
对于这一类的查询, 由于 in 的查询条件中绑定变量个数的不同, 会导致 SQL 版本变多,
从而导致 PreparedStatementCache 的命中率下降。(因为 JBOSS/oracle 中都是以 SQL 文
本完全一致来匹配 PreparedStatementCache)。
因为 PreparedStatementCache 在 MYSQL 中没有作用(根本原因是 MYSQL 不支持绑定
变量) 所以 MYSQL 不需要考虑 in list 在 PSCACHE 中的优化,
, 下面给出 oracle 中两种 in list
sql 的解决方案:
方案 1:使用 oracle 中的 pipelined function(可以认为是一种性能较好的存储过程),实
现如下函数:
create or replace type t_array
as table of varchar2(4000)
/
--The PIPELINED keyword on line 4 allows this function
-- to work as if it were a table:
create or replace function
str2varlist(p_string in varchar2)
return t_array
PIPELINED
as
v_str VARCHAR2 (4000) DEFAULT p_string || ',';
v_n number(11);
begin
LOOP
v_n := INSTR (v_str, ',');
EXIT WHEN (NVL (v_n, 0) = 0);
pipe row(LTRIM (RTRIM (SUBSTR (v_str, 1, v_n - 1))));
v_str := SUBSTR (v_str, v_n + 1);
END LOOP;
return;
end;
53. /
这个函数的作用是将以逗号分隔的字符串转换成一个 table,如下:
zhoucang@zhoucang>select * from TABLE(str2varlist('123,456,789,012,345'));
COLUMN_VALUE
---------------------------------------------------------------------------
123
456
789
012
345
5 rows selected.
这样,每次我们的 SQL 执行时,只要传入一个固定长度的字符串即可,如:
select * from test where id in(select * from TABLE(str2varlist(:1)))
或者写成 join 的方式:
select /*+ use_nl(a b) ordered*/ b.* from
TABLE(str2varlist(:1)) a,test b
where a.column_value = b.id
这也不失为一种好方法,至少使用起来非常的方便。
方案 2:这是一种看起来很土的方法,同事建议的,真的很土,即固定 in 后面条件的个数,
每次传入不同的值,不足的可以使用一个不存在的值来进行补充。如:
select * from test where id in(:1,:2,:3,:4,:5,:6,:7,:8,:9,:10)
固定 10 个 ID 的查询,不足 10 个以-1 来补充(如果 ID 不为负数)。
因为 oracle 中的 in 限制最多为 999 个,所以,极端情况下,也只需要固定 999 个 ID
即可。
如何选择方案 1 还是方案 2?
专门测试了一下,测试过程主要逻辑如下:
方案 1,创建 pipelined function,执行 20000 次查询:
sql = " select /*+ use_nl(a b) ordered*/ b.* from
TABLE(str2varlist(:1)) a,test b
where a.column_value = b.id ";
54. for (int i = 0; i < 20000; i++) {
stmt1.setString(1,"1111,2222,3333");
}
方案 2,固定绑定变量个数,执行 20000 次查询:
sql = "select * from test where id in(?,?,?,?,?,?,?,?..........) ";
for (int i = 0; i < 20000; i++) {
stmt.setString(1, "1111");
stmt.setString(2, "2222");
stmt.setString(3, "3333");
stmt.setString(4, "-1");
stmt.setString(5, "-1");
stmt.setString(6, "-1");
……
}
测试主要以数据库的性能损耗为主要考虑指标(因为应用上带来的好处对我们来说诱惑
力实在太大了,基本上没什么坏处,性价比很高),测试结果如下(给出数据库中单个 SQL
主要的性能参考指标):
说明: 应用总 内 存 读 返 回 单 条 单条 SQL
执行时 (块) 记录 SQL 响应时间
间:秒 CPU 时
间(us)
3 个绑定变量,传 3 个 id 38 9 3 75.84365 75.84365
20 个绑定变量,传 3 个 id 39 10.06 3 94.48995 94.48995
100 个绑定变量, 传 3 个 id 43 10.05955 3 112.8007 112.8007
PPLINE,3 个 id 41 9.01435 3 149.5074 149.5074
100 个绑定变量, 10 个 id
传 53 31.05955 10 220.5576 220.55765
5
10 个绑定变量,传 10 个 id 43 30 10 174.7577 174.7577
PPLINE,10 个 id 51 30.03795 10 358.4637 358.46375
5
PPLINE 方案(方案 1):
1. CPU:会增加 CPU 的消耗 1 倍左右,原因是数据库需要对字符串进行解析。
2. IO:对 IO 基本无损耗。
3. 响应时间:数据库响应时间增加一倍。
固定绑定变量个数(方案 2) :
1. CPU:CPU 的消耗,增加幅度在 20%~60%,之间,具体因 in 的个数而定。
2. IO:增加了一个逻辑读, 这一个逻辑读是由于查询-1 时产生的, 主要是由于索引 branch
节点查询引起,由于 root 节点一定需要查询,索引一般为 3-4 层,可以认为,这个逻辑读
开销在 1-3 个左右,基本上可以认为在 1 个左右(这个可以结合 B 树索引结构得知)。并
55. 且这个 branch 节点非常热,IO 开销肯定是逻辑读。
3. 响应时间: 增加幅度在 20%~60%,之间,具体因 in 的个数而定。
显然,选择方案 2 会更加节省数据库的资源。
从测试结果可以看出, 当传入 3 个 ID 的时候,使用 100 个绑定变量和 20 个绑定变量,
还是存在一定的差异。因此,对于 in 的 SQL 我们可以再进行分级,根据执行频率,评估适
当的多给出几个版本,如:
Id=:1 :适用于 1 个 ID 的查询
Id in(20 个绑定变量) :适用于 ID 个数据在 2-20 个之前的查询。
Id in (999 绑定变量) :适用于 ID 个数在 20 个以上的查询。
56. 调优篇-合理设置连接数的 min 值和 max 值
前面两节 jboss 连接池的启动及 prefill 参数配置和 JBOSS 连接池的初始化及关闭中,
已经详细解释了 JBOSS 连接池的内部管理机制。本文就实际使用中的一些经验,分享一下
连接池 MIN 值和 MAX 值设置上的一些经验,最后再补充下关于 blocking-timeout-millis 设
置的一些建议。
连接池设置不合理可能导致的后果:
连接池的 MIN 数和 MAX 数,看似很简单的两个参数,其实对于应用和数据库的影响,
并不是那么的简单。我们先来看看连接数设置不合理可能产生的后果:
1. 连接池 MIN 设置过小,应用业务量突增,或者启动时可能产生连接风暴。
2. 连接池 MIN 值设置过大,会造成资源的浪费,主要包括数据库和应用内存,连接数
的浪费,同时连接池 MIN 值设置过大,也会导致连接被频繁的创建和销毁。这是由
连接池的工作机制决定的。
3. 连接池 MAX 值设置过大。在极端情况下,当应用发生异常时,会导致连接数被撑
到 MAX 值,有可能导致数据库连接数被耗尽,从而导致正常的业务受到影响。当
连接数被撑到 MAX 值,在获取连接超时的时候,应用的线程池也有可能受到影响,
这是一系列的连锁反应。
以上是连接池 MIN 值和 MAX 值设置最主要的 3 点影响。其中,影响最大的并且最难配置的
是第 2 点,即连接数的 MIN 值设置。
设置连接池的 min-pool-size:
一般来说,我们可以按照业务高峰时期的压力来估算连接数。比如,在高峰时期,每秒
有 5000 个并发请求,每个请求的处理时间为 2ms,则每秒总共需要 5000*2ms=10s 的连
接处理时间。因此,连接数的 MIN 值我们可以设置为 10 个(适当上浮 1-2 个),这样就能
基本上解决连接风暴的问题了。当然在业务低峰时期,10 个连接数的设置可能偏大了点,
所以这里对于连接数的 MIN 值设置也做了一个权衡,保守起见,我们需要以高峰时需要的
正常连接数来设置 MIN 值,以避免连接风暴的发生。因为这样做是最保险的,当然,这带
了一些额外的代价,会牺牲掉部分的数据库和应用的资源。
有时候,我们对于业务的估计可能并不准备,如业务刚上线,对于连接数不能很好的评估,
这个时候我们可以对连接池的 MIN 值进行调优,共有两种方式,如下:
通过 ORACLE 监听日志对 min-pool-size 调优:
对 于 ORACLE 数 据 库 , 我 们 可 以 使 用 数 据 库 的 监 听 日 志 来 进 行 调 优 , 即 通 过
listener.log(数据库监听日志)进行调优。收集以下几个信息:
1. 根据监听日志,我们可以得到某个应用集群 A 在 2 小时内创建的连接总数,假设统
计一共为 N 个。
2. 假设 A 集群在 2 小时内的连接数维持在一个值:M。 (后面会对这个 M 进行说明)
3. A 集群有 S 台应用服务器,假设应用服务器负载均衡。
我们知道, 连接池 IDLE 清理是 15 分钟执行一次的, 每次清理空闲时间超过 30 分钟的连接。
所以,2 小时内,每台应用平均创建的连接数为:N/4S 个。
假设:
57. 1. M=min 连接数,则我们的应用可以下调的 MIN 连接数为 N/4S 个,即设置为 M-N/4S。
(由连接池的原理可以知道,这些新创建的连接完全都是空闲的连接,只是由于配置了 MIN
值较大而产生的,实际上这些连接一直处于空闲状态)
2.假设 M 值>min 连接数,则需要将 min 值设置为 M。
3.假设 M<min,这不可能发生。
之前对于某核心系统做的一次调优, 上线后效果很不错,参考 一个生产库的 JBOSS 连接池调
整优化及分析
通过监控 JBOSS 连接池对 min-pool-size 的调优:
连接池默认参数及获取连接池中相关的统计信息一节中, 我们提到了连接池中有一些方
法可以获取连接数的信息,其中有三个方法:
//获取连接数,内部执行:return created - destroyed;即所有创建的连接数-所有销毁的连接数
public int getConnectionCount()
{
return connectionCounter.getCount();
}
//创建连接的总数
public int getConnectionCreatedCount()
{
return connectionCounter.getCreatedCount();
}
//连接销毁总数
public int getConnectionDestroyedCount()
{
return connectionCounter.getDestroyedCount();
}
通过监控这三个参数我们即可进行连接数的调优,具体的调优方式和方法 1 一致,只是
我们获取创建的连接数,通过监控连接池中的状态获取。如下图所示,我们监控了
10:00-12:00 数据库的连接数:
58. 通过上面的图可以知道,我们的连接数 MIN 值设置过大了(上图是对于单台应用的连接
池监控)。因为连接池始终保持在 MIN 值 10 个,说明连接数过剩,而我们的连接池不停的
创建,销毁。由于定时任务是 15 分钟启动 1 次的,所以我们在图上可以看到每隔 15 分钟
说有 1 个连接被销毁。在 30 分钟平均有 2 个连接被销毁。根据同样的 ORACLE 监听日志的
调优算法:
我们的连接数可以被设置为 M-N/4S,即 10-8/4=8 个。
这是二种连接池 MIN 调整的方案。其实原理是完全一样的,只是获取数据的两种方式
不同,对于有条件的应用,建议监控 JBOSS 的连接池进行调优。这样,连接池的一切状态,
都会尽在掌握之中,调优也会变的更加简单高效。
设置连接的 max-pool-size:
其实我们在调整连接池 MIN 值的时候,已经是按业务的高峰时期进行调整。所以 MAX
值调整的意义其实并不大,我们只要将 MAX 值设置在一个安全的阀值即可。保证不超过数
据库的连接数,同时又保证在业务偶尔分布不均匀时,应用也能够获取连接,防止连接池取
不到的情况。
我们经常会看到应用程序报错:“No ManagedConnections available within
configured blocking timeout xx [ms]”,这其实是由于连接池达到最大值了,没有可用