【发布时间】:2017-03-17 18:34:10
【问题描述】:
我需要另一种眼光。
我已经用这个确切的代码写出了一个数百 GB 的 zip 文件,在 MacOSX 上本地没有任何修改。
使用 100% 未更改的代码,刚刚部署到运行 Ubuntu 的 AWS 实例,同样的代码会遇到内存不足问题(堆空间)。
这是正在运行的代码,将 MyBatis 流式传输到磁盘上的 CSV 文件:
File directory = new File(feedDirectory);
File file;
try {
file = File.createTempFile(("feed-" + providerCode + "-"), ".csv", directory);
} catch (IOException e) {
throw new RuntimeException("Unable to create file to write feed to disk: " + e.getMessage(), e);
}
String filePath = file.getAbsolutePath();
log.info(String.format("File name for %s feed is %s", providerCode, filePath));
// output file
try (FileOutputStream out = new FileOutputStream(file)) {
streamData(out, providerCode, startDate, endDate);
} catch (IOException e) {
throw new RuntimeException("Unable to write feed to file: " + e.getMessage());
}
public void streamData(OutputStream outputStream, String providerCode, Date startDate, Date endDate) throws IOException {
try (CSVPrinter printer = CsvUtil.openPrinter(outputStream)) {
StreamingHandler<FStay> handler = stayPrintingHandler(printer);
warehouse.doForAllStaysByProvider(providerCode, startDate, endDate, handler);
}
}
private StreamingHandler<FStay> stayPrintingHandler(CSVPrinter printer) {
StreamingHandler<FStay> handler = new StreamingHandler<>();
handler.setHandler((stay) -> {
try {
EXPORTER.writeStay(printer, stay);
} catch (IOException e) {
log.error("Issue with writing output: " + e.getMessage(), e);
}
});
return handler;
}
// The EXPORTER method
import org.apache.commons.csv.CSVPrinter;
public void writeStay(CSVPrinter printer, FStay stay) throws IOException {
List<Object> list = asList(stay);
printer.printRecord(list);
}
List<Object> asList(FStay stay) {
List<Object> list = new ArrayList<>(46);
list.add(stay.getUid());
list.add(stay.getProviderCode());
//....
return list;
}
这是我在本地运行时的 JVM 堆空间图(使用 jvisualvm)。我已经在本地与 Java 8(jdk1.8.0_51 和 1.8.0_112)一致地运行它,并取得了很好的结果。甚至写出 TB 的数据。
^ 在上面,最大堆空间设置为 4 gigs,最多增加到 1.5 gigs,然后降到大约 500 MB,同时按预期将数据流式传输到 CSV 文件。
但是,当我在带有 jdk 1.8.0_111 的 Ubuntu 上运行它时,完全相同的操作将无法完成,堆空间不足(java.lang.OutOfMemoryError: Java heap space)
我已将 Xmx 值从 8 gigs 提高到 16 到 25 gigs,但堆空间仍然不足。同时......文件的总大小总共只有 10 Gigs......这真的让我感到困惑。
这是 JVisualVm 图在 Ubuntu 盒子上的样子:
我毫不怀疑它在两种环境中运行的代码完全相同,并且在每个环境中执行相同的操作(相同的数据库服务器提供相同的数据)
此时我能想到的唯一区别是:
- 操作系统 - Ubuntu 与 Mac OS X
- AWS 中的托管 VM 与硬金属笔记本电脑
- AWS 中数据库和 Ubuntu 服务器之间的网络速度更快
- 在Ubuntu下JDK版本是1.8.0_111,本地试过1.8.0_51和1.8.0_112
谁能帮助解决这个问题?
更新
我尝试用显式刷新/关闭语句替换所有“try-with-resources”语句,但没有成功。
更重要的是,当我开始看到数据进入时,我试图在 Ubuntu 机器上强制进行垃圾收集,但它没有效果——肯定有什么东西阻止了在 Ubuntu 机器上收集堆...在 OS X 上运行完全相同的代码时,让我再次编写完整的墨西哥卷饼没问题。
更新 2
除了上述环境的差异之外,我能想到的唯一其他差异是,AWS 中服务器之间的连接速度是否如此之快,以至于它流式传输数据的速度比将数据刷新到磁盘的速度要快……但这仍然不能解释我总共只有 10 Gig 数据的问题,并且它会炸毁具有 20 Gig 堆空间的 JVM。
是否有可能在 Ubuntu/Java 级别存在错误?
更新 3
尝试替换 CSVPrinter 的输出以使用完全独立的库(OpenCSV 的 CSVWriter 代替 Apache 的 CSV 库),结果相同。
只要这段代码开始从数据库接收数据,堆就会开始爆炸,垃圾收集器无法回收任何内存......但仅限于 Ubuntu。在 OS X 上,所有内容都会立即回收,并且堆永远不会增长。
我也尝试在每次写入后刷新流,但也没有运气。
更新 4
得到堆转储打印出来,据此我应该查看数据库驱动程序。特别是亚马逊红移驱动程序中的 InboundDataHandler。
我正在使用带有自定义结果处理程序的 myBatis。我尝试将结果处理程序设置为在它得到结果时有效地不做任何事情(新的 ResultHandler() { // 方法被覆盖,实际上什么也不做})并且我知道我没有保留那里的任何引用。
由于是AWS/Redshift定义的InboundDataHandler……这让我觉得它可能低于myBatis级别……要么:
- 我正在设置的 SqlSessionFactory 出错
- 仅在 Ubuntu / AWS 中弹出的 Redshift 驱动程序中的错误
- 我已覆盖的结果处理程序中的错误
这里是我设置 SqlSessionFactoryBean 的地方:
@Bean
public javax.sql.DataSource redshiftDataSource() throws ClassNotFoundException {
log.info("Got to datasource config");
// Dynamically load driver at runtime.
Class.forName(dataWarehouseDriver);
DataSource dataSource = new DataSource();
dataSource.setURL(dataWarehouseUrl);
dataSource.setUserID(dataWarehouseUsername);
dataSource.setPassword(dataWarehousePassword);
return dataSource;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory() throws ClassNotFoundException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(redshiftDataSource());
return factoryBean;
}
这是我作为测试运行的 myBatis 代码,以验证不是我持有我的 ResultHandler 中的记录:
warehouse.doForAllStaysByProvider(providerCode, startDate, endDate, new ResultHandler<FStay>() {
@Override
public void handleResult(ResultContext<? extends FStay> resultContext) {
// do nothing
}
});
有没有办法可以强制 SQL 连接不挂在记录或其他东西上?我将在我的本地机器上再次重申,这种内存泄漏没有问题......它仅在托管 AWS 环境中运行代码时才会出现。在这两种情况下,数据库驱动程序和服务器都是相同的。
更新 6 我想它终于解决了。感谢所有向我指出堆转储方向的人。这极大地帮助了将其缩小到有问题的班级。
在那之后,我对 AWS redshift 驱动程序进行了一些研究,它明确表示您的客户应该为对大数据的任何操作指定一个限制。所以我在我的 myBatis 配置中发现了如何做到这一点:
<select id="doForAllStaysByProvider" fetchSize="1000" resultMap="FStayResultMap">
select distinct
f_stay.uid,
这就成功了。
请注意,即使在处理从 AWS 远程下载的更大数据集(AWS 中的数据库,在家笔记本电脑上执行的代码)时,这也不是必需的,因为我正在覆盖 myBatis ResultHandler 单独处理每一行,从不保留任何对象。
然而,AWS redshift jdbc 驱动程序只有在 AWS 中运行时才会发生一些奇怪的事情(AWS 中的数据库,AWS 实例中执行的代码),这会导致此 InboundDataHandler 永远不会释放其资源,除非指定了 fetchSize。
这是正在运行的服务器堆,比以前在 AWS 中运行得更远,堆空间从未超过 500Mb,在我在 jvisualvm 中点击“强制 gc”后,它显示“已使用”堆小于 100mb:
再次非常感谢所有帮助指导这一点的人!
【问题讨论】:
-
尝试显式关闭您的输出流
-
您能告诉我们数据实际上是如何写入文件的吗?在我的脑海中,Linux 和 MacOS JVM 可能会有一些细微的实现差异。相同的 Java 代码,可能不同的字节码。
-
你能找到 什么 正在填满你的堆吗? Visualvm 可能会告诉你。您也可以启用
-XX:HeapDumpOnOutOfMemoryError。 -
谢谢,我将
-XX:+HeapDumpOnOutOfMemoryError添加到我的tomcat java_opts(需要+ 号)。我手边没有截图,但是所有的内存分配基本上都在char []数组中... -
您应该考虑至少将最终更新提取到答案 Cuga 中。
标签: java amazon-web-services memory memory-leaks heap-memory