Spark面对OOM问题的解决方法及优化总结

转载请保持完整性并注明来源链接:  http://blog.csdn.net/yhb315279058/article/details/51035631
    Spark中的OOM问题不外乎以下两种情况
  • map执行中内存溢出
  • shuffle后内存溢出
    map执行中内存溢出代表了所有map类型的操作,包括:flatMap,filter,mapPatitions等。shuffle后内存溢出的shuffle操作包括join,reduceByKey,repartition等操作。后面先总结一下我对Spark内存模型的理解,再总结各种OOM的情况相对应的解决办法和性能优化方面的总结。如果理解有错,希望在评论中指出。

Spark 内存模型:
    Spark在一个Executor中的内存分为三块,一块是execution内存,一块是storage内存,一块是other内存。
  • execution内存是执行内存,文档中说join,aggregate都在这部分内存中执行,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘,能够减少IO。其实map过程也是在这个内存中执行的。
  • storage内存是存储broadcast,cache,persist数据的地方。
  • other内存是程序执行时预留给自己的内存。
    execution和storage是Spark Executor中内存的大户,other占用内存相对少很多,这里就不说了。在spark-1.6.0以前的版本,execution和storage的内存分配是固定的,使用的参数配置分别是spark.shuffle.memoryFraction(execution内存占Executor总内存大小,default 0.2)和spark.storage.memoryFraction(storage内存占Executor内存大小,default 0.6),因为是1.6.0以前这两块内存是互相隔离的,这就导致了Executor的内存利用率不高,而且需要根据Application的具体情况,使用者自己来调节这两个参数才能优化Spark的内存使用。在spark-1.6.0以上的版本,execution内存和storage内存可以相互借用,提高了内存的Spark中内存的使用率,同时也减少了OOM的情况。
    在Spark-1.6.0后加入了堆外内存,进一步优化了Spark的内存使用,堆外内存使用JVM堆以外的内存,不会被gc回收,可以减少频繁的full gc,所以在Spark程序中,会长时间逗留再Spark程序中的大内存对象可以使用堆外内存存储。使用堆外内存有两种方式,一种是在rdd调用persist的时候传入参数StorageLevel.OFF_HEAP,这种使用方式需要配合Tachyon一起使用。另外一种是使用Spark自带的spark.memory.offHeap.enabled 配置为true进行使用,但是这种方式在1.6.0的版本还不支持使用,只是多了这个参数,在以后的版本中会开放。
    OOM的问题通常出现在execution这块内存中,因为storage这块内存在存放数据满了之后,会直接丢弃内存中旧的数据,对性能有影响但是不会有OOM的问题。

内存溢出解决方法:
1. map过程产生大量对象导致内存溢出:
    这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。
    面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有shuffle的过程。

2.数据不平衡导致内存溢出:
    数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。这里就不再累赘了。

3.coalesce调用导致内存溢出:
    这是我最近才遇到的一个问题,因为hdfs中不适合存小问题,所以Spark计算后如果产生的文件太小,我们会调用coalesce合并文件再存入hdfs中。但是这会导致一个问题,例如在coalesce之前有100个文件,这也意味着能够有100个Task,现在调用coalesce(10),最后只产生10个文件,因为coalesce并不是shuffle操作,这意味着coalesce并不是按照我原本想的那样先执行100个Task,再将Task的执行结果合并成10个,而是从头到位只有10个Task在执行,原本100个文件是分开执行的,现在每个Task同时一次读取10个文件,使用的内存是原来的10倍,这导致了OOM。解决这个问题的方法是令程序按照我们想的先执行100个Task再将结果合并成10个文件,这个问题同样可以通过repartition解决,调用repartition(10),因为这就有一个shuffle的过程,shuffle前后是两个Stage,一个100个分区,一个是10个分区,就能按照我们的想法执行。

4.shuffle后内存溢出:
    shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

5. standalone模式下资源分配不均匀导致内存溢出:
    在standalone的模式下如果配置了--total-executor-cores 和 --executor-memory 这两个参数,但是没有配置--executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置--executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。

