分布式数据库内存分配:分布式DBLE
分布式数据库内存分配:分布式DBLEdble version:Dble has an error message 'unknown pStmtId when executing' when the client set useServerPrepStmts=true #1122事发当天 2019年4月12日下午,GitHub得到举报,两名 DBLE 用户各发现了一个极为凶残的Bug。DBLE 社区片儿警马上赶到案发现场进行取证并对Bug们开始展开调查。举报信息如下:error message:
原创: 鲍凤奇
本文摘要:
DBLE 是一款企业级的开源分布式中间件,江湖人送外号 “MyCat Plus”。Prepared Statement 协议是 MySQL 5.1 版本新加入的功能。MyCat 从1.6版本实现了 Prepared Statement 协议,但 MyCat 存在一些至今仍未修复的Bug。
本文将从两名 DBLE 用户提交的Bug开始说起,详细解读 DBLE 是如何实现 Prepared Statement 协议的。
事发当天
2019年4月12日下午,GitHub得到举报,两名 DBLE 用户各发现了一个极为凶残的Bug。DBLE 社区片儿警马上赶到案发现场进行取证并对Bug们开始展开调查。举报信息如下:
BugOne(https://github.com/actiontech/dble/issues/1122)error message:
Dble has an error message 'unknown pStmtId when executing' when the client set useServerPrepStmts=true #1122
dble version:
dble-9.9.9.9-884fc6b612d64cc22101226536f8fd1d24580857-20190221182143
BugTwo(https://github.com/actiontech/dble/issues/1124)error message:
use PreparedStatement with JDBC and MySQL J Connector will get wrong result when useCursorFetch=true #1124
dble version:
dble 2.18.10.5 and before (not yet test on later version)
信息分析
每次执行时解析语句的开销更少。通常,数据库应用程序处理大量几乎相同的语句,只更改子句中的文字或变量值,例如 WHERE 查询和删除,SET 更新和 VALUES 插入。
MySQL Prepared Statement 的二进制协议交互过程如图:
注意:
1. COMSTMTSENDLONGDATA 必须在 COMSTMTEXECUTE 前发送。
2. COMSTMTFETCH 必须在 COMSTMTEXECUTE 后发送。
3. COMSTMTRESET 是专为重置 COMSTMTSENDLONGDATA ,不能单独使用。
通过一图一表,我们对 MySQL Prepared Statement 协议交互中的各种行为做了一个回顾。下面让我们看看 DBLE 是如何实现的。
DBLE 对 prepare statement 协议的实现
对于客户端的预编译 请求,DBLE缓存了 SQL 并模拟返回报文,对于服务端改用 COM_QUERY 命令执行,并将各节点返回数据整合转换格式返回客户端。
说明:
1. 在 COMSTMTPREPARE 阶段,DBLE 此时接收的 SQL 不完整,不能确定下发节点,但MySQL Prepared Statement 协议要求此处返回一个 response 报文,因此 DBLE 会伪装COMSTMTPREPAREOK 报文返回。若有些 MySQL 驱动(如 JDBC )想从 response 报文中获取信息,这些信息会不准确。
2. 在 COMSTMTEXECUTE 阶段,DBLE 会根据客户端传输的参数将预编译SQL替换为具体的SQL,并使用 COMQUERY 命令下发至后端节点,因为 COMQUERY 不再使用二进制协议传输,因此DBLE需要对后端返回的数据进行转换后再返回客户端。
3. DBLE 不支持 COMSTMT_FETCH 命令。
好了,当我们了解完 MySQL 和 DBLE 对 Prepared Statement 协议实现过程后,我们再回过头来看那个两个Bug到底是怎么来的。
#1122 Bug分析
经过分析,在同一连接中,当发起 COMSTMTCLOSE 销毁当前 prepare statement 后,紧接着又创建一个 prepare statement。这两个操作是在 DBLE 中是异步进行,存在线程安全的问题。知道问题的根源之后,解决方案是 DBLE 将两个操作变成同步操作,避免线程安全的问题。
#1124 Bug分析
使用 JDBC 时,在URL中设置 useCursorFetch=true 的参数,希望开启 MySQL Server Side 游标功能。但是要使用 MySQL Server Side 游标需要满足下面条件:
- 必须是SELECT语句
- 设置了fetchSize > 0
- 设置了useCursorFetch = true
- 数据集类型为ResultSet.TYPEFORWARDONLY
- 数据集并发设置为ResultSet.CONCURREADONLY
- Server versions 5.0.5 or newer
这是因为 JDBC 在代码层面做了如下限制:
// we only create cursor-backed result sets if // a) The query is a SELECT // b) The server supports it // c) We know it is forward-only (note this doesn't preclude updatable result sets) // d) The user has set a fetch size if (this.resultFields != null && this.useCursorFetch && getResultSetType() == ResultSet.TYPE_FORWARD_ONLY && getResultSetConcurrency() == ResultSet.CONCUR_READ_ONLY && getFetchSize() > 0) { packet.writeByte(OPEN_CURSOR_FLAG); } else { packet.writeByte((byte) 0); // placeholder for flags }
那如何开启游标功能呢?以下是 JDBC 部分功能在进行预处理是开启游标的示例:
public static void testPrepareStmt() { Connection conn = null; PreparedStatement stmt = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/poc?useCursorFetch=true" "root" "123456"); stmt = conn.prepareStatement("select long_col_1 long_col_2 from problemTable where to_days(create_time) <= to_days(now()) and id = ?"); stmt.setFetchSize(10); stmt.setInt(1 2); ResultSet rs = stmt.executeQuery(); int count = 0; while(rs.next()){ count; System.out.println("########### row " count " ###################"); System.out.println("long_col_1 : " rs.getString(1)); System.out.println("long_col_2 : " rs.getString(2)); System.out.println(); } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException ce) { ce.printStackTrace(); } finally { try { if (stmt != null) { stmt.close(); } if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
尾声
#1122 和#1124 两个Bug从被定位到抓获认罪只用了5天,随后 DBLE 社区发布 DBLE 2.19.03.0 版本,将真相大白于天下。
我们始终相信:真相只有一个!至此DBLE又踩平了一个 MyCat 的坑。