Spark每日半小时(38)——Spark Streaming:性能调优

性能调优

从集群上的Spark Streaming应用程序中获得最佳性能需要进行一些调整。在高层次上,我们需要考虑两件事:

  1. 通过有效使用集群资源减少每批数据的处理时间。
  2. 设置正确的批量大小,以便可以像接收到的那样快速处理批量数据(即,数据处理与数据提取保持同步)。

减少批处理时间

可以在Spark中进行许多优化,以最大限度地缩短每个批处理的处理时间。

数据接收中的并行度

通过网络接收数据(如Kafka,Flume,Socket等)需要将数据反序列化并存储在Spark中。如果数据接收称为系统中的瓶颈,则考虑并行化数据接收。请注意,每个输入DStream都会创建一个接收单个数据流的接收器(在工作机器上运行)。因此,可以通过创建多个输入DStream可以分成两个Kafka输入流,每个输入流只接收一个主题。这将运行两个接收器,允许并行接收数据,从而提高整体吞吐量。这些多个DStream可以组合在一起以创建单个DStream。然后,可以在统一流上应用在单个输入DStream上应用的转换。如下:

int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<>(numStreams);
for (int i = 0; i < numStreams; i++) {
  kafkaStreams.add(KafkaUtils.createStream(...));
}
JavaPairDStream<String, String> unifiedStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
unifiedStream.print();

应考虑的另一个参数是接收器的块间隔,它由配置参数spark.streaming.blockInterval决定。对于大多数接收器,接收的数据存储在Spark的内存中之前合并为数据块。每批中的块决定了在类似map的转换中处理接收数据的任务书。每批每个接收器的任务数量大约是(批处理间隔/块间隔)。例如,200ms的块间隔将每2秒批次创建10个任务。如果任务数量太少(即,少于每台计算机的核心数),那么效率将会很低,因为所有可能的核心都不会用于处理数据。要增加给定批处理间隔的任务书,请减少块间隔。但是,建议的块间隔最小值约为50ms,低于该值,任务启动开销可能会出现问题。

使用多个输入流/接收器接收数据的替代方案是显式地重新分区输入数据流(使用inputStream.repartition(<number of partitions>))。这会在进一步处理之前将收到的批量数据分布到集群中指定数量的机器上。

数据处理中的并行度

如果在计算的任何阶段中使用的并行任务的数量不够高,则可能未充分利用集群资源。例如,对于像reduceByKey和reduceByKeyAndWindow的分布式reduce操作,默认的并行任务数由配置属性spark.default.parallelism控制。我们可以将并行级别作为参数传递。

数据序列化

通过调整序列化格式可以减少数据序列化的开销。在流式传输的情况下,有两种类型的数据被序列化。

  • 输入数据:默认情况下,通过Receiver接收的输入数据通过StorageLevel.MEMORY_AND_DISK_SER_2存储在执行程序的内存中。也就是说,数据被序列化为字节以减少GC开销,并且为了容忍执行器故障而被复制。此外,数据首先保存在内存中,并且仅在内存不足以保存流式计算所需的所有数据数据时才移除到磁盘。这种序列化显然有开销:接收器必须反序列化接收的数据并使用Spark的序列化格式重新序列化。
  • 流式传输操作生成的持久RDD:流式计算生成的RDD可以保留在内存中。例如,窗口操作将数据保留在内存中,因为它们将被多次处理。但是,与StorageLevel.MEMORY_ONLY的Spark Core默认值不同,流式计算生成的持久RDD默认使用StorageLevel.MEMORY_ONLY_SER(即序列化)保留,以最大限度地减少GC开销。

在这两种情况下,使用Kryo序列化可以减少CPU和内存开销。对于Kryo,请考虑注册自定义类,并禁用对象引用跟踪。

在需要为流应用程序保留地数据量不大地特定情况下,将数据(两种类型)保存为反序列化对象可能是可行地,而不会产生过多地GC开销。例如,如果我们使用几秒钟地批处理间隔而没有窗口操作,则可以尝试通过相应地显式设置存储级别来禁用持久数据中地序列化。这将减少由于序列化导致地CPU开销,可能在没有太多GC开销的情况下提高性能。

任务启动开销

如果每秒启动地任务数量很高(例如,每秒50或更多),则向从属设备发送任务地开销可能很大,并且将难以实现亚秒级延迟。通过以下更改可以减少开销:

  • 执行模式:在独立模式或粗粒度Mesos模式下运行Spark可以获得比细粒度Mesos模式更好地任务启动时间。

这些更改可以将批处理时间减少100毫秒,从而允许亚秒级批量大小可行。

设置正确的批次间隔

要使集群上运行的Spark Streaming应用程序保持稳定,系统应该能够以接收数据的速度处理数据。换句话说,批处理数据应该在生成时尽快处理。通过监视流式Web UI中的处理时间可以找到是否适用于应用程序,其中批处理时间应小于批处理间隔。

根据流式计算的性质,所使用的批处理间隔可能对应用程序在固定的一组集群资源上可以维持的数据速率产生重大影响。例如,让我们考虑一下早期的WordCountNetwork示例。对于特定数据速率,系统可能能够每2秒跟上报告字数,但不是每500毫秒。因此需要设置批处理间隔,以便可以维持生产中的预期数据速率。

