一、Spark Streaming 基础概念
1、DStream
Discretized Stream 是 SS 的基础抽象,代表持续性的数据流和经过各种 Spark 原语操作后的结果数据流。DStream 本质上是一个以时间为键,RDD 为值的哈希表,保存了按时间顺序产生的 RDD,而每个 RDD 封装了批处理时间间隔内获取到的数据。Spark Streaming每次将新产生的 RDD 添加到哈希表中,而对于已经不再需要的 RDD 则会从这个哈希表中删除,所以 DStream 也可以简单地理解为以时间为键的 RDD 的动态序列。如下图:
StreamingContext(sparkConf, Seconds(5)) ; (conf: SparkConf, batchDuration: Duration)
2、窗口时间间隔(窗口长度):windowDuration
窗口时间间隔又称为窗口长度,它是一个抽象的时间概念,决定了Spark Streaming 对 RDD 序列进行处理的范围与粒度,即用户可以通过设置窗口长度来对一定时间范围内的数据进行统计和分析。假如设置批处理时间间隔为 1s,窗口时间间隔为 3s。如下图,DStream 每 1s 会产生一个 RDD,红色边框的矩形框就表示窗口时间间隔,一个窗口时间间隔内最多有 3 个 RDD,Spark Streaming 在一个窗口时间间隔内最多会对 3 个 RDD 中的数据进行统计和分析。
window(windowDuration: Duration, slideDuration: Duration)
3、滑动时间间隔:slideDuration
滑动时间间隔决定了 SS 程序对数据进行统计和分析的频率。它指的是经过多长时间窗口滑动一次形成新的窗口,滑动时间间隔默认情况下和批处理时间间隔相同,而窗口时间间隔一般设置的要比它们两个大。在这里必须注意的一点是滑动时间间隔和窗口时间间隔的大小一定得设置为批处理时间间隔的整数倍。
如下图,批处理时间间隔是 1 个时间单位,窗口时间间隔是 3 个时间单位,滑动时间间隔是 2 个时间单位。对于初始的窗口 time 1-time 3,只有窗口时间间隔满足了才触发数据的处理。这里需要注意的一点是,初始的窗口有可能覆盖的数据没有 3 个时间单位,但是随着时间的推进,窗口最终会覆盖到 3 个时间单位的数据。当每个 2 个时间单位,窗口滑动一次后,会有新的数据流入窗口,这时窗口会移去最早的两个时间单位的数据,而与最新的两个时间单位的数据进行汇总形成新的窗口(time3-time5)。
二、SparkStreaming实战
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object WordCount {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1
val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中对数据进行切分形成一个RDD
//2.监听Socket接收数据
//ReceiverInputDStream就是接收到的所有的数据组成的RDD,封装成了DStream,接下来对DStream进行操作就是对RDD进行操作
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.72.131",9999)
//3.操作数据
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_,1))
val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
wordAndCount.print()
ssc.start()//开启
ssc.awaitTermination()//等待停止
}
}
1.在192.168.72.131 地址的机器上先执行nc -lk 9999
2.然后执行代码
3.不断的在1中输入不同的单词
hadoop spark sqoop hadoop spark hive hadoop
4.观察IDEA控制台输出
sparkStreaming每隔5s计算一次当前5s内的数据,然后将每个批次的数据输出
效果如下:
2. updateStateByKey
在上面的那个案例中存在这样一个问题:
每个批次的单词次数都被正确的统计出来,但是结果不能累加!
如果需要累加需要使用updateStateByKey(func)来更新状态.
object WordCount2 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1
val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中对数据进行切分形成一个RDD
//requirement failed: ....Please set it by StreamingContext.checkpoint().
//注意:我们在下面使用到了updateStateByKey对当前数据和历史数据进行累加
//那么历史数据存在哪?我们需要给他设置一个checkpoint目录
ssc.checkpoint("e://checkpoint")//开发中HDFS
//2.监听Socket接收数据
//ReceiverInputDStream就是接收到的所有的数据组成的RDD,封装成了DStream,接下来对DStream进行操作就是对RDD进行操作
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.72.131",9999)
//3.操作数据
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_,1))
//val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
//====================使用updateStateByKey对当前数据和历史数据进行累加====================
val wordAndCount: DStream[(String, Int)] =wordAndOneDStream.updateStateByKey(updateFunc)
wordAndCount.print()
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
//currentValues:当前批次的value值,如:1,1,1 (以测试数据中的hadoop为例)
//historyValue:之前累计的历史值,第一次没有值是0,第二次是3
//目标是把当前数据+历史数据返回作为新的结果(下次的历史数据)
def updateFunc(currentValues:Seq[Int], historyValue:Option[Int] ):Option[Int] ={
val result: Int = currentValues.sum + historyValue.getOrElse(0)
Some(result)
}
}
1.在192.168.72.131 地址的机器上先执行nc -lk 9999
2.然后执行以上代码
3.不断的在1中输入不同的单词,
hadoop spark sqoop hadoop spark hive hadoop
4.观察IDEA控制台输出
sparkStreaming每隔5s计算一次当前5s内的数据,然后将每个批次的结果数据累加输出。执行效果如下:
3. reduceByKeyAndWindow
滑动窗口转换操作的计算过程如下图所示,我们可以事先设定一个滑动窗口的长度(也就是窗口的持续时间),并且设定滑动窗口的时间间隔(每隔多长时间执行一次计算),比如设置滑动窗口的长度(也就是窗口的持续时间)为24H,设置滑动窗口的时间间隔(每隔多长时间执行一次计算)为1H,那么意思就是:每隔1H计算最近24H的数据。
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
object wordCount3 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1
val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中对数据进行切分形成一个RDD
//2.监听Socket接收数据
//ReceiverInputDStream就是接收到的所有的数据组成的RDD,封装成了DStream,接下来对DStream进行操作就是对RDD进行操作
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.72.131",9999)
//3.操作数据
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_,1))
//4.使用窗口函数进行WordCount计数
//reduceFunc: (V, V) => V,集合函数
//windowDuration: Duration,窗口长度/宽度
//slideDuration: Duration,窗口滑动间隔
//注意:windowDuration和slideDuration必须是batchDuration的倍数
//windowDuration=slideDuration:数据不会丢失也不会重复计算==开发中会使用
//windowDuration>slideDuration:数据会重复计算==开发中会使用
//windowDuration<slideDuration:数据会丢失
//下面的代码表示:
//windowDuration=10
//slideDuration=5
//那么执行结果就是每隔5s计算最近10s的数据
//比如开发中让你统计最近1小时的数据,每隔1分钟计算一次,那么参数该如何设置?
//windowDuration=Minutes(60)
//slideDuration=Minutes(1)
val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5))
wordAndCount.print()
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
}
1.先执行nc -lk 9999
2.然后执行以上代码
3.不断的在1中输入不同的单词
hadoop spark sqoop hadoop spark hive hadoop
4.观察IDEA控制台输出
现象:sparkStreaming每隔5s计算一次当前在窗口大小为10s内的数据,然后将结果数据输出。效果如下:
3.4. 统计一定时间内的热门词汇TopN
3.4.1. 需求
模拟百度热搜排行榜
- 统计最近10s的热搜词Top3,每隔5秒计算一次
- WindowDuration = 10s
- SlideDuration = 5s
object WordCount4 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1
val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中对数据进行切分形成一个RDD
//2.监听Socket接收数据
//ReceiverInputDStream就是接收到的所有的数据组成的RDD,封装成了DStream,接下来对DStream进行操作就是对RDD进行操作
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.72.131",9999)
//3.操作数据
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_,1))
//4.使用窗口函数进行WordCount计数
val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5))
val sorteDStream: DStream[(String, Int)] = wordAndCount.transform(rdd => {
val sortedRDD: RDD[(String, Int)] = rdd.sortBy(_._2, false) //逆序/降序
println("===============top3==============")
sortedRDD.take(3).foreach(println)
sortedRDD
}
)
//No output operations registered, so nothing to execute
sorteDStream.print
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
}
1.先执行nc -lk 9999
2.然后在执行以上代码
3.不断的在1中输入不同的单词
hadoop spark sqoop hadoop spark hive hadoop
4.观察IDEA控制台输出,效果如下:
三、Spark Streaming处理速度滞后于读取速度测试
def main(args:Array [String]){
val conf = new SparkConf().setAppName("NetCatStreamingWordCountDelay")
conf.setMaster("local [3]")
//每秒钟接收数据
val ssc = new StreamingContext(conf,Seconds(1))
val lines = ssc.socketTextStream("192.168.26.140" , 9999)
//每个处理应该花费大约4秒钟的时间。
lines.foreachRDD(rdd => {
println("即使rdd为空也是输出")
Thread.sleep(4 * 1000)
})
ssc.start()
ssc.awaitTermination()
}
上面的代码:
1.时间间隔设置为1秒,也就是说,每隔1秒钟,Spark Streaming将创建一个RDD
2.处理的速度是4秒,也就是,处理速度滞后于数据的读取速度
如何解读上面的统计信息:
1. Spark Streaming一共运行了1分钟35秒= 95秒
2.一共处理了23个批次,每隔处理的时间4秒多点,因为23个批次总耗时是92秒
3.批次的时间间隔是1秒
等待批次为什么是1?应该是95 / 1-23 = 72个才对
我从火花流UI中复制下面的内容,我不知道为什么Waiting批量是1,我的理解是它应该是72.
以下是我的理解:
1.总时间是1分35秒= 95秒
2.批量间隔为1秒,95秒内产生95个批次。
3.处理过的批次是23(正确,因为在我的处理代码中,它什么都不做,只是睡了4秒)
4.然后等待的批次应该是95-23 = 72
Tathagata Das回复:非常好的问题!这是因为当前的代码是这样写的,使得ui认为一个批处理只在实际开始处理时才等待。计算中不考虑在队列中等待的批处理。在等待中计数可能更直观。
1.处理时间指的是每个批次的耗用时间,统计的时间包括一个批次的处理耗时,批次的最小耗时。以及25%,50%,75%时间段的耗时
2.总延迟:1分10秒如何计算出来的?总共95秒,有23个批出来完(23秒),那么总延时是95-23 = 72秒。
3.调度延迟表示一个批次从入队到出队的延迟,上例中,最后一个批次的延时是1分6秒钟。会不断的累加。
3. Spark官方文档解释
除了Spark的监控功能外,Spark Streaming还有其他特定功能。当使用StreamingContext时,Spark Web UI将显示一个附加的Streaming
选项卡,其中显示有关正在运行的接收方(接收方是否处于活动状态,接收记录数,接收方错误等)和已完成批次(批处理时间,排队延迟等)的统计信息)。这可以用来监视流应用程序的进度。
Web UI中的以下两个指标尤为重要:
- 处理时间 - 处理每批数据的时间。
- 计划延迟 - 批次在队列中等待处理先前批次的时间。
如果批处理时间一直超过批处理间隔和/或排队延迟持续增加,则表示系统无法处理批生产速度快,落后的批次。在这种情况下,考虑减少批处理时间。
Spark Streaming程序的进度也可以使用StreamingListener接口进行监控,该接口允许您获取接收器状态和处理时间。请注意,这是一个开发人员API,将来可能会改进(即报告更多信息)
1、设置合理的批处理时间(batchDuration)。批处理时间应小于批处理间隔。
Spark Streaming 是微批处理。
SparkConf sparkConf = new SparkConf().setAppName("SparkStreaming").setMaster("local[*]");
JavaStreamingContext javaStreamingContext = new JavaStreamingContext(sparkConf, Durations.seconds(1000));
在构建StreamingContext的时候,需要我们传进一个参数,用于设置Spark Streaming批处理的时间间隔。Spark会每隔batchDuration时间去提交一次Job,如果你的Job处理的时间超过了batchDuration的设置,那么会导致后面的作业无法按时提交,随着时间的推移,越来越多的作业被拖延,最后导致整个Streaming作业被阻塞,这就间接地导致无法实时处理数据,这肯定不是我们想要的。
另外,虽然batchDuration的单位可以达到毫秒级别的,但是经验告诉我们,如果这个值过小将会导致因频繁提交作业从而给整个Streaming带来负担,所以请尽量不要将这个值设置为小于500ms。在很多情况下,设置为500ms性能就很不错了。
那么,如何设置一个好的值呢?我们可以先将这个值位置为比较大的值(比如10S),如果我们发现作业很快被提交完成,我们可以进一步减小这个值,知道Streaming作业刚好能够及时处理完上一个批处理的数据,那么这个值就是我们要的最优值。
找出适合您的应用程序的正确批处理大小的一种好方法是使用保守的批处理间隔(例如5-10秒)和低数据速率进行测试。要验证系统是否能够跟上数据速率,可以检查每个已处理批处理经历的端到端延迟的值(可以在Spark驱动程序log4j日志中查找“ Total delay”,也可以使用 StreamingListener 接口)。如果延迟保持与批次大小相当,则系统是稳定的。否则,如果延迟持续增加,则意味着系统无法跟上,因此不稳定。一旦有了稳定配置的想法,就可以尝试提高数据速率和/或减小批处理大小。注意,由于暂时的数据速率增加而引起的延迟的瞬时增加可能是好的,只要延迟减小回到较低的值(即小于批大小)即可。
2、增加Job并行度
我们需要充分地利用集群的资源,尽可能的将Task分配到不同的节点,一方面可以充分利用集群资源;另一方面还可以及时的处理数据。比如我们使用Streaming接收来自Kafka的数据,我们可以对每个Kafka分区设置一个接收器,这样可以达到负载均衡,及时处理数据(关于如何使用Streaming读取Kafka中的数据,可以参见《Spark Streaming和Kafka整合开发指南(一)》和《Spark Streaming和Kafka整合开发指南(二)》)。
再如类似reduceByKey()和Join函数都可以设置并行度参数。
3、使用Kryo系列化。
Spark默认的是使用Java内置的系列化类,虽然可以处理所有自继承java.io.Serializable的类系列化的类,但是其性能不佳,如果这个成为性能瓶颈,可以使用Kryo系列化类,关于如何在Spark中使用Kroy,请参见《在Spark中自定义Kryo序列化输入输出API》。使用系列化数据可以很好地改善GC行为。
4、缓存需要经常使用的数据
对一些经常使用到的数据,我们可以显式地调用rdd.cache()
来缓存数据,这样也可以加快数据的处理,但是我们需要更多的内存资源。
5、清除不需要的数据
随着时间的推移,有一些数据是不需要的,但是这些数据是缓存在内存中,会消耗我们宝贵的内存资源,我们可以通过配置spark.cleaner.ttl
为一个合理的值;但是这个值不能过小,因为如果后面计算需要用的数据被清除会带来不必要的麻烦。而且,我们还可以配置选项spark.streaming.unpersist
为true(默认就是true)来更智能地去持久化(unpersist)RDD。这个配置使系统找出那些不需要经常保有的RDD,然后去持久化它们。这可以减少Spark RDD的内存使用,也可能改善垃圾回收的行为。
6、设置合理的GC
GC是程序中最难调的一块,不合理的GC行为会给程序带来很大的影响。在集群环境下,我们可以使用并行Mark-Sweep垃圾回收机制,虽然这个消耗更多的资源,但是我们还是建议开启。可以如下配置:
1 | spark.executor.extraJavaOptions = -XX : +UseConcMarkSweepGC |
更多的关于GC行为的配置,请参考Java垃圾回收相关文章。这里就不详细介绍了。
7、设置合理的CPU资源数
很多情况下Streaming程序需要的内存不是很多,但是需要的CPU要很多。在Streaming程序中,CPU资源的使用可以分为两大类:(1)、用于接收数据;(2)、用于处理数据。我们需要设置足够的CPU资源,使得有足够的CPU资源用于接收和处理数据,这样才能及时高效地处理数据。
2、Spark Streaming 调优-多线程并行处理任务
对于spark streaming程序和spark程序的区别,最大的一个问题就是spark streaming程序需要控制每次处理的时间。
场景1:程序每次处理的数据量是波动的,比如周末比工作日多很多,晚八点比凌晨四点多很多。
一个spark程序处理的时间在1-2小时波动是OK的。而spark streaming程序不可以,如果每次处理的时间是1-10分钟,就很蛋疼。
设置10分钟吧,实际上10分钟的也就那一段高峰时间,如果设置每次是1分钟,很多时候会出现程序处理不过来,排队过多的任务延迟更久,还可能出现程序崩溃的可能。
场景2:程序需要处理的相似job数随着业务的增长越来越多
我们知道spark的api里无相互依赖的stage是并行处理的,但是job之间是串行处理的。spark程序通常是离线处理,比如T+1之类的延迟,时间变长是可以容忍的。而spark streaming是准实时的,如果业务增长导致延迟增加就很不合理。
举个例子:
目前在做的反作弊规则引擎,离线的处理程序是spark,在线的规则引擎是spark streaming。
离线的问题不大,在线规则引擎如果因为规则越来越多,每次batch处理的时间越来越长是不合理的。
解决方案:把job放到线程池里多线程执行
spark虽然是串行执行job,但是是可以把job放到线程池里多线程执行的。如何在一个SparkContext中提交多个任务
而spark streaming同样可以。
以下是关键代码:
DStream.foreachRDD{
rdd =>
//创建线程池
val executors=Executors.newFixedThreadPool(rules.length)
//将规则放入线程池
for( ru <- rules){
val task= executors.submit(new Callable[String] {
override def call(): String ={
//执行规则
runRule(ru,spark)
}
})
}
//每次创建的线程池执行完所有规则后shutdown
executors.shutdown()
}
注意点
1.最后需要executors.shutdown()。
如果是executors.shutdownNow()会发生未执行完的task强制关闭线程。
如果使用executors.awaitTermination()则会发生阻塞,不是我们想要的结果。
如果没有这个shutdowm操作,程序会正常执行,但是长时间会产生大量无用的线程池,因为每次foreachRDD都会创建一个线程池。
2.可不可以将创建线程池放到foreachRDD外面?
不可以,这个关系到对于scala闭包到理解,经测试,第一次或者前几次batch是正常的,后面的batch无线程可用。
3.线程池executor崩溃了就会导致数据丢失
原则上是这样的,但是正常的代码一般不会发生executor崩溃。至少我在使用的时候没遇到过。
结果
对于目前的规则引擎项目,使用了多线程并发提交Job的操作,可以在毫秒级处理多数据源大吞吐量且存在数据量波动的数据流。并且在资源充足的情况下,可以随意横向拓展业务且不增加延迟时间。
四、Kafka作为高并发
最近在项目中使用了Kafka作为高并发处理的一个环节,即多台机器采集数据后同时发送到Kafka,消费端接收数据后保存到elasticsearch里面。
在这个过程中,原本以为用Kafka就那样,建个topic分几个分区,数据生产端直接发送数据到该topic,然后消费端poll数据到本机处理就ok了。
实际上等项目跑起来后发现,数据消费速度很慢,完全达不到要求。我们的要求是每秒起码要有1万条的处理效率。
看到这里,很多人可能会说多开启几个消费端进程处理就行。因为Kafka消费端是非线程安全的,所以一个进程只能有一个消费端。实际上我后面加了几个消费端之后,处理的效率还是比较差的。
所以只能从代码逻辑入手了。
首先使用排除法,去除了插入es的影响,因为es使用bulk插入的速度也是验证过没问题的。
后面我们通过重构Kafka消费端处理就解决了该问题,原因及解决办法如下:
Kafka的项目实践_Gemini技术窝的博客-CSDN博客
1、幂等性验证降低速度
由于我们知道消息队列都是有可能导致的消息重复发送的,不管是网络抖动还是人为配置的原因,所以我在代码中原本是使用了redis存储消息的key,然后每次拉消息回来后,全部过滤一遍剔除重复的消息。后面我们重新思考了一下,对于我们的业务场景,多台机器采集了主机、CPU、内存等监控数据并发送到Kafka,这种监控数据不是特别在意重复问题,不像电商里面的订单一样涉及到了金钱,另外一方面我们做了一些其他的处理降低重复的概率。所以幂等性我就直接去掉了,处理如下:
生产者配置:props.put(“request.required.acks”, -1);//保证消息必须发送到Kafka
消费者配置:props.put(“auto.offset.reset”, “latest”);//只消费最新的数据
利用es的特性,如果插入的数据中ID一样的话会进行覆盖。
2、Kafka拉取消息和处理逻辑分开处理
这个是解决速度慢的关键,由于Kafka消费端是非线程安全,所以消费端拉取数据是使用单线程,处理数据则是多线程处理,代码逻辑如下:
创建线程池
private final ExecutorService threadPool = new ThreadPoolExecutor(
10, //corePoolSize, 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
30, //maximumPoolSize, 线程数的上限
30, TimeUnit.MINUTES, // 超过corePoolSize的线程的idle时长, 超过这个时间,多余的线程会被回收。
new ArrayBlockingQueue<>(30), // 任务的排队队列
Executors.defaultThreadFactory(), // 新线程的产生方式
(r, executor) - > { //拒绝策略
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
单线程消费数据:
List <MessageModel> msgList = new ArrayLis<>();
ConsumerRecords <String, String> records = alarmsConsumer.readRecords();
for (TopicPartition partition: records.partitions()) {
for (ConsumerRecord < String, String > record: records.records(partition)) {
MessageModel message = new MessageModel(record.key(), record.value());
msgList.add(message);
}
}
多线程处理数据
if (msgList != null && !msgList.isEmpty()) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
DealMessageInterface processor = new DealMessage();
//实际的处理逻辑
processor.dealMessage(msgList);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
总结一下:消息队列的幂等性校验要根据实际的情况来,有些甚至都不用考虑这个的;凡是需要快速处理的,根据情况尽量使用多线程模式。当然,很多时候不用去怀疑Kafka的效率,基本上都是自己的代码逻辑问题导致的速度慢。
二、Kafka Consumer消费性能提升
[root@hadoop101 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop101:9092 --topic mytopic01
>111
>222
>333
>444
>555
>666
>
[root@hadoop102 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic mytopic01 --consumer-property group.id=rootgroup01 --consu
333
555
666
[root@hadoop103 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop103:9092 --topic mytopic01 --consumer-property group.id=rootgroup01 --consumer-property
111
222
444
在任意节点上可以查看分组消费记录,这些消费记录都是保存下来的,即使新建消费者,也可以从上次最后的offset继续消费数据。
[root@hadoop101 kafka]$ bin/kafka-consumer-groups.sh --bootstrap-server hadoop101:9092 --group rootgroup01 --describe
Note: This will only show information about consumers that use the Java consumer API (non-ZooKeeper-based consumers).
TOPIC partition CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
mytopic01 0 5 5 0 consumer01-1c642a44-5ec3-40d1-8921-6948db5bfaf4 /192.168.1.102 consumer01
mytopic01 1 4 4 0 consumer01-1c642a44-5ec3-40d1-8921-6948db5bfaf4 /192.168.1.102 consumer01
mytopic01 2 4 4 0 consumer01-1c642a44-5ec3-40d1-8921-6948db5bfaf4 /192.168.1.102 consumer01
mytopic01 3 5 5 0 consumer02-1caeb802-a583-4126-981c-b246f76d59f5 /192.168.1.103 consumer02
mytopic01 4 4 4 0 consumer02-1caeb802-a583-4126-981c-b246f76d59f5 /192.168.1.103 consumer02
mytopic01 5 4 4 0 consumer02-1caeb802-a583-4126-981c-b246f76d59f5 /192.168.1.103 consumer02
[root@hadoop101 kafka]$
假定消费主题为helloTopic8 , 该主题设置的分区数量为3个。
当使用kafka的javaAPI消费数据时,如果启动同一个组下的多个consumer,程序不会报错,但是真正能消费到数据的consumer数量,只能和消费的主题的partition的个数一致(这里为3个)。其他的consumer不能消费到数据。
而当spark streaming 程序集成kafka后,DStream中的rdd的分区数和消费的helloTopic8的partition数量一致(也就是3个),所以运行spark streaming程序,也就相当于启动了3个task来读取kafka中helloTopic8的数据。当重复再启动一个程序时,会重新消费到helloTopic8中的3个分区的数量,导致原来的程序读取不到相应的数据,随即报错。
sparkstreaming 集成kafka后(Direct模式),同一个groupid下的多个spark-streaming consumer消费kafka中的数据时,会报错。(如何重现问题:把sparkstreaming集合kafka的程序,运行两次即可)
————————————————
该问题 类似于下面这个问题:
Caused by: java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access
val maped: DStream[String] = directStream.map(crd => (crd.value())) /*.cache()*/
val rdd = maped.foreachRDD(rdds => {
// 具体的业务逻辑是否合理不重要,对同一个rdd执行两次操作,然后进行join,这里会报错:
// Caused by: java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access
val rdd1 = rdds.filter(_.equals("a")).map((_, 1))
val rdd2 = rdds.filter(_.equals("b")).map((_, 1))
rdd1.leftOuterJoin(rdd2).foreach(println)
})
报错原因:
这里的两个rdd读取的是同一份数据,当执行action时,都会触发两次数据的读操作,(rdd中的一个分区对应着topic中的一个分区,也就是说kafka中的一个分区的数据这里被读取了2次) 但是,同一个分区的数据只能被一个consumer消费,所以这里报错。
解决方案:一个可行的解决方案是对rdd进行缓存或者checkpoint,然后要能保证,原始的kafka中的数据,只会被消费一次,然后剩下的数据消费都从缓存中获取数据。
maped.foreachRDD(rdd => {
// val rdds = rdd.cache()
val rdds = rdd.persist(StorageLevel.DISK_ONLY)
val rdd1 = rdds.filter(_.equals("a")).map((_, 1))
val rdd2 = rdds.filter(_.equals("b")).map((_, 1))
rdd1.leftOuterJoin(rdd2).foreach(println)
})
1. 方案一:在同一个Consumer Group中,创建多个Consumer,增加消费者性能。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/82b4c50a965bfa61ce08e1e1e9846e37.png#pic_center)
创建Topic
bin/kafka-topics.sh --create --zookeeper master:2181 --replication-factor 1 --partitions 10 --topic mul_consumer_10
表格中展示出不同Consumer实例下的执行结果:
Consumer 实例 | Partition | 结果 |
---|---|---|
8 | 10 | Consumer数量小于partition数量时,某个分区数据被消费完之后,会让空闲Consumer继续消费未消费的partition中的数据。(partition数量大于Consumer数量,会将多出的partition分给同一个Consumer) |
10 | 10 | 正常消费。每个Consumer实例分配一个partition,每个Consumer下的partition数据顺序消费。 |
12 | 10 | Consumer数量大于partition数量时,空闲的Consumer啥事也不干。 |
-
Consumer Thread 实现类
public class ConsumerThread implements Runnable { private KafkaConsumer<String,String> kafkaConsumer; private String topic ; public ConsumerThread(String brokers,String topicId , String topic) { Properties properties = buildKafkaProperty(brokers,topicId); this.kafkaConsumer = new KafkaConsumer<>(properties); this.topic = topic; this.kafkaConsumer.subscribe(Arrays.asList(this.topic)); } private static Properties buildKafkaProperty(String brokers,String groupId){ Properties properties = new Properties(); properties.put("bootstrap.servers", brokers); properties.put("group.id", groupId); properties.put("enable.auto.commit", "true"); properties.put("auto.commit.interval.ms", "1000"); properties.put("session.timeout.ms", "30000"); properties.put("auto.offset.reset", "earliest"); properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); return properties; } @Override public void run() { while (true){ ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(100)); for(ConsumerRecord<String,String> record : records){ System.out.print(Thread.currentThread().getName()); System.out.printf(":partition = %d, offset = %d ,key = %s , value = %s %n", record.partition(),record.offset(),record.key(),record.value()); } } } }
-
Consumer Group 实现类
public class ConsumerGroup { private List<ConsumerThread> consumerThreadList = new ArrayList<ConsumerThread>(); public ConsumerGroup(String brokers,String groupId,String topic,int consumerNumber) { for(int i = 0; i< consumerNumber;i++){ ConsumerThread consumerThread = new ConsumerThread(brokers,groupId,topic); consumerThreadList.add(consumerThread); } } public void start(){ for(ConsumerThread item : consumerThreadList){ Thread thread = new Thread(item); thread.start(); } } }
-
主类
public class ConsumerMain { public static void main(String[] args) { String brokers = "master:9092"; String groupid = "mul_consumer_group"; String topic2 = "mul_consumer_10"; int consumerNumber = 8; // 设置Consumer实例数量 ConsumerGroup consumerGroup = new ConsumerGroup(brokers,groupid,topic2,consumerNumber); consumerGroup.start(); } }
2. 方案二:只有一个Consumer ,创建多个Thread,增加消费者性能。
创建Topic
bin/kafka-topics.sh --create --zookeeper master:2181 --replication-factor 1 --partitions 5 --topic mul_consumer_5
表格中展示出多个线程下的执行结果:
Consumer Thread | Partition | 结果 |
---|---|---|
4 | 5 | 尽管可以提升消费者性能,但整个消费完全是乱序的。 |
-
Consumer Woker类 实现 Runnable接口
public class ConsumerWorker<K,V> implements Runnable { private final ConsumerRecords<K,V> records; private final Map<TopicPartition,OffsetAndMetadata> offsets; public ConsumerWorker(ConsumerRecords<K,V> record,Map<TopicPartition,OffsetAndMetadata> offsets){ this.records = record; this.offsets = offsets; } @Override public void run() { for(TopicPartition partition:records.partitions()){ List<ConsumerRecord<K,V>> partitionRecords = records.records(partition); for(ConsumerRecord<K,V> record : partitionRecords){ // 插入消息处理逻辑,本例只是打印消息 System.out.printf(Thread.currentThread().getName()); System.out.println(String.format(":partition = %d, offset = %d ,key = %s , value = %s %n", record.partition(),record.offset(),record.key(),record.value())); } // 上报位移信息 long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset(); synchronized(offsets){ if(!offsets.containsKey(partition)){ offsets.put(partition,new OffsetAndMetadata(lastOffset + 1)); }else{ long curr = offsets.get(partition).offset(); if(curr <=lastOffset + 1){ offsets.put(partition,new OffsetAndMetadata(lastOffset + 1)); } } } } } }
-
ConsumerThreadHandler创建多线程
public class ConsumerThreadHandler<K,V> { private final KafkaConsumer<K,V> consumer; //Java中对线程池定义的一个接口 private ExecutorService executors; private final Map<TopicPartition,OffsetAndMetadata> offsets = new HashMap<>(); public ConsumerThreadHandler(String brokerList,String groupId,String topic){ Properties props = new Properties(); props.put("bootstrap.servers",brokerList); props.put("group.id",groupId); // 手动提交offset此值设置为false props.put("enable.auto.commit","false"); props.put("auto.offset.reset","earliest"); props.put("key.deserializer", StringDeserializer.class.getName()); props.put("value.deserializer",StringDeserializer.class.getName()); consumer = new KafkaConsumer<>(props); consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() { @Override public void onPartitionsRevoked(Collection<TopicPartition> partitions) { consumer.commitSync(offsets); } @Override public void onPartitionsAssigned(Collection<TopicPartition> partitions) { offsets.clear(); } }); } /** * 消费主方法 * @param threadNumber 线程池中线程数 */ public void consume(int threadNumber){ // 手动创建线程池 executors = new ThreadPoolExecutor( threadNumber, threadNumber, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); // 进行消息消费 try{ while(true){ ConsumerRecords<K,V> records = consumer.poll(Duration.ofMillis(1000L)); if(!records.isEmpty()){ executors.submit(new ConsumerWorker<>(records,offsets)); } // 提交位移值 commitOffsets(); } }catch(WakeupException e){ // swallow this exception } } private void commitOffsets(){ // 尽量降低synchronized块对offsets锁定时间 Map<TopicPartition,OffsetAndMetadata> unmodfiedMap; // 对Offsets值进行加锁 synchronized(offsets){ //是空直接返回 if(offsets.isEmpty()){ return; } // 会返回一个“只读”的map unmodfiedMap = Collections.unmodifiableMap(new HashMap<>(offsets)); // 清空map offsets.clear(); } consumer.commitSync(unmodfiedMap); } public void close(){ //唤醒消费者。此方法是线程安全的,特别用于中止长时间轮询。 consumer.wakeup(); // 启动有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务 executors.shutdown(); } }
-
主类
public class Main { public static void main(String[] args) { String brokerList ="master:9092"; String topic = "mul_consumer_5"; String groupID = "mul_consumer_group"; final ConsumerThreadHandler<byte[],byte[]> handler = new ConsumerThreadHandler<>(brokerList,groupID,topic); // 根据CPU的核数,指定线程池创建线程数量 final int cpuCount = Runtime.getRuntime().availableProcessors(); Runnable runnable = new Runnable() { @Override public void run() { handler.consume(cpuCount); } }; new Thread(runnable).start(); try{ // 20秒后自动停止测试程序 Thread.sleep(200000L); }catch (InterruptedException e){ // swallow this exception } System.out.println("Starting to close the consumer ..."); handler.close(); } }
结论
方案一和方案二都能增加Kafka Consumer的消费性能,但方案二无法保证消息消费的顺序性,因此我们需要结合具体业务场景,进行具体的代码实现。