6.在RDD中,共用对象能够减少OOM的情况:
    这个比较特殊,这里说记录一下,遇到过一种情况,类似这样rdd.flatMap(x=>for(i <- 1 to 1000) yield ("key","value"))导致OOM,但是在同样的情况下,使用rdd.flatMap(x=>for(i <- 1 to 1000) yield "key"+"value")就不会有OOM的问题,这是因为每次("key","value")都产生一个Tuple对象,而"key"+"value",不管多少个,都只有一个对象,指向常量池。具体测试如下:

    这个例子说明("key","value")和("key","value")在内存中是存在不同位置的,也就是存了两份,但是"key"+"value"虽然出现了两次,但是只存了一份,在同一个地址,这用到了JVM常量池的知识.于是乎,如果RDD中有大量的重复数据,或者Array中需要存大量重复数据的时候我们都可以将重复数据转化为String,能够有效的减少内存使用.

优化:
    这一部分主要记录一下到spark-1.6.1版本,笔者觉得有优化性能作用的一些参数配置和一些代码优化技巧,在参数优化部分,如果笔者觉得默认值是最优的了,这里就不再记录。
代码优化技巧:
1.使用mapPartitions代替大部分map操作,或者连续使用的map操作:
    这里需要稍微讲一下RDD和DataFrame的区别。RDD强调的是不可变对象,每个RDD都是不可变的,当调用RDD的map类型操作的时候,都是产生一个新的对象,这就导致了一个问题,如果对一个RDD调用大量的map类型操作的话,每个map操作会产生一个到多个RDD对象,这虽然不一定会导致内存溢出,但是会产生大量的中间数据,增加了gc操作。另外RDD在调用action操作的时候,会出发Stage的划分,但是在每个Stage内部可优化的部分是不会进行优化的,例如rdd.map(_+1).map(_+1),这个操作在数值型RDD中是等价于rdd.map(_+2)的,但是RDD内部不会对这个过程进行优化。DataFrame则不同,DataFrame由于有类型信息所以是可变的,并且在可以使用sql的程序中,都有除了解释器外,都会有一个sql优化器,DataFrame也不例外,有一个优化器 Catalyst,具体介绍看后面 参考的文章。
    上面说到的这些RDD的弊端,有一部分就可以使用mapPartitions进行优化,mapPartitions可以同时替代rdd.map,rdd.filter,rdd.flatMap的作用,所以在长操作中,可以在mapPartitons中将RDD大量的操作写在一起,避免产生大量的中间rdd对象,另外是mapPartitions在一个partition中可以复用可变类型,这也能够避免频繁的创建新对象。使用mapPartitions的弊端就是牺牲了代码的易读性。

2.broadcast join和普通join:
    在大数据分布式系统中,大量数据的移动对性能的影响也是巨大的。基于这个思想,在两个RDD进行join操作的时候,如果其中一个RDD相对小很多,可以将小的RDD进行collect操作然后设置为broadcast变量,这样做之后,另一个RDD就可以使用map操作进行join,这样能够有效的减少相对大很多的那个RDD的数据移动。

3.先filter在join:
    这个就是谓词下推,这个很显然,filter之后再join,shuffle的数据量会减少,这里提一点是spark-sql的优化器已经对这部分有优化了,不需要用户显示的操作,个人实现rdd的计算的时候需要注意这个。

4.partitonBy优化:
    这一部分在另一篇文章 《spark partitioner使用技巧 》有详细介绍,这里不说了。

5. combineByKey的使用:
    这个操作在Map-Reduce中也有,这里举个例子:rdd.groupByKey().mapValue(_.sum)比rdd.reduceByKey的效率低,原因如下两幅图所示(网上盗来的,侵删)



    上下两幅图的区别就是上面那幅有combineByKey的过程减少了shuffle的数据量,下面的没有。combineByKey是key-value型rdd自带的API,可以直接使用。

6. 在内存不足的使用,使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache():
    rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重算,只是消耗点IO时间。

7.在spark使用hbase的时候,spark和hbase搭建在同一个集群:
     在spark结合hbase的使用中,spark和hbase最好搭建在同一个集群上上,或者spark的集群节点能够覆盖hbase的所有节点。hbase中的数据存储在HFile中,通常单个HFile都会比较大,另外Spark在读取Hbase的数据的时候,不是按照一个HFile对应一个RDD的分区,而是一个region对应一个RDD分区。所以在Spark读取Hbase的数据时,通常单个RDD都会比较大,如果不是搭建在同一个集群,数据移动会耗费很多的时间。

