【问题标题】:Java code runs out of space memory on AWS but not MacOSXJava 代码在 AWS 但不是 MacOSX 上耗尽了空间内存
【发布时间】: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


【解决方案1】:

终于找到解决办法了。

堆转储是最大的帮助——它表明亚马逊的 RedShift/postgres JDCB 驱动程序的 InboundDataHandler 类是罪魁祸首。

设置 SqlSession 的代码看起来是合法的,所以前往 Amazon's documentation 获得了这个 gem:

在检索大数据时避免客户端内存不足错误 使用 JDBC 设置,您可以使您的客户端批量获取数据 通过设置 JDBC fetch size 参数。

我们之前没有遇到过这种情况,因为我们在 MyBatis 中使用自定义 ResultHandlers 流式传输结果......但是当 AWS Redshift JDBC 驱动程序在 AWS 本身上运行与外部 AWS 连接时似乎有些不同。

根据文档的指导,我们在 MyBatis 选择查询中添加了一个“fetchSize”:

<select id="doForAllStaysByProvider" fetchSize="1000" resultMap="FStayResultMap">        
select distinct
    f_stay.uid,

瞧!一切都很顺利。这是我们所做的唯一更改,并且堆从未超过几百 MB。

您可以在上面的一个图表中看到堆超出图表的位置,一旦开始在 Amazon 上接收数据,堆就会线性上升,并且一旦开始就不会回收一盎司的堆空间。

我的猜测是 Redshift JDBC 驱动程序在 Amazon 环境中进行某种优化时正在做一些不同的事情......这就是我能想到的解释这种行为的全部内容。

很明显,亚马逊知道发生了什么,因为他们预先记录了它。我可能不知道发生了什么事的全部“原因”,但至少一切都以看起来令人满意的方式解决了。

感谢所有帮助过的人。

【讨论】:

  • 如果你在使用 MySql 时碰巧遇到这个问题。 MySql 仅支持 all 或 1 的 fetch size。一次获取 1 条记录,您需要将 fetchSize 设置为 Integer.MIN_VALUE (见图)。 stackoverflow.com/questions/2447324/…
猜你喜欢
  • 2012-03-08
  • 2019-02-04
  • 1970-01-01
  • 2021-09-19
  • 2011-04-23
  • 1970-01-01
  • 2015-08-23
  • 2012-11-10
  • 2012-08-19
相关资源
最近更新 更多