概述
KafkaConsumer is not safe for multi-threaded access的报错通常是因为KafkaConsumer被多个线程共享导致的。在Kafka 2.4版本的源码中我看到该特性仍然不被支持,如果遇到多线程不安全访问的情况就会抛出异常。
以下是可解决该问题的五种方式:
解决方案
1.独立管理消费者
解决方法1:为每个线程创建一个KafkaConsumer对象
这是最简单和最可靠的方法,每个线程拥有自己的KafkaConsumer对象,线程之间不会共享任何资源。以下是代码示例:
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "localhost:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "test-group",
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
def createConsumer(): KafkaConsumer[String, String] = {
val consumer = new KafkaConsumer[String, String](kafkaParams)
consumer.subscribe(Collections.singletonList("test-topic"))
consumer
}
val rdd = sc.parallelize(1 to 100, 10)
val offsetsAndMetadata = rdd.mapPartitions(iter => {
val consumer = createConsumer()
val records = consumer.poll(1000)
val endOffsets = consumer.endOffsets(consumer.assignment()).asScala
consumer.close()
endOffsets.iterator.map { case (tp, eo) =>
new OffsetAndMetadata(eo)
}
})
上述代码中,我们为每个分区创建了一个独立的KafkaConsumer对象,消费数据后关闭了该对象。这种方法具有最好的性能和可靠性,但是需要更多的资源和时间。
2.关闭spark消费者缓存
解决方案2:该方法实际与方案1原理相同,但只需要设置SparkConf参数。
添加 conf.set(“spark.streaming.kafka.consumer.cache.enabled”, “false”)
这个问题发生的原因是spark缓存kafka消费者,在官方文档对此参数有描述。
3.采用官方API不开多线程
解决方法3:使用KafkaUtils.createRDD方法
KafkaUtils.createRDD方法是Spark提供的一种用于从Kafka消费数据并创建RDD的方法。该方法在内部实现中使用了KafkaConsumer对象,但是将其封装在RDD的函数中,并自动处理了多线程访问的问题。以下是代码示例:
val ssc = new StreamingContext(sparkConf, Seconds(1))
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "localhost:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "test-group",
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](Seq("test-topic"), kafkaParams)
)
stream.map(record => (record.key, record.value)).print()
ssc.start()
ssc.awaitTermination()
上述代码中,我们使用了KafkaUtils.createDirectStream方法从Kafka消费数据并创建DStream。该方法会创建一个KafkaConsumer对象并将其封装在DStream的RDD操作中。这是一种非常方便的方法,不需要手动管理KafkaConsumer对象。
4.手动处理多线程锁
解决方法4:使用共享的KafkaConsumer对象
如果你一定要共享同一个KafkaConsumer对象,并且确定你的能够正确地处理多线程并发访问,则可以通过以下方式解决:
- 为KafkaConsumer对象添加锁,确保每个线程在访问KafkaConsumer对象时,都先获取到锁,释放锁后才允许其他线程访问该KafkaConsumer对象。
- 为KafkaConsumer对象创建一个对象池,每个线程在需要访问KafkaConsumer对象时,从对象池中获取,使用完毕后放回对象池,其他线程再获取。常见的对象池框架有Apache Commons Pool和Java ObjectPool。
请注意,虽然共享同一个KafkaConsumer对象的做法会有一些性能优势,但是必须确保代码能够正确处理并发访问问题,只推荐高手采用这种办法。
5.升级Spark到2.4+
解决方案5:根据相关ISSUE:issues.apache.org/jira/browse…
问题描述:[SPARK 2.2] | Kafka Consumer | KafkaUtils.createRDD throws Exception - java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access.
该问题单状态说明在Spark2.4修复了相应的问题,如果有条件可以升级尝试。