0

我尝试将一些文档从 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设置的情况下避免此类错误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);                                  
        }                   
}

然后在我的控制器中,我尝试这样称呼它:

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;

}

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

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

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

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 增加到 256M 并尝试导出 1.3M 列表,这就是它所显示的:

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

这对我来说似乎很混乱。这个数据是不是很不准确?否则,为什么会导致 memory_limit 设置为 128M 的内存耗尽错误?

4

2 回答 2

1

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

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

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

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

于 2012-04-10T21:01:32.750 回答
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 () 方法。

看起来,不管你怎么做,通过echoprintreadfile,输出总是被捕获,并卡在响应正文中,即使你在调度之前关闭了响应返回功能。

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

还有待确定如何告诉 Zend 不要“捕获”输出缓冲区。

于 2016-02-17T11:52:00.013 回答