确定适合我们的ing用程序批量大小的好方法是使用保守的批处理间隔(例如,5-10秒)和低数据速率进行测试。要验证系统是否能够跟上数据速率,我们可以检查每个已处理批处理所遇到的端到端延迟的值(在Spark驱动程序log4j日志中查找“总延迟”,或使用StreamingListener接口)。如果延迟保持与批量大小相当,则系统稳定。否则,如果延迟不断增加,则意味着系统无法跟上,因此不稳定。一旦了解了稳定的配置,就可以尝试提高数据速率和/或减少批量。注意,只要延迟减小到低值(即,小于批量大小),由于临时数据速率增加引起的延迟的瞬时增加可能是正常的。

内存调整

Spark Streaming应用程序所需的集群内存量在很大程度上取决于所使用的转换类型。例如,如果要在最后十分重的数据上使用窗口操作,那么我们的集群应该有足够的内存来保存10分钟的数据。或者,如果我们想使用updateStateByKey大量的键,那么需要的内存将很高。相反,如果我们想做一个简单的map-filter-store操作,那么必要的内存就会很低。

通常,由于通过接收器接收的数据与StorageLevel.MEMORY_AND_DISK_SER_2一起存储,因此不适合内存的数据将移除到磁盘。这可能会降低流应用程序的性能,因此建议我们根据应用程序的需要提供足够的内存。最好尝试小规模的查看内存使用情况进行相应估算。

内存调整的另一个方面是垃圾收集。对于需要低延迟的流应用程序,不希望有JVM垃圾收集引起大的暂停。

有一些参数可以帮助我们调整内存使用和GC开销:

  • DStream的持久性级别:如前面数据序列化部分所述,输入数据和RDD默认持久化为序列化字节。与反序列化持久性相比,这减少了内存使用和GC开销。启用Kryo序列化可进一步减少序列化大小和内存使用量。通过压缩(Spark配置spark.rdd.compress)可以实现内存使用的进一步减少,但代价是CPI时间。
  • 清除旧数据:默认情况下,DStream转换生成的所有输入数据和持久化RDD都会自动清除。Spark Streaming根据使用的转换决定何时清除数据。例如,如果我们使用10分钟的窗口操作,那么Spark Streaming将保留最后10分钟的数据,streamingContext.remember。
  • CMS垃圾收集器:强烈建议使用并发标记清除GC,以保持GC相关的暂停始终较低。尽管已知并发GC会降低系统的整体处理吞吐量,但仍建议使用它来实现更一致的批次处理时间。确保在驱动程序(使用:driver-java-options输入spark-submit)和执行程序(使用Spark配置spark.executor.extraJavaOptions)上设置CMS GC。
  • 其他提示:为了进一步降低GC开销,这里有一些尝试的建议。
    • 使用OFF_HEAP存储级别保留RDD
    • 使用具有较小堆大小的更多执行程序。这将降低每个JVM堆中的GC压力。

要记住的要点:

  • DStream与单个接收器相关联。为了获得读取并行性,需要创建多个接收器,即多个DStream。接收器在执行器内运行。它占据一个核心。确保在预订接收器插槽后又足够的内核进行处理,即spark.cores.max应考虑接收器插槽。接收器以循环方式分配给执行器。
  • 当从流源接收数据时,接收器创建数据块。每隔blockInterval毫秒生成一个新的数据块。在batchInterval期间创建N个数据块,其中N=batchInterval / blockInterval。这些块由当前执行程序的BlockManager分发给其他执行程序的块管理器。之后,将在驱动程序上运行的网络输入跟踪器通知块位置以进行进一步处理。
  • 在驱动程序上为batchInterval期间创建的块创建RDD。batchInterval期间生成的块是RDD的分区。每个分区都是Spark中的任务。blockInterval == batchInterval意味着创建了一个分区,并且可能在本地处理它。
  • 块中的映射任务在执行器中处理(一个接受块,另一个复制块),具有块不管块间隔,除非不是本地调度启动。具有更大的blockInterval意味着更大的块。较高的spark.locality.wait值会增加在本地节点上处理快的机会。需要在这两个参数之间找到平衡,以确保在本地处理更大的块。
  • 我们可以通过调用inputDstream.repartition(n)来定义分区数,而不是依赖于batchInterval和blockInterval。这会随机重新调整RDD中的数据以创建n个分区。为了更大的并行性,虽然以重新清洗为代价,也是可以接受的。
  • 如果我们由两个dstream,将形成两个RDD,并且将创建两个将一个接一个地安排的执行。为了避免这种情况,我们可以union这两个dstream。这将确保为dstream的两个RDD形成单个union RDD。然后,此union RDD被视为单个作业。但是,RDD的分区不会收到影响。
  • 如果批处理时间超过批处理间隔,那么显然接收者的内存将开始填满并最终导致抛出异常(BlockNotFoundException)。目前没有办法暂停接收器,使用SparkConf配置spark.streaming.reveiver.maxRate,可以限制接收器的速率。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值