我尝试将一些文档从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时会导致内存耗尽错误?
答案 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;输出缓冲区。