Java-sqlite-jdbc - 知识库

Java-sqlite-jdbc 编辑

sqlite-jdbc问题反馈

2014.8.12

问题背景

在X86机上、或者龙芯上,长时间运行sqlite-jdbc,会出偶然的崩溃问题。

在不同的机器上崩溃频率不一致。龙芯做过的测试如下:

MIPS X86
出错
  • 客户: 32bit Kylin + JDK6 + sqlite3.7.15 + ClogProxy (15小时出错)
  • 龙芯内部: 32bit Fedora13 + JDK6 + sqlite3.7.15 + ClogProxy (出错时间不确定 1~20小时不等)
32bit Fedora19 + JDK6 + sqlite3.7.15 + ClogProxy (基本稳定在两小时内出错)
  • 客户:32bit Kylin + JDK6u45 + sqlite3.7.2 + MonitorAgent (基本稳定在两小时内出错)
  • 龙芯内部:32bit Ubuntu10.04 + JDK6u45 + sqlite3.7.15 + MonitorAgent (19小时出错)
不出错
  • 客户:64bitJDK 两天多不出错
  • 龙芯内部: CentOS6.4 + 64bitJDK6 + sqlite3.7.15 + ClogProxy (2台*30小时不出错)
  • 客户:
  • 龙芯内部:32bit Ubuntu12.04 + JDK6u45 + sqlite3.7.15 + ClogProxy 2台*30小时不出错
32bit CentOS6.4 + JDK6u45 + sqlite3.7.15 + ClogProxy 2台*15小时不出错
64bit Fedora20 + JDK6 + sqlite3.7.15 + ClogProxy 15小时不出错

初步分析

  • 由于此问题在X86机器上也存在,因此初步判定与龙芯CPU无关
  • 而且使用最新版本X86的jdk 6也出错误,因此初步判定与龙芯Jdk无关
  • 因此,问题一定出在sqlite-jdbc这个软件本身

进一步排查

  • 总结两周以来全部测试崩溃日志,出错位置都是在JsonSqliteOperation.class中
  • 对JsonSqliteOperation.class进行了反编译
  • 仿照JsonSqliteOperation.class,专门编写了小程序,试图复现问题
[UseTest.java]

import java.sql.*;

public class UseTest extends Thread {
    public static Connection conn;
    public int id;
    public int s = 0;;

    public UseTest(int n) {
        id = n;
    }

    public boolean hasTable(String tableName, Connection connect)
        throws SQLException
    {
        boolean isExistTable;
        DatabaseMetaData meta;
        ResultSet rs;
        isExistTable = false;
        meta = null;
        rs = null;
        try
        {
            meta = connect.getMetaData();
            if (meta != null)
            {
                rs = meta.getTables(null, null, tableName, null);
                if (rs.next())
                    isExistTable = true;
                if (rs != null) {
                    rs.close();
                    rs = null;
                }
                if (meta != null) {
                    meta = null;
                }
            }
        } catch(Exception e ) {
            e.printStackTrace();
        }

        return isExistTable;
    }

    public void run() {
       try {
          while(true) {
             Statement stat = conn.createStatement();

             if (hasTable("people", conn)) {
                 ResultSet rs = stat.executeQuery("select * from people;");
                 rs.close();
                 stat.close();
             }
             s++;
             if (id == 0 && (s % 10000) == 0)
                 System.out.println(id + ":" + (s++));
          }
       } catch (Exception e) {
             e.printStackTrace();
       }
    }

    public static int N = 8;
    public static void main(String[] args) throws Exception {
        Class.forName("org.sqlite.JDBC");
        conn = DriverManager.getConnection("jdbc:sqlite:test2.db");
        conn.setAutoCommit(true);

        UseTest[] t = new UseTest[N];
        for (int i = 0; i < N; i++)
        {
            t[i] = new UseTest(i);
            t[i].start();
        }
        for (int i = 0; i < N; i ++)
        {
            t[i].join();
        }
    }
}

编译和运行

$ javac -cp ../sqlite-jdbc-3.7.15.jar UseTest.java

$ java -cp ../sqlite-jdbc-3.7.15.jar:. UseTest

重要进展:发现sqlite-jdbc的错误

  • 运行上面的例子 UseTest,在很短时间内,发生内存溢出
  • 经过反复排查程序,确认UseTest.java没有问题
  • 检查溢出的位置,发现sqlite-jdbc-3.7.15.jar有严重的错误:MetaData.java存在内存溢出漏洞
[MetaData.java]

public synchronized ResultSet getTables() {
  ...
  return conn.createStatement().executeQuery(sql);
}
  • 在上面最后一句中,conn.createStatement()会创建一个隐含的 Stmt 对象
  • 而在getTables()返回以后,再也没有机会执行 Stmt.close(),从而导致内存永远得不到回收
  • 因此,无论这个内存溢出问题是否是导致崩溃问题的直接原因,这个内存溢出问题也足以导致程序无法正常运行

观察sqlite-jdbc-3.7.15.jar代码

  • 查看MetaData.java头部注释信息,发现这份代码来源于一个社区项目,主要作者是David Crawshaw,最早是在zentus.com公司期间开发
  • 这个代码中,大量充斥着不良设计风格,包括定义后未使用的变量getTables、不直观的sql拼接语句,等等。
  • 总体感觉,这份代码质量很差,在开源项目中属于品质低下的项目,使用这份代码做自己的项目会存在较大的风险

查找sqlite-jdbc的最新代码

  • 在主流代码拖管网站bitbucket.org上找到了sqlite-jdbc的活跃分支
https://bitbucket.org/xerial/sqlite-jdbc
  • 目前仍然在有作者维护

最新代码已经解决内存溢出问题!

* Grace Batumbya committed a32c004 2013-05-12

Fixes issue #73 DatabaseMetaData.getTables() Memory Leak
  • 在这个版本中,明确的把 getTables() 加上了 Stmt.close()的处理,消除了内存溢出问题
  • 而客户现在使用的sqlite-jdbc版本极为古老(对应的具体时间未知),还未进行这些错误修正,因此存在内存溢出问题,甚至还可能包含更多深层次问题

下一步工作

  • 到目前为止,虽然崩溃的原因尚未清楚,但是有力的找到了低版本中内存溢出的证据,能够证明版本升级是一个必要措施
  • 请客户首先升级到官方sqlite-jdbc的最新版本,再次测试崩溃问题是否存在
  • 龙芯也会同步做这个测试,如果官方sqlite-jdbc的最新版本仍然存在崩溃问题,龙芯会投入力量修正官方未解决的问题,直到协助客户把系统在龙芯机器上稳定运行