打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
监控业务系统数据库连接

2006 年 10 月 10 日

J2EE 服务器一般提供了数据库连接池活跃连接个数的信息,但难以提供连接的细节、执行的 SQL 语句以及是否产生锁等信息。一旦发生不稳定现象,开发人员容易归咎于应用服务器。本文分析了业务模块如何导致系统级别的问题,并提出了实时监控数据库连接细节并准确定位异常所在模块的方法,以方便排除业务模块问题。 读者可以增强对于系统问题分析和解决的能力,并能够明确如何分析和解决业务系统问题,而不是简单认为时应用服务器平台不够稳定造成的。同时,对 Java 核心 API 及 Proxy 技术的理解也将进一步增强。 目标读者:具有 J2EE 开发经验的技术人员,解决过或尝试解决过系统级别的问题。

影响业务系统运行稳定性的主要因素

电信行业的计算机应用比大多数行业复杂一些,对系统性能指标、安全性、稳定性等都有特殊要求。一旦由于发生软硬件故障,将造成广大电信用户的业务使用受到影响。常见故障类型有以下几种:

  • 硬件意外损坏,如硬盘故障、板卡损坏等。
  • 应用服务器软件性能问题,由于设计或配置上的问题,导致无法达到预期的性能指标,在系统压力过大时应用服务器崩溃。
  • 应用服务器与其它公司软件产品或第三方软件集成上存在问题,系统偶发故障,但难以定位。
  • 业务模块程序编写错误,可能导致系统局部功能异常,严重的情况下可能导致整个系统不可用。

一般地,电信业务系统都尽可能避免"单点故障",即通过在每一个可能引起故障的节点上,都进行冗余处理。例如,对于 J2EE 应用服务器一般都使用集群,如果群集中一台服务器发生软硬件而导致宕机时,其它服务器仍可完成所有请求的处理。因此,大多数硬件故障和应用服务器故障都不会立即引起业务系统服务的中断,而只会导致响应速度变慢。





回页首


业务模块对系统性能影响的原因分析

多数程序员认为,由程序员编写的业务模块如果存在问题,只会对系统造成局部的、功能性的影响,即某个新开发的模块不正常,而其它功能模块则一般能正常工作。至于系统级别的问题,例如无法获取数据库连接、浏览器请求响应速度慢等等,一般认为是由于应用服务器的安装或配置问题引起的。

然而,这种观点是片面的,甚至是错误的。主流的 J2EE 服务器,特别是 Websphere 5.1 经过许多复杂的商业应用的考验,已经非常稳定可靠。而这些系统级别的问题,恰恰有可能是一个粗心的程序员的一个小小疏忽造成的。

请看下面的例子,从数据库连接池中获取一个数据库连接,执行一组业务操作,然后释放连接:


