java.lang.ArrayIndexOutOfBoundsException: Index 38928 out of bounds for length 38928 at com.mysql.cj.NativeQueryBindings.getBinding(NativeQueryBindings.java:191) at com.mysql.cj.NativeQueryBindings.setFromBindValue(NativeQueryBindings.java:198) at com.mysql.cj.jdbc.ClientPreparedStatement.setOneBatchedParameterSet(ClientPreparedStatement.java:591) at com.mysql.cj.jdbc.ClientPreparedStatement.executeBatchWithMultiValuesClause(ClientPreparedStatement.java:675) at com.mysql.cj.jdbc.ClientPreparedStatement.executeBatchInternal(ClientPreparedStatement.java:409) at com.mysql.cj.jdbc.StatementImpl.executeBatch(StatementImpl.java:795) at LoadDataWorker.loadWarehouse(LoadDataWorker.java:364) at LoadDataWorker.run(LoadDataWorker.java:187) at java.base/java.lang.Thread.run(Thread.java:1583)
最终 ArrayIndexOutOfBoundsException 异常出现在 MySQL 驱动中,NativeQueryBindings#191 逻辑根据参数偏移位 parameterIndex 获取对应的参数值。从异常信息 Index 38928 out of bounds for length 38928 可以看出,bindValues 数组当前只有 38928 个值,需要通过 0-38927 来获取,初步判断应该是 Proxy 对于预编译 SQL 中的参数处理存在问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/** * Returns the structure representing the value that (can be)/(is) * bound at the given parameter index. * * @param parameterIndex * 0-based * @param forLongData * is this for a stream? * @return BindValue */ public BindValue getBinding(int parameterIndex, boolean forLongData) { if (this.bindValues[parameterIndex] != null && this.bindValues[parameterIndex].isStream() && !forLongData) { this.longParameterSwitchDetected = true; } returnthis.bindValues[parameterIndex]; }
//Thefollowing five values must add up to 100 //Thedefault percentages of 45, 43, 4, 4 & 4 match the TPC-C spec newOrderWeight=45 paymentWeight=43 orderStatusWeight=4 deliveryWeight=4 stockLevelWeight=4
//Directory name to create for collecting detailed result data. //Comment this out to suppress. resultDirectory=result/mysql/mysql8.0.direct.wh.81_%tY-%tm-%td_%tH%tM%tS osCollectorScript=./misc/os_collector_linux.py osCollectorInterval=1 osCollectorSSHAddr=chexiaopeng01@quick07v.mm.bjat.qianxin-inc.cn osCollectorDevices=net_eth0 blk_sda
/** * COM_STMT_PREPARE_OK packet for MySQL. * * @see <a href="https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_stmt_prepare.html#sect_protocol_com_stmt_prepare_response_ok">COM_STMT_PREPARE_OK</a> */ @RequiredArgsConstructor publicfinalclassMySQLComStmtPrepareOKPacketextendsMySQLPacket { privatestaticfinalintSTATUS=0x00; privatefinalint statementId; privatefinalint columnCount; privatefinalint parameterCount; privatefinalint warningCount; @Override protectedvoidwrite(final MySQLPacketPayload payload) { payload.writeInt1(STATUS); payload.writeInt4(statementId); // TODO Column Definition Block should be added in future when the meta data of the columns is cached. payload.writeInt2(columnCount); payload.writeInt2(parameterCount); payload.writeReserved(1); payload.writeInt2(warningCount); } }
既然 MySQL 协议中定义的 parameterCount 最大为 65535,那么 BenchmarkSQL 测试原生 MySQL 也应当报错,而实际反馈原生 MySQL 不会出现异常。为了一探究竟,我们打算再测试下原生 MySQL,看下协议上是如何处理的。调整 JDBC URL 直接指向 MySQL 数据库,并执行单测程序。
测试并抓包后发现,原生 MySQL 同样不支持 2 字节以上的 parameterCount,MySQL 会直接抛出异常。此时 MySQL 驱动捕获到 1390 异常码后,会将预编译 SQL 转换为非预编译 SQL,直接将参数拼接在 VALUES 中,然后再次发起请求。
到这里问题终于明确了,Proxy 对于预编译参数超过 65535 的情况,未进行异常校验,导致通过 Netty 返回报文时丢失了一个字节,进而出现 MySQL 驱动中报出的参数 Index 越界异常。
问题解决
使用 Wireshark 我们搞清楚了 Proxy 执行 BenchmarkSQL 出现参数 Index 越界的原因,当预编译参数超过 65535 时,需要参考 MySQL 的行为抛出异常,此时 MySQL 驱动会再次发起非预编译的请求,将参数拼接在 VALUES 中。在 Proxy MySQLComStmtPrepareExecutor 类中,我们增加对参数个数的校验,超过 65535 则抛出异常。