暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一个诡异的Impala Upsert错误问题排查

等我放学 2021-02-08
1250
Impala是Cloudera开源的mpp计算引擎,Kudu是Cloudera开源的列存组件。由于kudu对单条数据读写的快速响应,加上impala对Kudu的深入融合,使得Impala with HDFS+Kudu的模型得以兼顾海量数据的ad-hoc分析和离散数据的CURD操作。基于Impala的特点,小哥布林所负责的系统(下称B系统)采用了Impala作为交互查询的核心组件,并且用Impala+Kudu提供了数据API子模块的底层存储。

起因

B系统在数据API子模块是基于SpringCloud框架的,数据映射层采用了MyBaits,JDBC库使用了ImpalaJDBC41v2.6.12。在一个服务开发的过程中,小伙伴们发现了一个诡异的事情:upsert数据的时候,impala会插入之前提交query中的数据。
其中MyBaits的sqlmap定义如下:
第一次客户端传入的数据如下:

第二次客户端传入的数据如下:
客户端报文返回操作成功,在impala中输入select * from tablename
后显示结果如下(其中a
字段为主键):
客户端发送了两次共四条数据,impala也插入了四条数据,可是其中第三、四条数据与第一、二条是重复的,a:"3"
a:"4"
相关的两条数据却没插入成功。
另一个测试用例的运行,出现了更诡异的事情:
第一次客户端传入的数据如下:
第二次客户端传入的数据如下:
客户端报文返回操作成功,在impala中输入select * from tablename
后显示结果如下:
数据库中只有两条记录,而且每条记录均不是客户端发送的记录。

排错过程

根据第一种情况,首先会想到看看impala是不是执行出错了。于是登录了impalad web ui,查看一下query,发现如下所示:
impala接收到的query就是错误的,所以首先排除了impala组件本身的问题。
接下来就得看看是不是mybatis的问题了。mybatis这块讲真其实不大熟,刚好就乘着排错走一走源码了。从DAO接口调用的流程图如下:
DAO接口通过AutoWired注入以后被MapperProxy封装,调用DAO函数时,MapperProxy会在cachedMethod中找到函数句柄后调用该函数(如果cached中没有就新建一个),Method通过SqlSession找到query类型,并根据不同的类型调用SqlSession的各类方法。SqlSession找到实际的Executor(或者CachedExecutor)构造query,最终交由Statement实际执行。其中statement实际执行时,由Druid构造的PreparedStatement将根据实际的JDBC库选择合适的连接,构造合适的Statement后交由JDBC库执行。
跟踪堆栈信息可以得到,最终的statement是Hive41PreparedStatement
,是ImpalaJDBC提供的,其中关键的两个字段是m_prepareSql
m_parameterInputValues
:
第一次客户端请求时字段如下:
第二次客户端请求时字段如下:
由此可以看出,从Mybaits解析到stmt执行之前,SQL解析仍没有问题,所以排除Mybatis出Bug的可能。
排到这里,就有点崩了,因为ImpalaJDBC并没有开源。当前几乎可以确定是ImpalaJDBC解析SQL的时候出了问题,但排查进入了深水区,没办法只能反编译接着搞了。由于反编译的代码关联到IDE后行号无法对应,所以只能从堆栈信息中看函数输入和输出来判断当前执行逻辑。通过堆栈信息判断的程序执行逻辑如下:
上图是SQL拼接的执行逻辑,其中ImpalaInsertQueryGenerator
的主要作用是将?
参数替换为实际的参数值并生成最终执行的SQL语句。值得说明的是,upsert语句在JDBC底层是使用Insert的语法解析器,并通过执行器m_upsertQuery
开关将insert
替换为upsert
ImpalaInsertQueryGenerator
通过IPTNode
接口的递归调用来实现AST解析。
经过单步跟踪,终于发现在ImpalaInsertQueryGenerator
中有一段解析ListNode的代码,大意如下:
这段代码大意是将paramPTListNode
中每一个元素提出来进行单独的适配,而单独适配的函数大意如下:
由于query中的表达式为(?,?),(?,?)
,这是一个嵌套list类型,所以在visitChildren
匹配时会将进入visit(PTListNode paramPTListNode)
函数进行单项匹配。可是this.m_contexts
中存放了所有的input内容,所以m_paramValues
变量中其实存了n*n*m个元素,其中n为表达式list的长度,m为表达式list每一个sublist的长度。在最后生成sql时,ImpalaInsertQueryGenerator
会从m_paramValues
中逐个poll出解析的语句,长度为n*m。又因为ImpalaInsertQueryGenerator
是一个singleton,所以第二次客户端调用时,仍然会从m_paramValues
中poll出,即poll出了原来的元素,至此真相大白。有兴趣的小伙伴也可以根据这个逻辑看看第二个测试用例是怎么错的。

解决

由于ImpalaJDBC不开源,所以我们没法修改代码来彻底解决问题,只能绕过这个错误逻辑。根据上一节的排错过程,我们知道错误是本质是解析m_paramValues
的add数量(n*n*m)和poll数量(n*m)不一致造成的,m是恒定的,所以只能改n。当n=1时,n*n=n,所以将Mybatis中的多条语句批量upsert变成单条upsert即可以绕过问题,测试通过。
复盘本次排错,均是源码级的排错。但是对于没有源代码的jar,可以利用反编译+堆栈信息来大致判断错误逻辑,通过input和output来判断执行逻辑的具体位置,从而大致推算出错误发生的原因。

文章转载自等我放学,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论