大数据-102 Spark Streaming 与 Kafka 集成全解析:Receiver 与 Direct 两种方式详解 附代码案例

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI篇持续更新中!(长期更新)

AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!"快的模型 + 深度思考模型 + 实时路由",持续打造实用AI工具指南!📐🤖

💻 Java篇正式开启!(300篇)

目前2025年09月15日更新到: Java-124 深入浅出 MySQL Seata框架详解:分布式事务的四种模式与核心架构 MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

章节内容

上节我们完成了如下的内容:

  • Spark Streaming DStream 有状态转换
  • DStream 有状态转换 案例

基础介绍

针对不同的Spark、Kafka版本,集成处理数据的方式有两种:

  • Receiver Approach
  • Direct Approach

对应的版本: 版本的发展:

Kafka-08 接口

Receiver based Approach

基于 Receiver 的 Kafka 消费方式采用 Kafka 旧版本(0.8.2.1 及之前版本)的高阶消费者 API 实现。这种方式的整体工作流程如下:

  1. 数据接收与存储

    • 每个 Receiver 作为一个长期运行的 Task 被调度到 Executor 上执行
    • 接收到的 Kafka 数据首先存储在 Spark Executor 的内存中,底层通过 BlockManager 进行管理
    • 默认情况下,每 200ms(由 spark.streaming.blockInterval 参数控制)会将累积的数据生成一个 Block
    • 这些 Block 会被复制到其他 Executor 以实现容错(默认复制因子为 2)
  2. 数据处理流程

    • Spark Streaming 定期生成 Job 时,会根据这些 Block 构建 BlockRDD
    • 最终这些 RDD 会被作为普通的 Spark 任务执行
    • 每个 Block 对应 RDD 的一个分区,因此可以通过调整 blockInterval 来控制 RDD 的分区数量
  3. 关键特性与注意事项

    Receiver 部署特性

    • 每个 Receiver 作为一个常驻线程运行在 Executor 上,会持续占用一个 CPU 核心
    • Receiver 数量由调用 KafkaUtils.createStream() 的次数决定,每次调用创建一个 Receiver
    • 可通过多个 createStream() 调用来实现多个 Receiver 并行消费

    并行度限制

    • Kafka Topic 的分区数与 Spark RDD 分区数没有直接关联
    • 增加 Kafka Topic 的分区数只会增加单个 Receiver 内部的消费线程数
    • 实际的 Spark 处理并行度仍由 Block 数量决定
    • 示例:一个 Receiver 消费 4 分区 Topic,但仍只生成单个 RDD 分区

    性能考量

    • 数据本地性问题:包含 Receiver 的 Executor 会优先被调度执行 Task,可能导致集群负载不均衡
    • 默认 blockInterval 为 200ms,可根据数据量调整:
      • 小数据量:可适当增大间隔减少开销
      • 大数据量:可减小间隔提高并行度

    可靠性保障

    • 默认配置下,Receiver 方式可能在故障时丢失已接收但未处理的数据
    • 可通过启用预写日志(WAL)提高可靠性:
      • 设置 spark.streaming.receiver.writeAheadLog.enable=true
      • 数据会先写入 HDFS 等可靠存储
      • 但会带来额外的磁盘 IO 开销,降低吞吐量约 10-20%
  4. 典型应用场景

    • 适合对延迟不敏感、吞吐量适中的场景
    • 当需要与 Kafka 0.8.x 旧版本兼容时
    • 需要简单实现 Exactly-once 语义时可结合 WAL 使用
    • 不适合需要高吞吐、低延迟或严格资源隔离的场景

Kafka-08接口(Receiver方式)

  • Offset 保存在ZK中,系统管理
  • 对应Kafka版本 0.8.2.1 +
  • 接口底层实现使用Kafka旧版消费者 高阶API
  • DStream底层实现为BlockRDD

Kafka-08接口(Receiver with WAL)

  • 增强了故障恢复的能力
  • 接收的数据与Driver的元数据保存到HDFS
  • 增加了流式应用处理的延迟

Direct Approach

Direct Approach 是 Spark Streaming 不使用 Receiver 集成 Kafka 的方式,在企业生产环境中使用较多,相较于 Receiver,有以下特点:

  • 不使用 Receiver,减少不必要的CPU占用,减少了 Receiver接收数据写入BlockManager,然后运行时再通过 BlockId、网络传输、磁盘读取等来获取数据的整个过程,提升了效率,无需WAL,进一步减少磁盘IO
  • Direct方式生的RDD是KafkaRDD,它的分区数与Kafka分区数保持一致,便于把控并行度。注意:在Shuffle 或 Repartition 操作后生成的 RDD,这种对应关系会失效
  • 可以手动维护 Offset,实现 Exactly Once 语义

Kafka-10 接口

