这里的Spark Streaming 性能调优,主要涉及从Kafka读数据、往Kafka中写数据的过程。
Spark Streaming 对接 Kafka
Spark Streaming 对接 kafka有receive和direct两种方式。
基于Receiver的方式
这种方式利用接收器(Receiver)来接收kafka中的数据,其最基本是使用Kafka高阶用户API接口。对于所有的接收器,从kafka接收来的数据会存储在spark的executor中,之后spark streaming提交的job会处理这些数据。如下图:
需要注意的点:
- 在Receiver的方式中,Spark中的partition和kafka中的partition并不是相关的,所以如果我们加大每个topic的partition数量,仅仅是增加线程来处理由单一Receiver消费的主题。但是这并没有增加Spark在处理数据上的并行度。
- 对于不同的Group和topic我们可以使用多个Receiver创建不同的Dstream来并行接收数据,之后可以利用union来统一成一个Dstream。
- 如果我们启用了Write Ahead Logs复制到文件系统如HDFS,那么storage level需要设置成 StorageLevel.MEMORY_AND_DISK_SER,也就是
KafkaUtils.createStream(..., StorageLevel.MEMORY_AND_DISK_SER)
直连方式
在spark1.3之后,引入了Direct方式。不同于Receiver的方式,Direct方式没有receiver这一层,其会周期性的获取Kafka中每个topic的每个partition中的最新offsets,之后根据设定的maxRatePerPartition来处理每个batch。其形式如下图:
这种方法相较于Receiver方式的优势在于:
- 简化的并行:在Receiver的方式中我们提到创建多个Receiver之后利用union来合并成一个Dstream的方式提高数据传输并行度。而在Direct方式中,Kafka中的partition与RDD中的partition是一一对应的并行读取Kafka数据,这种映射关系也更利于理解和优化。
- 高效:在Receiver的方式中,为了达到0数据丢失需要将数据存入Write Ahead Log中,这样在Kafka和日志中就保存了两份数据,浪费!而第二种方式不存在这个问题,只要我们Kafka的数据保留时间足够长,我们都能够从Kafka进行数据恢复。
- 精确一次:在Receiver的方式中,使用的是Kafka的高阶API接口从Zookeeper中获取offset值,这也是传统的从Kafka中读取数据的方式,但由于Spark Streaming消费的数据和Zookeeper中记录的offset不同步,这种方式偶尔会造成数据重复消费。而第二种方式,直接使用了简单的低阶Kafka API,Offsets则利用Spark Streaming的checkpoints进行记录,消除了这种不一致性。
下面我们都采用直连的方式读取kafka数据。
直连方式的kafka offset管理
而在Direct的方式中,我们是直接从kafka来读数据,那么offset需要自己记录,可以利用checkpoint、数据库或文件记录或者回写到zookeeper中进行记录。
拉去数据量控制
spark.streaming.kafka.maxRatePerPartition=40000
spark.streaming.backpressure.enabled=true
使用高性能算子
- 使用reduceByKey/aggregateByKey替代groupByKey
- 使用mapPartitions替代普通map
- 使用foreachPartitions替代foreach
- 使用filter之后进行coalesce操作
- 使用repartitionAndSortWithinPartitions替代repartition与sort类操作
Spark Streaming 并行度
DStream的Partition个数
Spark Streaming 消费 Kafka的最直接的并行度和kafka的topic的partition个数相关,采用直连的方式topic的partition个数决定了Spark Streaming 的 RDD的partition个数。所以可以通过增加topic的paritition个数来提高Spark Streaming并行度。
Repatition
Spark 提供了api可以控制RDD的partition数量,使用repartition函数来增加partittion个数(会产生shuffle),使用coalesce函数来减少partition个数。
Executors和cpu核心数
executor的数量和每个executor配置的cpu cores直接影响着Spark的处理能力。每个Partition会对应启动一个task,每个task至少需要一个线程来处理。 --num-executors 控制分配的executor个数,--executor-memory控制每个executor分配的内存资源,--executor-cores控制每个executor的cpu cores。num-executors 和 executor-cores 根据DStream的partition个数和数据量大小设置。
举例:
假设Spark 提交到Yarn上运行,yarn有3个node manager节点,spark streaming任务执行中产生了6个partitions,那么num-executors和executor-cores如何设置呢?
针对上面的情况,为了充分利用分布式的特点,建议num-executors建议设成3;为了spark中的task能够快速的被执行,executor-cores建议设成2,这样可以实现每个cpu core处理一个task。
Spark on Yarn 弹性伸缩
Spark on Yarn提供了基于资源的使用情况,弹性伸缩的特性。
启用spark.dynamicAllocation,需要将 spark.shuffle.service.enabled和 spark.dynamicAllocation.enabled设为true,其他的 spark.shuffle.service.* 和 spark.dynamicAllocation.* 配置是可选的。
配置示例如下:
spark.shuffle.service.enabled=true
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.executorIdleTimeout=60s
spark.dynamicAllocation.initialExecutors=1
spark.dynamicAllocation.maxExecutors=5
spark.dynamicAllocation.minExecutors=0
去Spark Streaming中的shuffle
写Kafka
上文阐述了Spark如何从Kafka中流式的读取数据,下面我整理向Kafka中写数据。与读数据不同,Spark并没有提供统一的接口用于写入Kafka,所以我们需要使用底层Kafka接口进行包装。
最直接的做法我们可以想到如下这种方式:
rdd.foreachRDD{ r => {
r.foreachPartition {fp => {
if (fp.nonEmpty) {
MyKafkaProducer.setkafkaParams(kafkaProParams)
fp.foreach(e => {
// send to kafka
})
}
}
}
}
}
显然这种做法是不灵活且低效的,因为每个rdd的partition都需要建立一次连接。如何解决呢?
参考:
Spark踩坑记——Spark Streaming+Kafka;