从mongodb导出允许的内存大小耗尽错误

时间:2012-04-10 07:58:26

标签: php mongodb mongodb-php

我尝试将一些文档从mongodb导出到.csv。对于一些大型列表,文件将类似于40M,我得到有关内存限制的错误:

Fatal error: Allowed memory size of 134217728 bytes exhausted 
(tried to allocate 44992513 bytes) in
/usr/share/php/Zend/Controller/Response/Abstract.php on line 586

我想知道为什么会发生这种错误。什么消耗这么多的内存?如何在不更改现在设置memory_limit的{​​{1}}的情况下避免此类错误。

我使用这样的东西:

128M

然后在我的控制器中,我尝试将其称为:

public static function exportList($listId, $state = self::SUBSCRIBED)
{
        $list = new Model_List();       
        $fieldsInfo = $list->getDescriptionsOfFields($listId);
        $headers = array(); 
        $params['list_id'] = $listId;
        $mongodbCursor = self::getCursor($params, $fieldsInfo, $headers);
        $mongodbCursor->timeout(0);
        $fp = fopen('php://output', 'w');       
        foreach ($mongodbCursor as $subscriber) {
            foreach ($fieldsInfo as $fieldInfo) {           
                $field = ($fieldInfo['constant']) ? $fieldInfo['field_tag'] : $fieldInfo['field_id'];
                if (!isset($subscriber->$field)) {
                    $row[$field] = '';
                } elseif (Model_CustomField::isMultivaluedType($fieldInfo['type'])) {
                    $row[$field] = array();     
                    foreach ($subscriber->$field as $value) {
                        $row[$field][] = $value;                        
                    }
                    $row[$field] = implode(self::MULTIVALUED_DELEMITOR, $row[$field]);
                } else {
                    $row[$field] = $subscriber->$field;
                }                               
            }               
            fputcsv($fp, $row);                                  
        }                   
}

所以我在导出数据的文件的末尾。令人感到奇怪的是,对于我用1M文档导出的列表,它成功导出并显示:

public function exportAction()
{

    set_time_limit(300);


    $this->_helper->layout->disableLayout();
    $this->_helper->viewRenderer->setNoRender();
    $fileName = $list->list_name . '.csv';

    $this->getResponse()->setHeader('Content-Type', 'text/csv; charset=utf-8')
                        ->setHeader('Content-Disposition', 'attachment; filename="'. $fileName . '"');                                                              

    Model_Subscriber1::exportList($listId);
    echo 'Peak memory usage: ', memory_get_peak_usage()/1024, ' Memory usage: ', memory_get_usage()/1024;

}

但是当我尝试导出1.3M文档时,几分钟后我才进入导出文件:

> Peak memory usage: 50034.921875 Kb Memory usage: 45902.546875 Kb

我导出的文档大小大致相同。

我将memory_limit增加到256M并尝试导出1.3M列表,这就是它显示的内容:

  

峰值内存使用量:60330.4609375Kb内存使用量:56894.421875 Kb。

这对我来说似乎很困惑。这些数据不是那么不准确吗?否则,为什么在memory_limit设置为128M时会导致内存耗尽错误?

2 个答案:

答案 0 :(得分:1)

虽然文档的大小可能大致相同,但PHP分配处理它们的大小与文档大小或文档数量不成正比。这是因为不同类型在PHP中需要不同的内存分配。您可以随时释放一些内存,但我没有在您的代码中看到任何可以放置的地方。

最好的答案可能就是增加内存限制。

您可以做的一件事是将处理卸载到外部脚本并从PHP调用它。许多语言以比PHP更高效的内存方式进行这种处理。

我也注意到memory_get_peak_usage()并不总是准确的。我会尝试一个实验来将mem_limit增加到256并在更大的数据集(130万)上运行它。您可能会发现它的报告低于128限制。

答案 1 :(得分:0)

我可以在导出CSV文件的类似情况下重现此问题,其中我的系统应该具有足够的内存,如 memory_get_usage ()所示,但结束了同样的致命错误: 致命错误:允许的内存大小

我通过将CSV内容输出到物理临时文件中来规避这个问题,我最终将其压缩,然后再将其读出。 我在一个循环中编写了文件,因此每次迭代只写了一小段数据,所以我永远不会超出内存限制。 压缩后,压缩率是这样的,我可以处理原始文件的大小超过我最初碰到墙的大小的10倍。总而言之,这是成功的。

提示:在创建存档时,在调用$ zip-> close()之前,不要取消链接存档组件,因为此调用似乎是执行商业。否则你最终会得到一个空档案!

代码示例:

<?php
$zip = new ZipArchive;
if ($zip->open($full_zip_path, ZipArchive::CREATE) === TRUE) {
    $zip->addFile($full_csv_path, $csv_name);
    $zip->close();

    $Response->setHeader("Content-type", "application/zip; charset=utf-8");
    $Response->setHeader("Content-disposition", "attachment; filename=" . $zip_name);

    $Response->setBody(file_get_contents($full_zip_path));
}
else {
    var_dump(error_get_last());
    echo utf8_decode("Couldn't create zip archive '$full_zip_path'."), "\r\n";
}

unset($zip);
?>

注意:向zip存档添加项目时,如果使用基于Windows的操作系统,请不要在项目名称前加前导斜杠。

关于原始问题的讨论:

引用行的Zend文件是

public function outputBody()
{
    $body = implode('', $this->_body);
    echo $body;
}

来自 Zend_Controller_Response_Abstract 类的 outputBody ()方法。

看起来,无论你是通过 echo ,还是打印,还是 readfile ,都会捕获输出并卡住进入响应体,即使您在发送之前关闭响应返回功能。

我甚至尝试在 echo loop 中使用 clearBody()类方法,记住每个 $ response-&gt; sendResponse() 后跟 $ response-&gt; clearBody()会释放内存,但失败了。 Zend处理响应发送的方式是我总是得到原始CSV文件的完整大小的内存分配。

尚未确定如何告诉Zend不要&#34;捕获&#34;输出缓冲区。