摘要:Direct 模式上线前有两个问题绕不开:并行度怎么调,offset 怎么管。前者很多人栽在"executor 加了不少,吞吐还是上不去"上------因为 Direct 模式的并行度天花板是 Kafka 分区数;后者决定你能否做到 exactly-once。这篇把这两个问题一次讲清,各给一套能落地的配置和代码。
关键词:Spark Streaming, Direct 模式, 并行度, Kafka 分区, offset 管理, exactly-once
上篇:并行度设置
一、先记住一条铁律
Direct 模式下,RDD 的分区数 = Kafka 的分区数,每个 Kafka 分区对应一个 RDD 分区。这直接决定了并行度的上限。
所以一个最常见的误区是:executor 加了一堆、--executor-cores 调得很大,但 topic 只有 3 个分区------并行度还是 3,一个 batch 最多就 3 个 task 在跑,其余核全闲着。调并行度,先看 Kafka 分区数,别在 executor 数量上空转。
二、三个决定并行的因子

- Kafka 分区数:决定 RDD 分区数,是并行度的天花板。
- executor 核数:决定同时能跑多少 task。总核数要 ≥ 分区数,否则 task 排队。
- maxRatePerPartition:每个分区每个 batch 最多拉多少条,用来限流/反压。
三、调优目标和配置
调优目标就一句:一个 batch 的处理时间 < batchInterval。做不到,batch 就会堆积,端到端延迟越来越大。
处理跟不上时,按顺序三招:
- 增加 Kafka 分区数(提高并行度上限);
- 加 executor 核数(让 task 不排队);
- 调大 batchInterval(给每个 batch 更多时间)。
bash
# 提交时设置 executor 核数
spark-submit --executor-cores 4 --num-executors 6 ...
# 限流:每分区每 batch 最多拉 10000 条
# kafkaParams 里加:
"spark.streaming.kafka.maxRatePerPartition" -> "10000"
# 配合反压(动态调整拉取速率)
"spark.streaming.backpressure.enabled" -> "true"
两个注意点:
- 核数要给后续 stage 留余地。拉数据的 task 只占一部分核,shuffle、reduce 这些后续 stage 也要核。核数刚好等于分区数时,后续 stage 会没核可用。
- Direct 模式没有
blockInterval参数。那是 Receiver 模式切 block 用的,Direct 模式调了没效果,别搞混。
下篇:offset 管理
四、三种方式,选哪种

方式一:自动管理(默认,存 checkpoint)
什么都不用做,Spark 自动把 offset 存进 checkpoint,Job 成功自动提交。前提是开了 checkpoint 且 enable.auto.commit=false。
局限:offset 和业务状态一起锁在 checkpoint 里,没法独立回放、迁移。适合简单场景。
方式二:手动管理(commitAsync)
scala
stream.foreachRDD { rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
// ... 处理 rdd,写外部存储 ...
stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) // 成功才提交
}
核心是"处理成功才提交",实现处理---提交的原子性,这是 exactly-once 的实现方式。上一篇文章说过:offsetRanges 只能在第一个转换里取,rdd.map(...) 之后类型就丢了。
方式三:外部存储(ZK / 数据库)
把 offset 存到 ZK 或数据库,独立于 checkpoint。典型做法是把处理结果和 offset 放进同一个事务------结果写成功,offset 才推进,实现端到端 exactly-once。这是最严格、也是生产上对一致性要求高时的选择。
五、指定 offset 回放
有时候需要从某个位置重新消费(比如补数据、事故回放),用 Assign 策略 + 显式 fromOffset:
scala
val fromOffsets: Map[TopicPartition, Long] = Map(
new TopicPartition("topic-a", 0) -> 1000L,
new TopicPartition("topic-a", 1) -> 2000L
)
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
ConsumerStrategies.Assign[String, String](
fromOffsets.keys.toList, kafkaParams, fromOffsets)
)
fromOffsets 指定每个分区从哪个 offset 开始消费,配合外部存储的 offset 就能做精确回放。
六、总结
- 并行度铁律:RDD 分区数 = Kafka 分区数,调并行度先加 Kafka 分区,别空加 executor。
- 三个并行因子:Kafka 分区(上限)、executor 核数(不排队)、maxRatePerPartition(限流)。
- 调优目标:一个 batch 处理时间 < batchInterval;核数要给后续 stage 留余地。
- offset 三种方式:自动(存 checkpoint,简单)、手动(commitAsync,处理成功才提交)、外部存储(结果+offset 同事务,端到端 exactly-once)。
- 需要回放时用 Assign + fromOffsets 指定起始位置。
作者 :大数据技术实践者
博客 :blog.starzy.cn
GitHub :starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践