xiaobaoqiu Blog

Think More, Code Less

Crate Jdbc查询死循环bug

历经一个月的项目,使用Crate提升搜索速度终于要上线.先交代一下背景,我们使用的crate相关的依赖的版本:

1.crate-client  0.47.8
2.crate-jdbc    1.5.1

发布之后,验证阶段一切正常,等把流量入口打开,一段时间之后,猛然发现crate的一个查询服务所在的机器load飙到20往上,机器是4核CPU,4G内存的虚拟机.

top -H发现有大量的tomcat线程(10+),每个线程占用的CPU都达到20%,并且这种线程有增多的趋势.

vmstat命令发现r数字特别高,即正在等待CPU的线程非常多.

jstack的结果发现runnable的线程多达300多.

看现象,感觉问题是线程池分配没有回收,但是定位不到问题在哪.

最终定位的问题是在特定场景下存在死循环,并且直接才crate服务器执行相同的查询逻辑正常返回,因此基本定位在crate-jdbc中.

1.死循环场景

1.crate中数据为空,这是很从crate中查询数据;
2.crate中存在满足查询条件数据,我们分页查询,假设总数据量为10页,我们因为代码问题,直接查询第11页的内容(实际上肯定不存在);

2.原因

原因是这个版本crate-jdbc和mybatis一起存在死循环,如下,我们对crate的查询首先经过mybatis,从

1.MapperProxy.invoke
2.MapperMethod.execute
3.SqlSessionTemplate.selectList, SqlSessionTemplate.invoke
4.DefaultSqlSession.selectList
5.BaseExecutor.query, BaseExecutor.queryFromDatabase
6.ReuseExecutor.doQuery
7.RoutingStatementHandler.query
8.PreparedStatementHandler.query
9.CratePreparedStatement.execute()
10.DefaultResultSetHandler.handleResultSets, DefaultResultSetHandler.getFirstResultSet

问题的代码就在DefaultResultSetHandler.getFirstResultSet中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
    ResultSet rs = stmt.getResultSet();
    while (rs == null) {
        // move forward to get the first resultset in case the driver
        // doesn't return the resultset as the first result (HSQLDB 2.1)
        if (stmt.getMoreResults()) {
            rs = stmt.getResultSet();
        } else {
            if (stmt.getUpdateCount() == -1) {
                // no more results. Must be no resultset
                break;
            }
        }
    }
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
}

其中stmt为CratePreparedStatement,其getMoreResults()实现:

1
2
3
4
@Override
public boolean getMoreResults() throws SQLException {
    return false;
}

因此逻辑进入到else中,stmt.getUpdateCount()的实现:

1
2
3
4
5
6
7
8
@Override
public int getUpdateCount() throws SQLException {
    checkClosed();  //check connection是否被关闭
    if (resultSet == null && sqlResponse != null) {
        return (int) sqlResponse.rowCount();
    }
    return -1;
}

其中CratePreparedStatement的resultSet为null,并且sqlResponse不为空,sqlResponse的rowCount为0,因此getUpdateCount()返回值为0.因此getFirstResultSet中继续在while循环中,无法跳出.

3.绕过

归结起来产生问题的很简单,就是查询的结果集为空的时候,就会产生死循环,针对两种死循环场景,我们都可以绕过:

1.查询之前,先执行count查询,当count为0,即crate中没有满足我们查询条件的数据,则直接返回空数据集;
2.分页查询的时候,先执行count查询,当分页的offset大于等于count,则值额节返回空数据集;