参数优化部分:
8. spark.driver.memory (default 1g):
    这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。

9. spark.rdd.compress (default false) :
    这个参数在内存吃紧的时候,又需要persist数据有良好的性能,就可以设置这个参数为true,这样在使用persist(StorageLevel.MEMORY_ONLY_SER)的时候,就能够压缩内存中的rdd数据。减少内存消耗,就是在使用的时候会占用CPU的解压时间。

10. spark.serializer (default org.apache.spark.serializer.JavaSerializer )
    建议设置为 org.apache.spark.serializer.KryoSerializer,因为KryoSerializer比JavaSerializer快,但是有可能会有些Object会序列化失败,这个时候就需要显示的对序列化失败的类进行KryoSerializer的注册,这个时候要配置spark.kryo.registrator参数或者使用参照如下代码:
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1]
, classOf[MyClass2]))
val sc = new SparkContext(conf)

11. spark.memory.storageFraction (default 0.5)
    这个参数设置内存表示 Executor内存中 storage/(storage+execution),虽然spark-1.6.0+的版本内存storage和execution的内存已经是可以互相借用的了,但是借用和赎回也是需要消耗性能的,所以如果明知道程序中storage是多是少就可以调节一下这个参数。

12.spark.locality.wait (default 3s):
    spark中有4中本地化执行level,PROCESS_LOCAL->NODE_LOCAL->RACK_LOCAL->ANY,一个task执行完,等待spark.locality.wait时间如果,第一次等待PROCESS的Task到达,如果没有,等待任务的等级下调到NODE再等待spark.locality.wait时间,依次类推,直到ANY。分布式系统是否能够很好的执行本地文件对性能的影响也是很大的。如果RDD的每个分区数据比较多,每个分区处理时间过长,就应该把 spark.locality.wait 适当调大一点,让Task能够有更多的时间等待本地数据。特别是在使用persist或者cache后,这两个操作过后,在本地机器调用内存中保存的数据效率会很高,但是如果需要跨机器传输内存中的数据,效率就会很低。

13. spark.speculation (default false):
    一个大的集群中,每个节点的性能会有差异,spark.speculation这个参数表示空闲的资源节点会不会尝试执行还在运行,并且运行时间过长的Task,避免单个节点运行速度过慢导致整个任务卡在一个节点上。这个参数最好设置为true。与之相配合可以一起设置的参数有spark.speculation.×开头的参数。 参考中有文章详细说明这个参数。

以后有遇到新的内容再补充。

参考:

转载请保持完整性并注明来源链接:  http://blog.csdn.net/yhb315279058/article/details/51035631

