2021SC@SDUSC
前言
前几篇博客分析了关于Spark Streaming的运行流程部分,由于运行流程内容过多,在此暂时分析到这里,接下来分析一下Spark Streaming的性能调优机制。
Spark Streaming性能调优机制
Spark Streaming用于对大量数据的接收和处理,提高Spark集群的性能以应对更大的业务处理需要十分重要。最好的状态是数据接收的速度也能匹配,且Spark集群的硬件资源也能被充分应用,这就涉及性能能调优
一、并行度解析
在Spark集群资源允许的前提下,可以提高数据接收、数据处理的并行度。
1.1 数据接收的并行度
数据接收的并行度调优有多个方面
1.1.1 InputDStream的并行度
Spark Streaming应用程序中涉及数据接收的第一个DStream是InputDStream。
下面对Receiver方式进行讨论。每个InputDStream都会在某个Worker节点上创建一个Receiver。其实在写应用程序时,可以创建多个InputDStream来接收同一数据源的数据。还可以通过配置,让这些DStream分别接收数据源的不同分区的数据,最大DStream个数可以达到数据源提供的分区数。例如,一个接收两个Kafka Topic数据的输入DStream可以被拆分成两个接收不同Topic数据的DStream。
最后,可以在程序中把多个InputDStream再合并为一个DStream,进行后续处理。下面给出基于Kafka的Java代码:
// 多个InputDStream合并为一个DStream的Java代码
int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<JavaPairDStream<String, String>>(numStreams);
for (int i = 0; i < numStreams; i++) {
kafkaStream.add(KafkaUtils.createStream(...));
}
JavaPairDStream<String,String> unifiedStrem = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
1.1.2 Task的并行度
数据接收使用的BlockGenerator里面有个RecurringTimer类型的对象blockIntervalTimer,会周期性的发送BlockGenerator消息,进而周期性的生成和存储一个Block。这个周期有一个配置参数spark.streaming.blockInterval。这个时间周期的默认值是200ms。
读写Block会用到BlockManager。在小组讨论过程中,我从与小组成员李子旭讨论中了解到BlockManager是定义于Spark Core中的,而且是Storage模块与其他模块交互最主要的类(可见此篇文章了解Spark Core大致内容:山东大学软件工程应用与实践——Spark项目(二)),提供了读和写Block的接口。这里的Block,实际上就对应了RDD中提到的Partition,每一个Partition都会对应一个Block。而Spark Streaming按Batch Interval来组织一次数据接收和处理,所以Batch Interval内的Block个数就是RDD的Partition数,也就是RDD的并行Task数。
因此,Task的并行度大致等于Batch Interval / Block Interval。比如,Batch Interval 是2s,Block Interval是200ms,则Task并行度为10.
通过调小Block Interval,可以提高Task并行度。但一般最好不要让Block Interval低于50ms。
1.1.3 数据处理前的重分区
多输入流或者多Receiver的一个可选方法是,明确的重新分配输入数据流,即在进一步处理数据之前,利用DStream.repartition(<分区数>)进行重新分区,把接收的数据分发到集群上。
1.2 数据处理的并行度
如果运行在计算stage上的并发任务数不是足够大,就不会充分利用集群的资源。例如,对于分布式reduce操作,如reduceByKey和reduceByKeyAndWindow,默认的并发任务数通过配置属性spark.default.parallelism来确定。可以通过参数PairDStreamFunctions传递并行度,或者设置参数spark.default.parallelism修改默认值。
二、内存
Spark Streaming应用需要的集群内存资源是由使用的转换操作类型决定的。举例来说,如果想要使用一个窗口长度为10分钟的窗口操作,那么集群就必须有足够的内存来保存10分钟内的数据。如果想要使用updateStateByKey来维护许多key的state,那么内存资源就必须足够大。反过来说,如果想要做一个简单的map-filter-store操作,那么需要使用的内存就很少。
据资料了解:
通常来说,通过Receiver接收到的数据会使用StorageLevel.MEMORY_AND_DISK_SER_2持久化级别来进行存储,
因此无法保存在内存中的数据会溢写到磁盘上,而溢写到磁盘上会降低应用的性能。
因此,通常是建议为应用提供它需要的足够的内存资源。建议在一个小规模的场景下测试内存的使用量,并进行评估。
三、序列化
Spark Streaming默认将接收到的数据序列化存储,以减少内存使用。序列化和反序列化需要更多的CPU时间,更加高效的序列化方式(Kryo)和自定义的序列化接口可以更高效的使用CPU。使用Kryo时,一定要考虑注册自定义的类,并且禁用对应引用的tracking(spark.kryo.referenceTracking)。
在流式计算的场景下,有两种类型的数据需要序列化。
- 输入数据。
默认情况下,收到的输入数据是存储在Executor的内存中的,使用的持久化级别是StorageLevel.MEMORY_AND_DISK_SER_2。这意味着数据被序列化为字节,从而减小GC开销,并且会复制以进行Executor容错。因此,数据首先会存储在内存中,然后在内存不足时,会溢写到磁盘上,从而为流式计算来保存所有需要的数据,然后再使用Spark的序列化格式序列化数据。 - 流式计算操作生成的持久化RDD。流式计算操作生成的持久化RDD可能会持久化到内存中。但不像Spark Core(此处再次与组内成员李子旭交流后得知)的默认持久化级别StorageLevel.MEMORY_ONLY,DStream的默认持久化级别是MEMORY_ONLY_SER,默认就会减小GC开销。
在一些特殊场景中,或许可以将数据以非序列化的方式进行持久化,从而减少序列化和反序列化的CPU开销,而且又不会有太昂贵的GC开销。
其他
对于Spark Streaming性能调优机制还有诸如Batch Interval 、Task 、 JVM GC等机制,由于时间有限,在此不一一剖析。