清单1:一个基本正确的业务模块代码
                DataSource ds=null;                        try {                        Context context=new InitialContext();                        ds=(DataSource) context.lookup("MyDataSource");                        } catch (Exception ex) {                        ex.printStackTrace();                        }                        Connection conn=null;                        Statement st=null;                        String sValue=null;                        try {                        conn=ds.getConnection();                        st=conn.createStatement();                        ResultSet rs=st.executeQuery("select field1 from                        thetable");//-------line 13                        rs.next();                        sValue=rs.getString(1);                        try {st.close();} catch(Exception ex) {}    //------	line 16                        try {conn.close();} catch(Exception ex) {}                        } catch (SQLException ex) {                        try {st.close();} catch(Exception ex) {}	//-------line 19                        ry {conn.close();} catch(Exception ex) {}                        } finally {                        //----------line 22                        }                        

我们知道,释放数据库连接最好能在 finally 中进行,即 line 22 的位置。这样就可以确保在任何情况下这段代码被执行。但是,以上代码也同样能够保证在正常和异常情况下都能正确释放连接,请看 line 16- line 19 的代码。如果你在代码审查时发现了这段代码,可能要求更改。然而,随着业务的发展,业务逻辑逐渐复杂,可能需要将 line 13-line 15 的代码整合到一个新开发的类中,而将 line 13-line 15 更改为:


清单2:业务模块代码的重构
sValue=foo.getValue();                        

我们可能不会注意到已经发生了改变,foo.getValue() 方法理应返回一个合适的值,但也有可能抛出 RuntimeException。注意:不是 SQLException! 这个 RuntimeException 可能不是 Foo 类的程序员有意抛出的,它可能来自于一个无意的空指针错误,或者是数组越界。当这个错误被抛到外层的调用者的时候,我们应当意识到,line 13 后面的代码没有被执行,Catch(SQLException sex) 中的代码也没有被执行,注意,Connection 对象没有被合法的关闭!

由于业务系统的数据库连接都取自 J2EE 服务器连接池,当某个业务模块没有释放数据库连接,将导致数据库连接长期不使用的超时,J2EE 服务器能够在超时时长结束后,释放数据库连接,供其它业务请求使用。那么,在 J2EE 服务器提供给业务模块的数据库连接资源中,总有一部分被闲置浪费了,等待超时回收,而有一部分则被正常使用。如果浪费的比率足够大,业务模块对数据库连接的正常请求将不能得到响应,即出现已经分配了很多数据库连接,但仍不能满足业务模块需求的情况。

假设以上错误发生在话费查询模块,在通常情况下,话费查询模块调用不是很频繁,浪费的数据库连接数比率较低,系统整体仍能正常使用。而当出帐后的几天内,大量客户查询话费,错误频繁发生导致数据库连接大量被浪费,无法满足业务模块的正常请求。此时,群集中的所有应用服务器将此起彼伏发生异常,严重时可能同时发现异常,无法使用。客户浏览器则可能出现白屏或无法响应等错误。到夜间请求量少的时候,系统又恢复正常,因此很难准确定位。这样的错误,无论在群集中配置多少台应用服务器都不可能根本解决。

通过上面的例子,我们清晰的观察到,一个普通的业务模块,而不是应用服务器及业务核心代码,可能导致非常严重的 A 级故障。除了通过改进开发人员的编程习惯,并进行严格的代码审查外,我们还应当谋求对业务系统的数据库连接状况进行实时监控,了解数据库访问的细节,从而在故障的潜伏期发现解决。

我们需要了解的信息有:

1. 系统中多少个线程在进行与数据库有关的工作?其中,而多少个线程正在执行 SQL 语句?这可以让我们评估数据库是不是系统瓶颈。

2. 多少个线程在等待获取数据库连接?获取数据库连接需要的平均时长是多少?数据库连接池是否已经不能满足业务模块需求?如果存在获取数据库连接较慢,如大于 100ms,则可能说明配置的数据库连接数不足,或存在连接泄漏问题。

3. 哪些线程正在执行 SQL 语句?执行了的 SQL 语句是什么?数据库中是否存在系统瓶颈或已经产生锁?如果个别 SQL 语句执行速度明显比其它语句慢,则可能是数据库查询逻辑问题,或者已经存在了锁表的情况,这些都应当在系统优化时解决。

4. 最经常被执行的 SQL 语句是在哪段源代码中被调用的?最耗时的 SQL 语句是在哪段源代码中被调用的?在浩如烟海的源代码中找到某条 SQL 并不是一件很容易的事。而当存在问题的 SQL 是在底层代码中,我们就很难知道是哪段代码调用了这个 SQL,并产生了这些系统问题。

要了解这些信息,就必须跟踪业务代码对数据库的主要操作,包括 Connection 的获取,Statement 的创建,SQL 语句执行等主要操作的执行细节或执行时间。我们通过以下几个步骤进行:

1. 将 J2EE 服务器上业务系统使用的数据源名(如 CustomDataSource),更改为一个新名字(如 RealDataSource)。

2. 编写 DataSource 类,并通过 JNDI 注册到 J2EE 环境中,使用原配置的数据源名(CustomDataSource)。该 DataSource 类的 getConnection 访问将返回指定的代理连接对象,该对象实现 Connection 接口。

3. 编写 ProxyConnectionHandle 类,用于动态创建 ProxyConnection 实例,并拦截 Connection 接口的访问。该 Connection 实例的 createStatement/prepareStatement/prepareCall 方法都将返回代理 Statement 对象,分别实现 Statement/PreparedStatement/CallabledStatement 接口。用同样的方式编写 ProxyStatementHandle 类,拦截对 Statement 接口的调用。

4. 编写 ReportHandle 接口,当 ProxyConnectionHandle、ProxyStatementHandle 拦截到需要记录的事件时,都通过该接口注册。

5. 编写 BaseReport 类,实现 ReportHandle 接口,记录并管理数据库的实时状态信息。实现一个 JSP,访问 BaseReport 类的实例,获取数据库连接的实时状态并展现出来。

6. 通过当前数据库在用的连接当前状态,Connection 创建时间、Connection 创建栈、Statement 创建时间,最后执行的 SQL 语句,SQL 语句执行时间,SQL 语句执行栈等信息,将能够提供我们所需要的必要信息,并为系统优化提供必要帮助。





回页首


实现数据源代理

DataSource 是 J2EE 应用服务器中数据源类的公共接口,只要实现了 DataSource 接口,就可以在通过 JNDI 在 J2EE 服务器中注册一个数据源,并能够被业务模块以标准方式从 JNDI 中获取并访问。通过编写一个 DataSource 接口的实现类 ProxyDataSource,拦截 getConnection() 方法,将能够将我们自定义的 Connection 接口实现类的实例返回给业务模块,从而提供 Connection 层次上的拦截能力。


清单3:ProxyDataSource 对象的 getConnection 方法
        public Connection getConnection(String sUserName,String sPassword)                        throws SQLException{                        //先从真实的连接池获取一个标准的 oracle 连接对象,                        该连接不会被直接传递到客户                        Connection realConnection=null;                        ProxyConnectionHandle proxyConnectionHandle=null;                        try {                        if (sUserName!=null && sPassword!=null) {                        realConnection=this.realDataSource.getConnection                        (sUserName,sPassword);                        } else {                        realConnection=this.realDataSource.getConnection();                        }                        if (proxyDisabled) {        //直接返回真实的连接                        return realConnection;                        } else {                    //返回代理连接,并通知 reportHandle                        proxyConnectionHandle=new                        ProxyConnectionHandle(realConnection,this);                        //-----------line 14                        //获取一个过滤过的连接                        Connection proxyConnection=proxyConnectionHandle.                        newProxyConnection();                        //添加到proxyConnetionSet                        this.proxyConnectionHandleSet.add(proxyConnectionHandle);                        this.reportHandle.connectionOpen                        (proxyConnectionHandle.hashCode(),null);                        return proxyConnection;   //----------line 20                        }                        } catch (SQLException sex) {                        this.reportHandle.connectionOpen                        (proxyConnectionHandle.hashCode(),sex);                        throw sex;                        }                        }                        public Connection getConnection() throws SQLException {                        return getConnection(null,null);                        }                        

当 proxyDisabled 属性为 true 时,禁用代理机制,即不监控。此时直接返回 realConnection 对象,即从 J2EE 应用服务器的数据源中获取的 Connection 对象。而当 proxyDisabled 为 false 时,启用代理机制,返回的 Connection 接口对象为动态创建的 proxyConnection 对象,进一步创建的 Statement 对象也是动态创建的 proxyStatement 对象。通过以下几个步骤实现。请参考清单 3 中 line14-line20 的代码。

1. 使用 realDataSource 和 this 作为参数,构造 ProxyConnectionHandle 对象。ProxyConnectionHandle 对象是一个 InvokedHandle 对象,能够拦截动态创建的 ProxyConnection 对象的所有调用。请参阅 JDK 的 Proxy 类及 InvokedHandle 类的相关文档。

2. 调用 ProxyConnectionHandle 实例的 newProxyConnection() 方法,动态创建我们所需要的 ProxyConnection 对象。在创建的过程中,我们指定了必须实现 Connection 接口,并将其赋值给 Connection 接口的实例 proxyConnection。proxyConnection 对象的所属类是动态创建的,其类名不确定。

3. 将 proxyConnectionHandle 添加到 proxyConnectionHandleSet 集。当连接释放时,将把 proxyConnectionHandle 从该集删除。

4. 将获取 Connection 的事件向 reportHandle 注册。这样,通过 reportHandle 就能够获取获取数据库连接的情况。同样,当连接关闭时,也应向 reportHandle 注册。

为了在 WebSphere5.1 ND 群集中稳定运行,通过 JNDI 注册的对象应当足够简单,且实现序列化接口,以便在群集的各个节点之间传递。因此,在 ProxyDataSource 外围包装了一层 SerializableDataSource,实现序列化接口。详细代码请参阅源代码清单。

另外,我们需要将 SerializableDataSource 对象通过 JNDI 注册到 J2EE 服务器中。以下 DataSourceInit 类应确保在Web应用启动时被加载,这可以通过设置启动类实现。


清单4:注册 DataSource
public class DataSourceInit {                        static {                        try {                        System.out.println("--begin--");                        FileLogger.initial();                        InputStream is=Thread.currentThread().getContextClassLoader().                        getResourceAsStream("proxydatasource.properties");                        Properties prop=new Properties();                        prop.load(is);                        Enumeration enum=prop.propertyNames();                        while (enum.hasMoreElements()) {                        String sProxyDataSourceName=(String) enum.nextElement();                        String sRealDataSourceName=prop.getProperty                        (sProxyDataSourceName);                        //绑定                        Context context=new InitialContext();                        SerializableDataSource ds=new SerializableDataSource                        (sProxyDataSourceName,sRealDataSourceName);                        context.rebind(sProxyDataSourceName,ds);                        System.out.println("从配置文件绑定代理数据源:"+                        sProxyDataSourceName+",对应真实数据源为:"+sRealDataSourceName);                        }                        System.out.println("--end--");                        } catch (Exception ex) {                        System.out.println("--exception--");                        ex.printStackTrace();                        }                        }                        }                        

通过 context.rebind() 方法,我们将创建的 SerializableDataSource 对象注册到 J2EE 服务器中,且使用的是 ProxyDataSourceName。业务模块只知道通过 ProxyDataSourceName 获取数据源,而不会感知其与 J2EE 服务器提供的 RealDataSource 之间的区别和联系。接下来我们将重点关注如何提供实现标准 Connection 接口及 Statement 接口的对象给业务模块。





回页首


实现 Connection 代理及 Statement 代理

在 ProxyDataSource.getConnection() 方法中,通过调用 proxyConnectionHandle.newProxyConnection() 方法获得一个标准 Connection 接口对象。


清单5:通过 Proxy 技术创建 Connection 对象
        protected Connection newProxyConnection() {                        Class[] interfaces=new Class[1];                        interfaces[0]=java.sql.Connection.class;                        Object o=Proxy.newProxyInstance(realConnection.getClass().                        getClassLoader(),interfaces,this);                        if (! (o instanceof Connection)) {  //对 Tomcat 的特殊处理                        o=Proxy.newProxyInstance(realConnection.getClass().                        getSuperclass().getClassLoader(),realConnection.getClass().                        getSuperclass().getInterfaces(),this);                        }                        Connection proxyConnection=(Connection)o;                        // Proxy.newProxyInstance(realConnection.getClass().                        getClassLoader(),realConnection.getClass().getInterfaces(),this);                        return proxyConnection;                        }                        

运用 Java 语言提供的 Proxy 技术,我们动态构造出一个代理对象,能够拦截源对象(J2EE 平台提供的 Connection)的方法调用,调用到实现 InvocationHandle 的 ProxyConnectionHandle 类中。这样,我们就可以对各种方法调用分别记录处理。请看 ProxyConnectionHandle 中拦截方法 invoke 的实现:


清单6:拦截方法及其处理
        public Object invoke(Object proxy, Method m, Object[] args)                        throws Exception{                        try {                        Object oReturn=null;                        if (m.getName().equals("close")) {                        this.proxyDataSource.closeConnection(this);                        } else if (m.getName().equals("createStatement")                        ||m.getName().equals("prepareStatement")                        ||m.getName().equals("prepareCall")) {                        //构造方法参数                        Method proxyMethod=null;                        Class[] parameterTypes=null;                        if (args!=null) {                        parameterTypes=new Class[args.length];                        for (int i=0;i<args.length;i++) {                        parameterTypes[i]=args[i].getClass();                        }                        }                        proxyMethod=this.getClass().getDeclaredMethod                        (m.getName(),parameterTypes);                        oReturn=proxyMethod.invoke(this,args);                        //部分代码省略,请参考代码清单                        //…..                        }                        return oReturn;                        } catch (java.lang.reflect.InvocationTargetException ite) {                        Throwable th=ite.getTargetException();                        if (th instanceof RuntimeException) {                        throw (RuntimeException) th;                        } else {                        throw new RuntimeException(th);                        }                        } catch (Exception ex) {                        if (ex instanceof RuntimeException) {                        throw (RuntimeException) ex;                        } else {                        throw new RuntimeException(ex);                        }                        }                        }                        

当 Connection 对象的 close 方法被调用时,将调用该实例所属 ProxyDataSource 的 closeConnection 方法,进行连接关闭及关闭事件记录。处理步骤可参照 ProxyDataSource 的 getConnection() 方法。而当 Connection 对象的 createStatement 方法、prepareStatement 方法或 prepareCall 方法被调用时,应当记录 Statement 对象被创建事件,并委托给 J2EE 服务器提供的 Connection 对象处理。由于以上三个方法有众多重载类型,因此,在这里使用了方法动态调用技术,根据具体的参数调用到 ProxyConnectionHandle 的具体方法中实现,如 createStatement() 方法。见清单7。

invoke 方法还分别对 InvocationTargetException 及 Exception 进行处理,包装并返回 RuntimeException,以避免对正常业务流程的可能影响。


清单7:创建 Statement 接口的代理对象
        public Statement createStatement() throws SQLException{                        Statement realStatement=null;                        ProxyStatementHandle proxyStatementHandle=null;                        Statement proxyStatement=null;                        try {                        realStatement=this.realConnection.createStatement();                        proxyStatementHandle=new ProxyStatementHandle(realStatement,this);                        proxyStatement=proxyStatementHandle.newProxyStatement();                        this.proxyStatementHandleSet.add(proxyStatementHandle);                        this.proxyDataSource.reportHandle.statementOpen                        (proxyStatementHandle.hashCode(),this.hashCode(),"",null);                        return proxyStatement;                        } catch (SQLException sex) {                        this.proxyDataSource.reportHandle.statementOpen                        (proxyStatementHandle.hashCode(),this.hashCode(),"",sex);                        throw sex;                        }                        }                        

从清单 7 中我们可以看到,具有 Statement 接口的代理对象的创建与 Connection 代理对象的创建非常相似。首先,执行 realConnection 对象的 createStatement() 方法,获取 realStatement 对象,再构造出其代理对象 proxyStatement,并将其返回给业务模块。同样,需要向 ReportHandle 接口对象汇报 StatementOpen 事件。





回页首


监控信息的记录与展现

ReportHandle 是一个 Interface,用于将 Connection、Statement 中发生的事件通知给指定的 Reprot 类。ReportHandle 的主要方法有:connectionOpen、connectionClose、connectionCommit、connectionRollback、statementOpen、statementClose、statementQuery、statementUpdate 等。在目前的实现中,我们使用较为简单的 BaseReport 类实现 ReportHandle 接口。当然也还可以实现其它更为复杂的实现类,以记录更为详细的信息。


清单8:查询 SQL 语句执行时记录的内容
        /**                        * 当 Statement(包括 PrepareStatement/CallableStatement)                        执行 executeQuery 一种方式执行时,回调此方法                        */                        public void statementQuery(int iStatementHashCode,int                        iConnectionHashCode,String sSql,String sTag,SQLException sex) {                        ConnectionData connectionData=(ConnectionData)                        this.connectionDataMap.get(new Integer(iConnectionHashCode));                        StatementData statementData=(StatementData)connectionData.                        statementDataMap.get(new Integer(iStatementHashCode));                        //设置相关属性                        statementData.lLastExecuteTime=(new Date()).getTime();                        statementData.lastExecuteThrowable=new Throwable();                        //仅在必要时设置,否则不应覆盖 PreparedStatement                        if (sSql!=null && sSql.trim().length()>1) {                        statementData.sLastSql=sSql;                        }                        }                        

清单 8 示例了执行 SQL 查询语句时,被 ProxyStatementHandle 拦截并调用 reportHandle.statementQuery 方法的处理。在该方法中,记录了该查询语句的执行时间、执行方法堆栈、SQL 语句等信息。这些信息保存在 StatementData 对象和相关联的 ConnectionData 对象中。最后,我们仅需要使用一个简单的 view.jsp 获取 BaseReport 对象及其 connectionDataMap 属性的内容,请参阅代码清单。View.jsp 的界面效果如下图。


图1:一个简单的连接监控页面

图1中,第一行只有 Connection 对象的打开时间,而 Statement 对象的打开时间为 null。这种情况在业务系统中只应该存在瞬间。而如果打开时间与系统当前时间有较大差距,或者重复出现大量类似记录,则显然是因为获取了数据库连接而从未使用过。这样,数据库连接资源就被白白浪费了。

第 2、3 行的情况,则可能是正常情况,也可能是异常情况。一般的,Connection 的打开时间与 Statement 的打开时间非常接近,且和系统当前时间非常接近。如果发现 Statement 是几分钟以前或更早打开的,则说明发生了异常的连接泄露。

对于各种正常和异常情况,可以分别点击 Connection 与 Statement 的各个"显示"按钮,显示方法的程序执行栈。


图2:显示指定 Statement 最后被执行数据库查询的细节

当发现了某个 Statement 可能存在异常时,图2示例了点击"最后执行栈"的"显示"按钮的结果。我们清楚的观察到在 index_jsp.java 第 83 行执行了最后一次 SQL 操作,执行的语句是"select sysdate from dual"。这样,我们就能够进行必要的优化处理。





回页首


结语

本文附件中的代码已经在实际系统中稳定运行 8 个多月。经过跟踪观察,对系统的额外性能开销小,且能够达到对在线系统J2EE数据库连接池状态动态跟踪的目标。所提供的跟踪性能比 J2EE 的连接池监控更强,适合在高可靠性、高稳定性、高性能的电信系统环境中使用。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
JDBC技术和数据库连接池专题
Java框架数据库连接池比较(c3p0,dbcp和proxool)
JDBC详解学习文档
中国java开发网 - 提示和技巧:jdbc 提示
java中通用的数据库连接与关闭方法类的简单写法
JDBC
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服