Spark消费Kafka数据多线程异常的解决方案

481 阅读3分钟

概述

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对象,并且确定你的能够正确地处理多线程并发访问,则可以通过以下方式解决:

  1. 为KafkaConsumer对象添加锁,确保每个线程在访问KafkaConsumer对象时,都先获取到锁,释放锁后才允许其他线程访问该KafkaConsumer对象。
  2. 为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修复了相应的问题,如果有条件可以升级尝试。