在流计算中,我们一般会选择 Storm、Spark、Flink 等计算框架,而对消息队列的选则 一般是Rabbitmq、Kafka 等,本片文章我们主要介绍 Kafka + Flink 框架在流计算中所遇到的问题及解决方案。
先聊下一个很古老的问题,Flink 消费Kafka中的数据,Kafka 有好几个分区,而如何保证Flink按顺序消费呢?(说详细一点儿:假设 Kafka topic 的分区为 8 个,生产者向Kakfa 发送数据,这些数据会均匀分布在这 8个分区中,而Flink消费Kafka中的数据时,如何保证消费的顺序就是按照生产者生产的顺序进行消费呢?),这个问题我们很久之前就在讨论,最终的结论是无法在消费者端保证顺序消费(这里Kafka的分区不为1),但我们可以在Kafka的生产端实现让同一特征数据分布在同一个分区里面,从而保证在消费端对这一特征数据顺序消费。而现在我们讨论一下如何在消费端尽量的保证顺序消费。
思路如下:
在Spark 、Flink 等计算框架中的窗口函数可以很好的实现这一问题(但只是让数据更加精确一点儿),我们利用Flink 的窗口函数,让一段时间内的数据落在同一窗口内,然后对这个窗口内的数据按照时间排序,然后在进行计算,但是在实际生产中我们又会遇到迟到的数据,此时我们的窗口时间又不能太长,而对于迟到的数据该如何监控、我们又该如何解决这些迟到的数据呢 ?在 Flink 计算框架中 解决了这一问题,现在我们来看下具体的代码实现 (若有更好的解决方案,希望读者能够留言探讨)。
举一个我在生产中的一个简单的案例:
/**
* @Description 业务逻辑处理
* @param stream
*/
private def businessLogic(stream: DataStream[String]): Unit = {
val dataStream = stream.filter(_.nonEmpty).map(line => {
val record = JSON.parseObject(line)
val time = record.getString("query.$time")
val preEvent = record.getString("query.$event")
val preIp = record.getString("clientIP")
val preProject = record.getString("query.$project")
(time, preEvent, preIp, preProject)
})
val watermarkStream = dataStream.assignTimestampsAndWatermarks(new MyTimestampsAndWatermarks)
val outPutTag = new OutputTag[Tuple4[String,String,String,String]]("late-data") {}
watermarkStream.keyBy(_._3)
.timeWindow(Time.seconds(3))
.allowedLateness(Time.seconds(2)) //允许数据迟到2秒
.sideOutputLateData(outPutTag)
.trigger(EventTimeTrigger.create())
.process(new MyProcessWindowFunction)
}
这里的 MyTimestampsAndWatermarks ( dataStream.assignTimestampsAndWatermarks(new MyTimestampsAndWatermarks) ) 需要自己实现 AssignerWithPeriodicWatermarks 接口,具体代码如下:
/**
* 定义 MyTimestampsAndWatermarks
*/
case class MyTimestampsAndWatermarks() extends AssignerWithPeriodicWatermarks[(String, String, String, String)] {
var currentMaxTimestamp: Long = _
// 最大允许的乱序时间是10s
var maxOutOfOrderness = 10000L
/** 定义生成watermark的逻辑 */
override def getCurrentWatermark: Watermark = {
new Watermark(currentMaxTimestamp - maxOutOfOrderness)
}
/** 定义如何提取timestamp */
override def extractTimestamp(element: (String, String, String, String), previousElementTimestamp: Long): Long = {
//TODO timeStamp 应该取当前系统时间,这里做测试
val timeStamp = element._1.toLong
currentMaxTimestamp = Math.max(timeStamp, currentMaxTimestamp)
timeStamp
}
}
这里的 MyProcessWindowFunction 需要自己实现 ProcessWindowFunction 抽象类,具体代码如下:
/**
* 定义 MyProcessWindowFunction
*/
case class MyProcessWindowFunction() extends ProcessWindowFunction[(String, String, String, String), (String, String, String, String), String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[(String, String, String, String)],
out: Collector[(String, String, String, String)]): Unit = {
val keyStr = key.toString
val arrBuf = ArrayBuffer[Long]()
val mapValue = new mutable.HashMap[ String, mutable.HashMap[String, String]]()
val arr = arrBuf.toArray
//对窗口内的数据按照时间进行排序
Sorting.quickSort(arr)
//组装结果
elements.map(line => {
val map = new mutable.HashMap[String,String]()
val timeStr = line._1
arrBuf.append(timeStr.toLong)
map.put("preEvent",line._2)
map.put("preIp",line._3)
map.put("preProject",line._3)
mapValue.put(timeStr,map)
})
val arrayLength = arr.length
for (i <- 0 until arrayLength ){
val time = arr(i).toString
val listValue = mapValue.get(time).get
val preEvent = listValue.get("preEvent").get
val preIp = listValue.get("preIp").get
val preProject = listValue.get("preProject").get
val res = (time,preEvent,preIp,preProject)
//将组装好的结果输出
out.collect(res)
}
}
}
在窗口内我们实现了对数据的排序,当窗口触发时将窗口内的数据输出 ,从而保证顺序消费计算。
而对于迟到的数据,我们之前定义了 outPutTag ( val outPutTag = new OutputTag[Tuple4[String,String,String,String]]("late-data") {} ),现在我们获取到迟到的数据,
//获取迟到太久的数据
val sideOutput = watermarkStream.getSideOutput[Tuple4[String, String, String,String]](outPutTag)
而对于迟到的数据该如何处理,这里就不再讨论,根据自己的业务去处理,可进一步研究自己的数据为何迟到,该如何去解决。
对于这一问题,就介绍到这里,如有不合适的地方希望读者能够积极留言讨论,若转载请说明出处, 谢谢 !