Spark Streaming官方文档翻译Spark Streaming总览
Spark Streaming官方文档翻译基本概念之初始化与Dstream
Spark Streaming官方文档翻译基本概念之输入数据流和接收器
Spark Streaming官方文档翻译基本概念之转换操作
Spark Streaming官方文档翻译基本概念之输出操作
Spark Streaming官方文档翻译基本概念之sql与Mllib
Spark Streaming官方文档基本概念之缓存与检查点
Spark Streaming官方文档翻译基本概念之累加器、广播变量和检查点
Spark Streaming官方文档翻译Spark Streaming应用之部署,升级,监控
Spark Streaming官方文档翻译Spark Streaming性能调优
Spark Streaming官方文档翻译Spark Streaming容错
Spark Streaming官方文档翻译Spark Streaming +Kafka 集成指南
Spark Streaming官方文档翻译Spark Streaming自定义接收器
要在集群上的Spark Streaming应用程序中获得最佳性能,需要进行一些调整。这些已在调优指南中详细讨论。本节重点介绍一些最重要的内容。
通过网络接收数据(如Kafka、Flume、socket等)需要将数据反序列化并存储在Spark中。如果数据接收成为系统中的瓶颈,那么可以考虑将数据接收并行化。注意,每个输入DStream创建一个接收方(运行在工作机器上),接收一个数据流。因此,通过创建多个输入数据流并配置它们以从源接收不同的数据流分区,可以实现接收多个数据流。例如,一个接收两个数据主题的Kafka输入DStream可以分成两个Kafka输入流,每个输入流只接收一个主题。这将运行两个接收器,允许并行接收数据,从而提高总体吞吐量。可以将多个DStream合并在一起来创建单个DStream。然后,应用于单个输入DStream上的转换可以应用于统一的流。这是这样做的。
val numStreams = 5
val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) }
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()
另一个需要考虑的参数是接收器的block interva,它是由配置参数spark.stream.blockinterval决定的。对于大多数接收方来说,接收到的数据在存储到Spark内存之前会被合并成数据 block。每个批处理中的块的数量确定了在map转换中用于处理接收数据的任务数。每批每个接收方的任务数量是可预估的(batch interval / block interval)。例如,200 ms的block interval将每批2秒创建10个任务。如果任务的数量过低(即少于每台机器的内核数量),那么它将是低效的,因为有可用的内核未用于处理数据。若要增加给定batch interval的任务数量,请减少block interval。但是,建议的 block interval最小值为50 ms,低于这个值可能会导致任务启动开销出现问题。
使用多个输入流/接收器接收数据的替代方法是显式地重新划分输入数据流(使用inputStream.repartition())。在进一步处理之前,它将接收到的数据重新划分分布到集群中指定数量的机器上。
对于 direct stream,请参阅Spark Streaming+ Kafka集成指南
如果在计算的任何阶段中使用的并行任务的数量都不够高,则集群资源可能没有得到充分利用。例如,对于诸如reduceByKey和reduceByKeyAndWindow之类的分布式归约操作,并行任务的默认数量由spark.default.parallelism配置属性控制。您可以将并行度级别作为参数传递(请参阅PairDStreamFunctions文档),或者设置spark.default.parallelism配置属性来更改默认值。
通过调整序列化格式,可以减少数据序列化的开销。对于流,有两种类型的数据正在被序列化。
在这两种情况下,使用Kryo序列化可以减少CPU和内存开销。有关更多细节,请参阅Spark调优指南
。对于Kryo,可以考虑注册自定义类,并禁用对象引用跟踪(请参阅配置指南中与Kryo相关的配置)。
在需要为流应用程序保留的数据量不大的特定情况下,可以将数据(两种类型)作为反序列化对象持久存储(数据存储不序列化),而不会导致过多的GC开销。例如,如果您正在使用几秒钟的批处理间隔,并且没有窗口操作,那么您可以通过显式地相应地设置存储级别来尝试禁用持久数据中的序列化。这将减少由于序列化而导致的CPU开销,从而在没有太多GC开销的情况下提高性能。
如果每秒启动的任务数量很高(比如每秒50个或更多),那么向从属服务器发送任务的开销可能会很大,并且很难实现次秒延迟。可以通过以下改变来减少开销:
这些更改可能会将批处理时间减少100毫秒,从而使次秒级的批大小成为可能。
要使运行在集群上的Spark Streaming应用程序稳定,系统应该能够处理接收到的数据。换句话说,处理批量数据的速度应该与生成数据的速度一样快。通过监视流web UI中的处理时间可以发现应用程序是否如此,其中批处理时间应该小于批处理间隔。
根据流计算的性质,所使用的批处理间隔可能对应用程序在一组固定的集群资源上能够维持的数据速率产生重大影响。例如,让我们考虑前面的WordCountNetwork示例。对于特定的数据速率,系统可以每2秒报告一次字数计数(即,批处理间隔为2秒),但不是每500毫秒一次。因此需要设置批处理间隔,以便能够维持生产中的预期数据速率。
为应用程序确定正确的批大小的一个好方法是使用保守的批处理间隔(例如,5-10秒)和较低的数据速率对其进行测试。要验证系统是否能够跟上数据速率,可以检查每个处理批所经历的端到端延迟的值(在Spark驱动程序log4j日志中查找“总延迟”,或者使用StreamingListener接口)。如果延迟保持与批处理大小相当,则系统是稳定的。否则,如果延迟持续增加,则意味着系统无法跟上,因此不稳定。一旦有了稳定配置的想法,就可以尝试增加数据速率和/或减少批处理大小。请注意,由于临时数据速率的增加而导致的暂时延迟的增加可能是正确的,只要延迟减少到一个较低的值(即,小于批处理大小)。
调优Spark应用程序的内存使用和GC行为在调优指南中有详细的讨论。强烈建议你读一读。在本节中,我们将讨论一些特定于Spark Streaming应用程序上下文的调优参数。
Spark STreaming应用程序所需的集群内存量在很大程度上取决于所使用的transformations类型。例如,如果您想对前10分钟的数据使用窗口操作,那么您的集群应该有足够的内存来在内存中保存10分钟的数据。或者,如果您希望使用带有大量键的updateStateByKey,那么所需的内存将会很大。相反,如果您想要执行简单的map-filter-store操作,那么所需的内存将会很低。
一般来说,由于通过接收器接收到的数据是用StorageLevel.MEMORY_AND_DISK_SER_2,不能够用内存存储的数据将溢出到磁盘。这可能会降低流应用程序的性能,因此建议根据流应用程序的需要提供足够的内存。最好尝试在小范围内查看内存使用情况并进行相应的估计。
内存调优的另一个方面是垃圾收集。对于需要低延迟的流应用程序,JVM垃圾收集导致的大量暂停是不可取的。
有几个参数可以帮助你调优内存使用和GC开销:
DStreams的持久性级别: 正如前面在数据序列化一节中提到的,输入数据和RDDs在默认情况下是作为序列化字节持久化的。与反序列化持久性相比(直接保存在内存),这减少了内存使用和GC开销。启用Kryo序列化进一步减少了序列化的大小和内存使用。通过压缩(参见Spark配置Spark .rdd.compress)可以进一步减少内存使用量,但要以CPU时间为代价。
清除旧数据: 默认情况下,由DStream转换生成的所有输入数据和持久的RDDs将被自动清除。Spark Streaming根据使用的转换决定何时清除数据。例如,如果您使用的是10分钟的窗口操作,那么Spark流将保留最后10分钟的数据,并主动丢弃旧的数据。通过设置streamingContext.remember,可以更长时间地保留数据(例如交互式地查询旧数据)。
CMS垃圾收集器: 强烈建议使用并发标记-清除GC,以保持与GC相关的暂停始终较低。尽管众所周知并发GC会降低系统的总体处理吞吐量,但仍然建议使用它来实现更一致的批处理时间。确保在驱动程序(在Spark-submit中使用–driver-java-options)和执行器(使用Spark配置Spark.executor.extrajavaoptions)上都设置了CMS GC。
其它建议: 为了进一步减少GC开销,这里还有一些技巧可以尝试。
要记住的要点:
DStream与单个接收器相关联。为了获得读并行性,需要创建多个接收器,即多个DStreams。接收器在执行器中运行。它占据一个核。确保在接收槽被预定后有足够的内核用于处理数据,spark.cores.max参数应该包含接收槽。接收者以循环方式分配给执行者。
当从流源接收数据时,接收器创建数据块。每隔一毫秒就会产生一个新的数据块。在batchInterval期间创建N个数据块,其中N = batchInterval/blockInterval。这些块由当前执行程序的块管理器分发给其他执行程序的块管理器。之后,运行在驱动程序上的网络输入跟踪器将被告知进一步处理的块位置。
在驱动程序上为在batchInterval期间创建的块,创建了RDD。batchInterval期间生成的块是RDD的分区。每个分区都是spark中的一个任务。blockInterval== batchinterval将意味着创建一个单独的分区,并且可能在本地处理它。
块上的映射任务在执行器(一个接收块,另一个复制块)中处理,不管块的间隔如何,除非出现非本地调度。拥有更大的blockinterval意味着更大的块。spark.locality.wait大的值增加了在本地节点上处理一个块的机会。需要在这两个参数之间找到平衡,以确保在本地处理较大的块。
您可以通过调用inputDstream.repartition(n)来定义分区的数量,而不是依赖于batchInterval和blockInterval。这将随机重组RDD中的数据,以创建n个分区。是的,为了更好的并行性。不过代价是shuffle。RDD的处理由驱动程序的jobscheduler作为job调度。
如果你有两个dstreams,就会形成两个RDDs,就会创建两个作业,它们会一个接一个地安排。为了避免这种情况,可以union两个dstreams。这将确保dstreams的两个rdd形成一个unionRDD。然后,这个unionRDD被视为单个作业。但是,RDDs的分区不受影响。
如果批处理时间大于batchinterval,那么显然接收方的内存将开始填满,并最终抛出异常(很可能是BlockNotFoundException)。目前,没有办法暂停接收器。使用SparkConf配置spark.stream.receiver.maxRate,限制接收速率。