<think>嗯,用户现在问的是如何优化导出接口处理39万条数据的性能。首先,我得考虑用户的实际场景。可能他们有一个系统,用户需要导出大量数据,比如报表或者日志,但当前接口在处理这么大数据量时速度太慢,甚至超时或者崩溃。用户可能是开发者或者运维人员,需要解决这个问题以提高用户体验和系统稳定性。 首先,我需要回顾之前的问题,用户之前问过关于EasyExcel的转换器,可能他们已经在用Java和EasyExcel处理Excel导出。现在面对大数据量导出,性能问题变得突出。用户可能已经尝试过一些基本优化,但效果不明显,所以需要更深入的解决方案。 接下来,我需要分析导出接口的瓶颈在哪里。通常,大数据导出的瓶颈可能在数据库查询、内存使用、数据处理逻辑、网络传输等方面。对于39万条数据,如果一次性查询出来,内存可能会爆掉,导致GC频繁甚至OOM错误。另外,数据库查询如果没优化,比如没有合适的索引,查询速度会很慢。数据处理方面,比如转换、格式化等操作如果处理不当,也会消耗大量时间。还有,网络传输大文件可能导致超时,或者客户端处理不过来。 然后,我应该分步骤来思考优化方案。首先,分页查询和流式处理是关键,避免一次性加载所有数据到内存。使用EasyExcel的流式导出功能,分页读取数据,边读边写,减少内存占用。然后,数据库优化,比如索引、只查询必要字段、避免复杂JOIN,甚至使用游标或者主键分页。异步导出和文件拆分也是不错的策略,用户触发导出后,后台生成文件,提供下载链接,避免接口长时间阻塞。还可以拆分成多个小文件,并行处理,提升速度。 在代码层面,要优化数据处理逻辑,避免在循环中进行复杂操作,预编译模板,使用缓存。网络方面,压缩文件,使用CDN加速下载。JVM调优,增加内存,调整GC策略,避免频繁Full GC。其他优化包括关闭Swagger等调试工具,调整连接池,使用更高效的数据格式如CSV。 还需要考虑用户可能的后续问题,比如如何具体实现异步导出,或者如何分片查询数据,或者如何监控导出任务的状态。用户可能还关心如何保证数据一致性,特别是在分页查询时数据是否有变化,是否需要加锁或者使用快照。 最后,要确保给出的建议具体可行,比如给出分页查询的示例代码,说明如何配置EasyExcel的流式导出,或者如何集成消息队列进行异步处理。同时提醒用户注意潜在问题,比如内存泄漏、分页查询的效率、网络带宽的限制等。</think>以下是针对导出39万条数据接口的性能优化方案,分步骤说明关键优化策略: --- ### 一、分页查询 + 流式导出(核心优化) #### 1. 避免全量数据加载 ```java // 伪代码示例:分页查询 + EasyExcel流式写入 int pageSize = 5000; // 每页大小 long total = getTotalCount(); // 总数据量39万 try (ExcelWriter excelWriter = EasyExcel.write(outputStream).build()) { for (int page = 1; page <= (total + pageSize - 1)/pageSize; page++) { List<DataDTO> chunk = queryByPage(page, pageSize); // 分页查询 WriteSheet writeSheet = new WriteSheet(); excelWriter.write(chunk, writeSheet); chunk.clear(); // 及时释放内存 } } ``` #### 2. 数据库优化 - **索引优化**:确保查询字段有覆盖索引 - **字段精简**:`SELECT *` → 改为明确需要的字段 - **游标查询**:使用`JDBC fetchSize`实现流式读取 ```java @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager mgr = new DataSourceTransactionManager(dataSource); mgr.setDataSource(dataSource); mgr.setDefaultTimeout(30); // 设置事务超时 return mgr; } ``` --- ### 三、异步导出 + 文件拆分(防超时) #### 1. 异步处理架构 ```mermaid graph TB A[用户点击导出] --> B[生成任务ID并返回] B --> C[消息队列推送任务] C --> D[后台Worker处理导出] D --> E[存储文件到OSS/S3] E --> F[邮件/站内信通知下载] ``` #### 2. 文件拆分策略 - 按时间范围分割:`export_2023Q1.csv`, `export_2023Q2.csv` - 按数据特征分割:`region=shanghai.csv`, `region=beijing.csv` --- ### 四、内存与数据处理优化 #### 1. 对象复用策略 ```java // 重用DTO对象减少GC private static final ThreadLocal<DataDTO> reuseDTO = ThreadLocal.withInitial(DataDTO::new); void processRow(ResultSet rs) { DataDTO dto = reuseDTO.get(); dto.setId(rs.getString("id")); // 复用对象填充数据... } ``` #### 2. 数据格式选择 | 格式 | 压缩率 | 解析速度 | 适用场景 | |------------|--------|----------|----------------------| | Excel | 低 | 慢 | 必须用Excel的场景 | | CSV | 中 | 快 | 大数据量首选 | | Parquet | 高 | 中 | 数据分析场景 | --- ### 五、网络传输优化 1. **压缩传输**:启用GZIP压缩 ```java response.setHeader("Content-Encoding", "gzip"); ``` 2. **断点续传**:支持`Range`头实现分片下载 3. **CDN加速**:将生成的文件推送到CDN节点 --- ### 六、极限优化方案(适用于千万级) 1. **直接数据库导出**:使用`mysqldump`或`COPY`命令直出CSV 2. **Spark并行导出**:分布式集群处理 ```scala spark.read.jdbc(...) .repartition(100) .write.csv("hdfs://output/") ``` 3. **列式存储**:使用ORC/Parquet格式提升压缩率 --- ### 七、监控指标建议 | 指标 | 预警阈值 | 监控工具 | |---------------------|---------------|------------------| | 导出耗时 | >300s | Prometheus | | JVM堆内存 | >80% | Grafana | | 数据库CPU | >70% | Datadog | | 网络出流量 | >100MB/s | Zabbix | 是否需要针对某个具体优化点(如分页查询实现细节)展开说明?
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值