Spark Streaming 与 Kafka 0.10整合,和 0.8版本的Direct方式很像,Kafka的分区和Spark的RDD分区是一一对应的,可以获取 Offsets 和 元数据,API使用起来没有显著的区别。

添加依赖

xml 复制代码
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
  <version>${spark.version}</version>
</dependency>

不要手动添加 org.apache.kafka相关的依赖,如 kafka-clients,spark-streaming-kafka-0-10已经包含相关的依赖了,不同的版本会有不同程度的不兼容。

使用 kafka010接口从Kafka中获取数据:

  • Kafka集群
  • Kafka生产者发送数据
  • Spark Streaming 程序接收数

KafkaProducer

编写代码

scala 复制代码
package icu.wzk

import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord}
import org.codehaus.jackson.map.ser.std.StringSerializer

import java.util.Properties

object KafkaProducerTest {

  def main(args: Array[String]): Unit = {
    // 定义 Kafka 参数
    val brokers = "h121.wzk.icu:9092"
    val topic = "topic_test"
    val prop = new Properties()
    prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])
    prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])

    // KafkaProducer
    val producer = new KafkaProducer[String, String](prop)
    for (i <- 1 to 1000) {
      val msg = new ProducerRecord[String, String](topic, i.toString, i.toString)
      // 发送消息
      producer.send(msg)
      println(s"i = $i")
      Thread.sleep(100)
    }
    producer.close()
  }
}

运行测试

shell 复制代码
i = 493
i = 494
i = 495
i = 496
i = 497
i = 498
i = 499
i = 500
i = 501
i = 502
i = 503
i = 504

运行过程截图为:

查看Kafka

我们在服务器上查看当前Kafka中的队列信息:

shell 复制代码
kafka-topics.sh --list --zookeeper h121.wzk.icu:2181

可以看到队列已经加入了,spark_streaming_test01:

KafkaDStream

编写代码

scala 复制代码
package icu.wzk

object KafkaDStream1 {

  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)
    val conf = new SparkConf()
      .setAppName("KafkaDStream1")
      .setMaster("local[*]")

    val ssc = new StreamingContext(conf, Seconds(2))
    val kafkaParams: Map[String, Object] = getKafkaConsumerParameters("wzkicu")
    val topics: Array[String] = Array("spark_streaming_test01")

    // 从 Kafka 中获取数据
    val dstream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils
      .createDirectStream(
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams))
    // dstream输出
    dstream.foreachRDD {
      (rdd, time) => if (!rdd.isEmpty()) {
        println(s"========== rdd.count = ${rdd.count()}, time = $time ============")
      }
    }

    ssc.start()
    ssc.awaitTermination()
  }

  private def getKafkaConsumerParameters(groupId: String): Map[String, Object] = {
    Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "h121.wzk.icu:9092",
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],
      ConsumerConfig.GROUP_ID_CONFIG -> groupId,
      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG -> "earliest",
      ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG -> (false: java.lang.Boolean)
    )
  }
}

运行结果

shell 复制代码
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/Users/wuzikang/.m2/repository/org/apache/spark/spark-unsafe_2.12/2.4.5/spark-unsafe_2.12-2.4.5.jar) to method java.nio.Bits.unaligned()
WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
========== rdd.count = 1000, time = 1721721502000 ms ============

运行截图如下:

生成数据

继续启动 KafkaProducer 的程序,让数据不断地写入 我们会看到控制台输出内容如下:

shell 复制代码
========== rdd.count = 1000, time = 1721721502000 ms ============
========== rdd.count = 9, time = 1721721710000 ms ============
========== rdd.count = 19, time = 1721721712000 ms ============
========== rdd.count = 19, time = 1721721714000 ms ============
========== rdd.count = 19, time = 1721721716000 ms ============
========== rdd.count = 20, time = 1721721718000 ms ============
========== rdd.count = 19, time = 1721721720000 ms ============
========== rdd.count = 19, time = 1721721722000 ms ============
========== rdd.count = 19, time = 1721721724000 ms ============

运行结果如下图所示:

相关推荐
生无谓2 小时前
spring.factories作用
后端
Focusbe2 小时前
为什么 “大前端” 需要 “微前端”?
前端·后端·架构
考虑考虑3 小时前
Optional中的flatMap使用
后端
张高培是我的爸爸3 小时前
DevOps 核心工具:深入浅出 Jenkins 自动化与实践
后端
王道长AWS_服务器3 小时前
AWS 的存储方案全对比:EBS、S3、EFS 用在哪?
后端·aws
用户4099322502123 小时前
能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL 凭什么这么香?
后端·ai编程·trae
生无谓3 小时前
拦截器和过滤器的区别
后端
Cache技术分享3 小时前
194. Java 异常 - Java 异常处理之多重捕获
前端·后端
张高培是我的爸爸3 小时前
(不用看视频)一文搞懂maven基础以及高级用法
后端