一、背景介绍
在前一篇文章TDengine在Apache Hive中的探索和实践中,我们通过对2.3.8版本Apache Hive的代码改造,使之支持查询TDengine数据源的数据,并支持关联查询多个不同数据源。
在使用过程中,我们陆续发现了一些问题,本文内容就是其中之一,特此记录下。
二、问题背景
我们在Hive创建了4张表,表名分别为fvp、dn、enpoint、student。
1.fvp和dn是TDengine类型的表 2.enpoint是Mysql类型的表
这三个表都是使用JdbcStorageHandler创建的。通过上一篇博文的分析,可知其底层对应的InputFormat都为JdbcInputFormat。
3.student是关联HDFS数据的表
由于是原生的Hive表,所以对应的InputFormat为TextInputFormat。
三、问题场景
在hive中执行如下sql,一共关联了上述4张表
select count(*) as count from fvp inner join endpoint on fvp.source_zone_code=endpoint.endpoint inner join student on student.name=fvp.source_zone_code inner join dn on fvp.source_zone_code=dn.fqdn where fvp.source_zone_code='915J'
上述sql运行结束后,hive控制台偶尔报错,偶尔执行成功。
每次报错时截图如下
四、查看和分析YARN日志
因为Hive底层会生成MR任务执行在YARN上,所以我们去YARN上查看详细日志。 访问IP:19888/jobhistory查看job列表,找到刚才执行的job。
1.查看日志
点击job id
由于我们设置了set mapred.map.tasks=22,所以此处生成了22个MapTask。
点进去
我们找其中一个失败的MapTask,点击进去看看
继续点击logs
日志如下,但是只展示了部分。点击here按钮查看详细日志
详细日志如下
2.日志解读
通过最后的日志截图,我们发现,当前处理失败的MapTask对应的切片是student表切片hdfs://CNSZ22PL0272:9000/user/hive/warehouse/student/student.txt:9+
前面说过,student表是HDFS上的数据,底层对应的InputFormat为TextInputFormat。
而观察截图最后的报错信息,可以看到是由TDengineDatabaseAccessor类(我自己在源码中写的类)中的getColumnNames方法抛出的,该方法里会使用JDBC-RESTful方式来获取TDengine表的字段。
伪逻辑如下:
//通过DBCP数据源获取连接
......
//切换到数据库
stmt.executeUpdate("use 数据库名");
//查询
stmt.executeQuery("select 字段1,字段2...字段n from 表名");
由此,引发的困惑是:
为什么Hive在处理HDFS数据源的数据时,会涉及到TDengine数据源的代码?难道是多个数据源关联时,底层的InputFormat会共用?
五、问题分析
1.确定InputFormat类与切片是否对应
这一步排查思路是,排查当多个不同数据源进行连接查询时,Hive能否根据不同数据源,找到正确的InputFormat。
当Hive解析sql后,会根据生成的物理执行计划提交MR任务,MR任务根据InputFormat生成切片。
所以我们先找到生成切片的代码。
进入writeSplits方法后,最后进入如下逻辑,其中 job.getInputFormat()是CombineHiveInputFormat
于是我们看看CombineHiveInputFormat的方法
其中生成切片的过程很长,涉及到JdbcInputFormat和TextInputFormat的代码,但此处我们不关心。
我们只关心:
每个数据源的MapTask和其InputFormat是否对应得上
最终返回的切片,如下
观察生成的切片,结论如下:
我们有4个数据源的表,在没有在HIve中手动设置mapred.map.tasks参数的情况下,Hive最终生成了5个切片(其中student占2个)。更重要的是,每个数据源的切片信息和其InputFormat是能对应得上的。
这说明Hive是可以根据不同数据源,找到正确的InputFormat的。
2. 加日志分析
既然每个数据源的切片信息和其InputFormat都能对应得上,说明多数据源关联时不会出现混淆的问题,那么只能进一步分析了。
通过上面YARN的日志
发现每个MapTask类会调用到MapOperator的getConvertedOI方法,该方法中通过getDeserializer方法触发了TDengine相关类的代码
所以,我们可以尝试在getConvertedOI方法中加入日志,打印当前表名和切片信息
3. 构建源码包并重新上传
我是在Hive项目的hive-exec模块的MapOperaor类中加入的日志代码,为了让代码在MR任务运行时生效,需要重新上传jar。
首先,使用maven构建hive-exec模块,并在Hive的lib目录下重新上传。我的hive的lib路径为
/app/hive/apache-hive-2.3.8-bin/lib
其次,还需要在hadoop的lib目录里重新上传(因为是分布式集群环境)。我的Hadoop的lib路径为
/app/hadoop/hadoop-3.3.0/share/hadoop/common/lib
当这些工作完成后,重新启动Hive。
六、再次分析
1.查看日志
我们再次运行同样的sql,按照同样步骤观察YARN某个MapTask任务的日志
可以发现如下重要信息:
在任务处理切片依旧为student表切片的前提下,getConvertedOI方法一共调用了4次,每个表一次。 在最后一次处理dn表信息时,抛出了异常(开头提到dn表是TDengine类型的表)。
2.最终的误解
通过上面信息,说明最开始就是我们误解了:
当多数据源关联查询时,每个MapTask都会去获取其他表的信息,这是Hive本身正常的逻辑,而不是当初我认为的InputFormat混淆。
而后来借助更详细的日志,才及时帮我们发现了这种误解。 之所以耗费了这么大精力去证明这是个误会,还是因为我对Hive底层执行逻辑不了解导致的。
所以,解决方案是要搞明白为什么使用JDBC-RESTful方式来获取TDengine表的字段时会报错,而不是从Hive自身的逻辑里找答案。
七、问题解决
我使用的JDBC-RESTful方式来获取TDengine表字段,伪逻辑如下
//通过DBCP数据源获取连接
......
//切换到数据库
stmt.executeUpdate("use 数据库名");
//查询
stmt.executeQuery("select * from 表名");
在将该问题反馈给涛思公司的工程师后,得到的回复是
有可能是因为使用dbcp连接池,去不同的database查造成的。在查询sql中指定dbName应该就可以解决这个问题。
最终我使用如下方式解决了问题
//通过DBCP数据源获取连接
......
//查询
stmt.executeQuery("select * from 数据库名.表名");