发现了一个非常诡异的问题,正在解决当中,把诡异问题记录下来,
有问题的是(代码一)示例,我的table.put(putList)这段代从效果上看没执行,从日志上我能看出来putList里的数据越来越多。putList.size()一直在增加
但是我如果把 val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))这个table的声明放在iterator.foreach的前边,就像代码示例二里的那么写,代码就可以执行,只是我每个Partition里都是一个独立的table,这问题就很大,我不是每个Partition里都有数据这导致我99%的table都是废操作,
除了浪费资源和浪费效率之外,时间长了还会出现异常:org.apache.hadoop.hbase.RegionTooBusyException,意思是我put次数太多了。所以这段代码必须优化,但是目前还没想到办法。
我这里有一个猜想,应该是每个Partition必须都提交之后才能提交,所以我代码2示例里每个数据table都创建了一个实例就会被执行,而放下边因为iterator.foreach不是每个Partition里都有数据所以,剩下的Partition相当于没执行或没执行完,而我有数据的Partition就一直在等其他的Partition有数据,所以没往下执行。
我的猜想好像是对了,昨天加班做了个实验,为此我决定简单的画一个图用来表达我的想法。图跟实现放下边了,代码三。
代码一:
df.foreachPartition(iterator => {
var putList = new util.ArrayList[Put]
iterator.foreach(message => {
val key= message.getAs[String]("key")
val requestMessage = message.getAs[Map[String, String]]("realtime")
var put: Put = new Put(key.getBytes())
requestMessage.keys.foreach {
key => put.addColumn("data".getBytes(), key.getBytes(), requestMessage(key).getBytes())
}
putList.add(put)
})
println("list:" + putList.hashCode() + ":" + putList.size())
// if (putList.size() > 0) {
val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))
table.put(putList)
println("table:" + table.hashCode() + ":" + putList.size())
table.close()
// }
}
代码二:
df.foreachPartition(iterator => {
var putList = new util.ArrayList[Put]
val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))
iterator.foreach(message => {
val key= message.getAs[String]("key")
val requestMessage = message.getAs[Map[String, String]]("realtime")
var put: Put = new Put(key.getBytes())
requestMessage.keys.foreach {
key => put.addColumn("data".getBytes(), key.getBytes(), requestMessage(key).getBytes())
}
putList.add(put)
})
println("list:" + putList.hashCode() + ":" + putList.size())
// if (putList.size() > 0) {
table.put(putList)
println("table:" + table.hashCode() + ":" + putList.size())
table.close()
// }
}
针对我的猜想我又做了个实验,实验成功了一半,我会把我的分析写下来,毕竟官方给的资料太少了,而且我也没找到我想看的部分。
下边首先是我的实验代码:if (putList.size() > 0)这个判断在代码一和二里都屏蔽了,但是开与不开的效果是一样的。
这里说一下我的猜想以及我对应的解决方案,但是这个方案只解决了一半,因为实现结果只成功了一半,数据只保存了一半。针对这个实验结果我下边会画一张图来解释我的理解。
先说一下我实验的方式,既然我的猜想是Partition必须全部执行,否则就都不执行,导致数据没进去,那我让Partition执行不就可以了吗,所以我先在iterator.foreach这行的下边执行了putList.clear(),毕竟putList是在iterator.foreach之前声明的。我在之后进行一下操作看看是否有效果。
另外我在iterator.foreach执行完成之后,执行了一行df.show(1),我的猜想是spark懒加载机制,那么我在进行一个action操作不就可以了,而我又不可能随便写个文件或者是别的输出,所以我用了个.show(1)显示一行数据。
实时证明我的实验成功了,代码三示例是可以保存数据的,但是我做了600条数据的实验,我发现确实有数据了,但是丢了一部分数据,没什么规律,数据条数越多,丢的越多。真对这个实验结果的猜测见下边图一:
代码三:
df.foreachPartition(iterator => {
var putList = new util.ArrayList[Put]
val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))
iterator.foreach(message => {
val key= message.getAs[String]("key")
val requestMessage = message.getAs[Map[String, String]]("realtime")
var put: Put = new Put(key.getBytes())
requestMessage.keys.foreach {
key => put.addColumn("data".getBytes(), key.getBytes(), requestMessage(key).getBytes())
}
putList.add(put)
if (putList.size() > 0) {
val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))
table.put(putList)
println("table:" + table.hashCode() + ":" + putList.size())
table.close()
}
putList.clear()
})
df.show(1)
猜想图如下,根据猜想,df.show(1)基本每次都是从partition1里查出来一条数据就完事了,所以partition2里的数据还是等于没有action操作,所以一直没被执行,从效果上看这部分数据就丢失了。
图一:
针对这个结果,我想到的对应办法有几个,并做了实验,结果都成功了:
1.我可以减小partition的数量,因为我的df在foreachPartition之前,有一个join操作,这个会触发shuffle操作,我可以在join之后加一个.repartition(1)
把我的分区数量改为1.但是这跟df.foreach和df.rdd.collect()就没有区别了,在一个分区里计算,效率太低,还容易内存溢出。(实验成功,1条数据没丢)
2.第2个方案,我可以让每个partition都执行一下一下show,比如我现在是设置的10秒的trigger时间,一共会有10条数据,但是我每次都show(100),这样明显查过并发量的输出,应该就会从所有partition里取数据,就会触发之前iterator.foreach里的操作了。但是这个办法还是不好,因为最终我只希望存入HBASE,其他的输出是在浪费我的CPU,内存,网络和硬盘一系列资源,这样不太好。(实验成功,1条数据没丢)
终极方案:
也是我最后暂时采用的写法,先初始化table,foreach后边判断putList大小进行put,最后table.close(),我做了一下测试,table并不会因为创建过多报错,所以浪费就浪费吧,而且在真是环境里实际上是不会浪费的,因为实际环境数据量很大所以每个Partition里肯定都是有数据的:
代码四:
def batchInsertHbase(df: Dataset[Row],df2: Dataset[Row], partitionNum: Int): Unit = {
val data = df.join(df2, df.col("id") === df2.col("id"), "left_outer").repartition(partitionNum)
data.foreachPartition(iterator => {
var putList = new util.ArrayList[Put]
//table初始化放在foreach的上边,因为我发现初始化table并关闭好像并不太浪费资源,我测试了循环初始化1万个table并关闭,并没有报错,getConnection返回的是一个静态连接,未来会优化,防止HBASE挂掉导致程序异常
val table: Table = HBaseConnectionManager.getConnection().getTable(TableName.valueOf(tableName))
iterator.foreach(message => {
val key= message.getAs[String]("key")
val requestMessage = message.getAs[Map[String, String]]("realtime")
var put: Put = new Put(key.getBytes())
requestMessage.keys.foreach {
key => put.addColumn("data".getBytes(), key.getBytes(), requestMessage(key).getBytes())
}
putList.add(put)
})
println("list:" + putList.hashCode() + ":" + putList.size())
if (putList.size() > 0) {
table.put(putList)
println("table:" + table.hashCode() + ":" + putList.size())
}
table.close()
})
println("批量插入HBASE:data_table")
}
后续更新来了:更新与2022-05-23
这个代码不执行的现象后来分析可能是停止了Spark任务之后,Kafka的Producer程序并没有关闭,这段时间会进入大量的数据几十万条,Spark正常启动的时候会一次性把增量全部加载进内存,导致看似程序压根没执行,其实是卡在数据加载那块了,程序其实压根加载不过来,会持续好久最后来个内存溢出什么的。
解决方式:
设置spark-kafka的一个属性maxOffsetsPerTrigger最大加载数据量,具体的量多少根据自己往kafka里放的数据大小来定就可以了。这样kafka启动的时候就只会一次性加载一部分增量不会卡死,后续也是每次加载那么多,所以你停SPARK任务的时候会有一部分数据堆积,但是只要你的任务写的没毛病,可能过几分钟之后数据就追上来了,依然是实时行,或者就是在停Spark任务之前先把Kafka的Producer程序停了,不过我觉得凡事还是靠自己比较好。
var reader = spark .readStream .format("kafka") .option("kafka.bootstrap.servers", props.getProperty("kafka.bootstrap.servers")) .option("subscribe", topic) .option("maxOffsetsPerTrigger", maxOffsetsPerTrigger)