使用的flink版本:1.9.1
異常描述
需求:
- 從kafka讀取一條數據流
- 經過filter初次篩選符合要求的數據
- 然后通過map進行一次條件判斷再解析。這個這個過程中可能返回null或目標輸出outData。
- 最后將outData通過自定義sink寫入hbase。
轉換核心代碼:
val stream: DataStream[Input] = source.filter(s => (!s.equals(null)) && (s.contains("\"type\":\"type1\"") || s.contains("\"type\":\"type2\"")))//一次過濾
.map(json => {
try {
val recode: JSONObject = JSON.parseObject(json)
val dataStr: String = recode.getString("data")
val type = recode.getString("type")
val data = JSON.parseObject(dataStr)
var id: String = ""
type match {
case "type1" => {
if (data.getInteger("act") == 2) { //二次過濾
if (data.getJSONArray("ids").toArray().length > 0)
id = recode.getString("id") + "," + data.getJSONArray("ids").toArray().mkString(",")
else
id = recode.getString("id")
Input( id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time"), recode.getLong("time") * 1000)//正常輸出----標記點:1
} else null//非目標輸出 導致問題的位置 此處給個隨便的默認值 只要不是null就不會出問題,但是這樣后面操作需要二次過濾-----標記點:2
}
case "type2" => {
if (data.getInteger("act") == 2) { //二次過濾
id = recode.getString("id")
Input(id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time"), recode.getLong("time") * 1000)//正常輸出----標記點:1
} else null //非目標輸出 導致問題的位置 此處給個隨便的默認值 只要不是null就不會出問題,但是這樣后面操作需要二次過濾 ----標記點:2
}
}
} catch {
case e => {
e.printStackTrace()
println("解析json失敗: ", json)
Input("id","sid", "sn", 0l)
}
}
}
)
val result: DataStream[Output] = stream.map(s => {
var rowkey = ""
s.id.split(",").map(id => rowkey += s"$id${9999999999l - s.ts}|")
if (rowkey.equals("")) {
null
} else {
Output(rowkey, s.sid, s.sn, s.ts + "")
}
})
result.addSink(new CustomSinkToHbase("habse_table", "cf", proInstance)).name("write to hbase").setParallelism(1)
自定義sink核心代碼
override def invoke(value: Output, context: SinkFunction.Context[_]): Unit = {
println(s"on ${new Date}, put $value to hbase invoke ") //輸出標記:1
try {
init()
val puts = new util.ArrayList[Put]()
value.rowkey.split("\\|").map(s => {
val rowkey = s
val put: Put = new Put(Bytes.toBytes(rowkey))
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("sid"), Bytes.toBytes(value.sid))
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("sn"), Bytes.toBytes(value.sn))
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("ts"), Bytes.toBytes(value.ts))
puts.add(put)
})
table.put(puts)
println(s"on ${new Date}, put $value to hbase succeese ")//輸出標記:2
} catch {
case e => {
e.printStackTrace()
if (table != null) table.close()
if (conn != null) conn.close()
}
}
}
執行情況
在程序啟動后,隨着數據流的進入會產生不一樣的結果:
- 如果數據從未有數據進入標記點2,那么一切正常
- 如果如果有數據進入標記點2,說明此時返回的是null,程序會馬上報錯:ExceptionInChainedOperatorException,后續的數據處理也會失敗,程序陷入死循環。
具體表現如下:
java.lang.Exception: org.apache.flink.streaming.runtime.tasks.ExceptionInChainedOperatorException: Could not forward element to next operator
at org.apache.flink.streaming.runtime.tasks.SourceStreamTask$LegacySourceFunctionThread.checkThrowSourceExecutionException(SourceStreamTask.java:217)
at org.apache.flink.streaming.runtime.tasks.SourceStreamTask.processInput(SourceStreamTask.java:133)
at org.apache.flink.streaming.runtime.tasks.StreamTask.run(StreamTask.java:301)
at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:406)
at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:705)
at org.apache.flink.runtime.taskmanager.Task.run(Task.java:530)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.flink.streaming.runtime.tasks.ExceptionInChainedOperatorException: Could not forward element to next operator
at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.pushToOperator(OperatorChain.java:654)
at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:612)
at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:592)
at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:727)
at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:705)
at org.apache.flink.streaming.api.operators.StreamSourceContexts$ManualWatermarkContext.processAndCollectWithTimestamp(StreamSourceContexts.java:310)
at org.apache.flink.streaming.api.operators.StreamSourceContexts$WatermarkContext.collectWithTimestamp(StreamSourceContexts.java:409)
at org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher.emitRecordWithTimestamp(AbstractFetcher.java:398)
at org.apache.flink.streaming.connectors.kafka.internal.KafkaFetcher.emitRecord(KafkaFetcher.java:185)
at org.apache.flink.streaming.connectors.kafka.internal.KafkaFetcher.runFetchLoop(KafkaFetcher.java:150)
at org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumerBase.run(FlinkKafkaConsumerBase.java:715)
at org.apache.flink.streaming.api.operators.StreamSource.run(StreamSource.java:100)
at org.apache.flink.streaming.api.operators.StreamSource.run(StreamSource.java:63)
at org.apache.flink.streaming.runtime.tasks.SourceStreamTask$LegacySourceFunctionThread.run(SourceStreamTask.java:203)
問題追蹤
在程序報錯后在taskmanager日志的表現為錯誤日志無限循環,web頁面的表現為任務的開始時間重置。
輔助輸出,確定程序出錯位置
通過在hbase中添加輔助輸出,結果如下
on Tue Apr 21 18:30:41 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
on Tue Apr 21 18:30:42 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
on Tue Apr 21 18:30:44 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
on Tue Apr 21 18:30:45 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
on Tue Apr 21 18:30:47 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
.
.
.
on Tue Apr 21 18:30:45 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
on Tue Apr 21 18:30:47 CST 2020, put Output(714114118412528160|,001,張三,1587471839) to hbase invoke
//並沒有到success這一步
如果數據流d1進入了標記點:2(輸出null);
那么后續的數據流d2進入標記點:1(正常輸出) ,此時在web頁面task-manager stdout的中出現d2在輸出標記:1 和輸出標記:2(沒有輸出2的部分)無限循環。
輸出標記:2 沒有執行 說明沒有寫hbase。加上錯誤產生的條件為要有數據進入標記點:2,初步分析是這個null的返回值影響到了后面hbase的操作。
問題解決
無效手段
- 寫hbase前過濾掉null的值
val result: DataStream[Output] = stream.map(s => {
var rowkey = ""
s.id.split(",").map(id => rowkey += s"$id${9999999999l - s.ts}|")
if (rowkey.equals("")) {
null
} else {
Output(rowkey, s.sid, s.sn, s.ts + "")
}
}).filter(_!=null)//過濾null
經過測試,此方法無效。
有效的手段
- 將二次過濾放到一次過濾的位置
source.filter(s => (!s.equals(null)) && (s.contains("\"type\":\"type1\"") || s.contains("\"type\":\"type2\"")) && (s.contains("\"act\":2"))//提前過濾act=2
問題解決,但是因為業務的問題,act不是通用條件,不具備通用性。當然可以進行了;進行兩次filter,但是過於繁瑣並且會產生多條數據流。
- 將標記點2的null改成默認值,然后通過二次過濾,去除默認值
type match {
case "type1" => {
if (data.getInteger("act") == 2) { //二次過濾
if (data.getJSONArray("ids").toArray().length > 0)
id = recode.getString("id") + "," + data.getJSONArray("ids").toArray().mkString(",")
else
id = recode.getString("id")
Input( id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time"), recode.getLong("time") * 1000)//正常輸出----標記點:1
} else Input("id","sid", "sn", 0l)//非目標輸出 默認值--標記點:2
}
case "type2" => {
if (data.getInteger("act") == 2) { //二次過濾
id = recode.getString("id")
Input(id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time"), recode.getLong("time") * 1000)//正常輸出----標記點:1
} else Input("id","sid", "sn", 0l) //非目標輸出 默認值--標記點:2
}
}
問題解決,但是從整體數據量來看,標記點1的數量僅為標記點2數量的六分之一到五分之一之間,此處會做很多無用的json解析。在大數據量的時候還是會對效率的些許影響
- 采用側輸出進行數據分流,將一次過濾的通過側輸出拆分,對拆分后的出具進行特定條件的二次過濾,然后進行對應的解析。
/**
* 數據流處理
*
* @param source
* @return
*/
def deal(source: DataStream[String]) = {
println("數據流處理")
//拆分數據流
val splitData: DataStream[String] = splitSource(source)
//解析type1的
val type1: DataStream[Input] = getMkc(splitData)
//解析type2
val type2: DataStream[Input] = getMss(splitData)
//合並數據流
val stream: DataStream[Input] = type1.union(type2)
//拼接rowkey
val result: DataStream[Output] = stream.map(s => {
var rowkey = ""
s.id.split(",").map(id => rowkey += s"$id${9999999999l - s.ts}|")
if (rowkey.equals("")) {
null
} else {
Output(rowkey, s.prdct_cd, s.sid, s.sn, s.ts + "")
}
})
//將結果寫入hbase
result.addSink(new CustomSinkToHbase("habse_table", "cf", proInstance)).name("write to hbase").setParallelism(1)
env.execute("test")
}
/**
* 從側輸出中獲取type1的數據,過濾開始演唱數據 .filter(_.contains("\"act\":2")) 進行解析
* @param splitData
* @return
*/
def getMkc(splitData: DataStream[String]): DataStream[Input] = {
splitData.getSideOutput(new OutputTag[String]("type1"))
.filter(_.contains("\"act\":2"))
.map(str => {
try {
val recode: JSONObject = JSON.parseObject(str)
val dataStr: String = recode.getString("data")
val data = JSON.parseObject(dataStr)
var id: String = ""
if (data.getJSONArray("ids").toArray().length > 0)
id = recode.getString("id") + "," + data.getJSONArray("ids").toArray().mkString(",")
else
id = recode.getString("id")
Input( id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time") * 1000)
} catch {
case e => {
e.printStackTrace()
println("解析json失敗: ", str)
Input("id","sid", "sn", 0l)
}
}
}
)
}
/**
* 從側輸出中獲取type2的數據,過濾開始演唱數據 .filter(_.contains("\"act\":2")) 進行解析
* @param splitData
* @return
*/
def getMss(splitData: DataStream[String]): DataStream[Input] = {
splitData.getSideOutput(new OutputTag[String]("type2"))
.filter(_.contains("\"act\":2"))
.map(str => {
try {
val recode: JSONObject = JSON.parseObject(str)
val dataStr: String = recode.getString("data")
val data = JSON.parseObject(dataStr)
var id: String = ""
id = recode.getString("id")
Input(id.reverse, data.getString("sid"), data.getString("sn"), recode.getLong("time") * 1000)
} catch {
case e => {
e.printStackTrace()
println("解析json失敗: ", str)
Input("id","sid", "sn", 0l)
}
}
}
)
}
/**
* 使用側輸出切分數據流
* @param source
* @return
*/
def splitSource(source: DataStream[String]) = {
source.process(new ProcessFunction[String, String] {
override def processElement(value: String, ctx: ProcessFunction[String, String]#Context, out: Collector[String]): Unit = {
value match {
case value if value.contains("\"type\":\"type1\"") => ctx.output(new OutputTag[String]("type1"), value)
case value if value.contains("\"type\":\"type2\"") => ctx.output(new OutputTag[String]("type2"), value)
case _ => out.collect(value)
}
}
})
}
問題解決,對比1的好處是,側輸出的時候,數據流還是只有一個,只是給數據打了一個標簽,並且對可后期業務的擴展很友好。
總結
其實雖然問題解決了,但是具體問題出現的原理並沒有整理明白。
目前猜測是null的輸出類型對后續的輸入類型有影響,但是具體的影響怎么發生,估計得抽空研究源碼才能知道了。后續有結果再更
本文為原創文章,轉載請注明出處!!!