调试内存泄漏,PHP 和 MySQL Blob 流文件下载

3
在Mac上使用MAMP v2.0 __ Apache/2.0.64 (Unix) -- PHP/5.3.5 -- DAV/2 mod_ssl/2.0.64 -- OpenSSL/0.9.7l -- MySQL 5.5.9。
我有一个脚本,我试图运行它,但它似乎给我造成了严重的内存泄漏问题,尝试过调试,但无法解决。
基本上,该脚本是文件管理器模块的一部分。当给定ID时,它会处理文件的下载。
整个文件以64kb块(每个记录)的BLOB形式存储在数据库表中,并在请求时流式传输到客户端。
引用:
数据库:file_management 表:file_details, file_data file_details
FileID - int(10) AUTO_INCREMENT
FileTypeID - int(10)
FileType - varchar(60)
FileName - varchar(255)
FileDescription - varchar(255)
FileSize - bigint(20)
FileUploadDate - datetime
FileUploadBy - int(5)
file_details
FileDataID - int(10) AUTO_INCREMENT
FileID - int(10)
FileData - BLOB
实际收到的错误是(来自php错误日志): [31-Oct-2011 09:47:39] PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 63326173 bytes) in /root/htdocs/file_manager/file_manager_download.php on line 150 现在,如果文件足够小,实际下载功能是正常的,例如在这种情况下,小于40mb,但是一旦超过这个大小,就像上面错误中的60mb文件一样,它就会失败。它只是下载一个0kb的文件。
显然,134217728字节比63326173字节(128mb vs 60mb)多。 Allowed memory size of 134217728 bytes 是php.ini中的指令:"memory_limit = 128M ; Maximum amount of memory a script may consume" 如果我将其设置为256M,则允许我下载那个60mb文件,以及高达80mb左右的文件。
此外,如果我将其设置为1024M,则允许我下载260mb文件,可能更大。
因此,您可以看到问题是脚本中某个地方存在泄漏,导致所有内存被耗尽。
以下是下载脚本:


    ini_set('display_errors',1);
error_reporting(E_ALL & ~E_NOTICE);

$strDB=mysql_connect("localhost","username","password")or die ("无法连接mysql.. 错误: (" . mysql_errno() . ") " . mysql_error());
$database=mysql_select_db("file_management",$strDB);
if (isset($_GET["id"])) {
// 用于表示每个64kb块的节点列表 $nodelist = array();
// 获取文件元数据 $sql_GetFileDetails = " SELECT FileID, FileTypeID, FileType, FileName, FileDescription, FileSize, FileUploadDate, FileUploadBy FROM file_details WHERE FileID = '".$_GET["id"]."';";
$result_GetFileDetails = mysql_query($sql_GetFileDetails) or die ("没有找到此文件ID相关的结果。
查询请求: " . $sql_GetFileDetails . "
错误: (" . mysql_errno() . ") " . mysql_error());
if (mysql_num_rows($result_GetFileDetails) != 1) { die ("发生MySQL错误。
查询请求: " . $sql_GetFileDetails . "
错误: (" . mysql_errno() . ") " . mysql_error()); }
// 设置文件对象以获取详细信息 $FileDetailsArray = mysql_fetch_assoc($result_GetFileDetails);
// 获取文件inode列表 $sql_GetFileDataNodeIDs = "SELECT FileDataID FROM file_data WHERE FileID = ".$_GET["id"]." order by FileDataID";
if (!$result_GetFileDataNodeIDs = mysql_query($sql_GetFileDataNodeIDs)) { die("未能检索文件inode列表
查询请求: " . $sql_GetFileDataNodeIDs . "
错误: (" . mysql_errno() . ") " . mysql_error()); }
while ($row_GetFileDataNodeIDs = mysql_fetch_assoc($result_GetFileDataNodeIDs)) { $nodelist[] = $row_GetFileDataNodeIDs["FileDataID"]; }
$FileExtension = explode(".",$FileDetailsArray["FileName"]); $FileExtension = strtolower($FileExtension[1]);
// 确定内容类型 switch ($FileExtension) {
case "mp3": $ctype="audio/mp3"; break; case "wav": $ctype="audio/wav"; break; case "pdf": $ctype="application/pdf"; break; //case "exe": $ctype="application/octet-stream"; break; case "zip": $ctype="application/zip"; break; case "doc": $ctype="application/msword"; break; case "xls": $ctype="application/vnd.ms-excel"; break; case "ppt": $ctype="application/vnd.ms-powerpoint"; break; case "gif": $ctype="application/force-download"; break; // 这将强制下载,而不是在浏览器中查看。 case "png": $ctype="application/force-download"; break; // 这将强制下载,而不是在浏览器中查看。 case "jpeg": $ctype="application/force-download"; break; // 这将强制下载,而不是在浏览器中查看。 case "jpg": $ctype="application/force-download"; break; // 这将强制下载,而不是在浏览器中查看。 default: $ctype="application/force-download"; // 这将强制下载,而不是在浏览器中查看。 }
// 发送头信息到客户端
header("Date: ".gmdate("D, j M Y H:i:s e", time())); header("Cache-Control: max-age=2592000"); //header("Last-Modified: ".gmdate("D, j M Y H:i:s e", $info['mtime'])); //header("Etag: ".sprintf("\"%x-%x-%x\"", $info['ino'], $info['size'], $info['mtime'])); header("Accept-Ranges: bytes"); //header("Cache-Control: Expires ".gmdate("D, j M Y H:i:s e", $info['mtime']+2592000)); header("Pragma: public"); // required header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header

我使用了Xdebug并输出了峰值内存使用情况,但似乎没有任何东西接近极限,总的来说,该页面的峰值内存使用量大约为900kb。

因此,我认为它正在将文件块聚合到内存中并且不释放它们,或者类似的问题,但是文件块是唯一会达到那个内存量并导致脚本失败的东西。

如果您愿意,我可以提供上传文件到数据库的脚本,这样您就可以测试我的脚本,只需让我知道即可

感谢任何帮助!

Mick


*/////////已解决/////////*

我想对hafichuk表示感谢,他的回复很好,并解决了我的整个问题。

问题有两个方面。

1-我没有在while循环中使用ob_flush()。我添加了它,它似乎释放了大量内存,使得更大的下载成为可能,但不是无限制的。

例如,使用memory_limit = 128M,我现在可以下载超过40mb,实际上我现在可以获得大约200mb的下载速度。但这就是它再次失败的地方。首先解决了内存问题。

教训1:刷新你的对象!

2-我正在使用mysql_query来检索SQL查询的结果。问题在于它缓冲这些结果,这增加了我的内存限制问题。

最后我使用了mysql_unbuffered_query,现在它完美运行。

但是,这确实带来了一些限制,即在读取结果时会锁定您的表。

教训2:如果不需要,请勿缓冲mysql结果!(在程序限制范围内)

最终教训:

所有这些修复方法都有效,但需要进行更多的测试以确保它们之间没有问题。

此外,我对对象和php内存分配有了更多的了解,但我希望有一种比xdebug提供的更好的方式来可视化调试该过程。如果有人对xdebug如何可以实际阐明此过程的任何想法,请在评论中让我知道。

希望这能帮助未来的某个人。

干杯

Mick


你在 while 循环中尝试过 http://php.net/manual/zh/function.ob-flush.php 吗? - hafichuk
hafichuk,你是个传奇!它(几乎)完美地工作了。只是想让你知道,它似乎可以完全利用memory_limit指令的限制,所以如果我将其设置为128M,并尝试下载一个140MB的文件,它可以正常工作,但如果我尝试下载一个250MB的文件,它会在流式传输到约200MB时失败,并显示PHP错误:[31-Oct-2011 11:46:01] PHP致命错误:在/root/htdocs/file_manager/file_manager_download.php的第125行(即ob_flush();行)中耗尽了134217728字节的允许内存大小(尝试分配132392961字节)。 - Quantico773
mysql_unbuffered_query 挽救了我的一天,谢谢。 - webtweakers
为了解决表锁定问题,您可以在mysql中选择blob的CHAR_LEN,然后按X字节的块连续调用字段上的SUBSTR。这就像PHP内部的贫民缓冲一样,但是它应该会在ob_flush()执行其操作时每次释放表锁定几微秒。甚至可以通过DB调用之间的sleep()来帮助您控制下载速度。这就是我从我的CMS流式传输图像的方式。 - chugadie
1个回答

1
你只需在 while 循环中执行 "ob_flush()" 操作即可。这将清除页面缓冲区。需要删除最后一个标题列表中的内容长度,因为在数据开始后不能发送标头。这不应该会影响文件下载,仅会更新下载进度表。

嗨hafichuk,我将这行代码添加到while循环中,并如上面的评论回复所述,它似乎可以工作,但仅限于memory_limit指令的限制。如果我要下载可能达到500mb的文件,我需要增加php.ini中的memory_limit指令吗?还是ob_flush能够处理任何文件大小,而不受指令的影响,只是我的代码在其他地方出了问题?非常感谢您的帮助。 - Quantico773
您的 mysql_fetch_X 会缓冲数据,因此您可能需要考虑使用 http://www.php.net/manual/zh/function.mysql-unbuffered-query.php。 - hafichuk
如果您考虑将文件存储在数据库之外并使用http://www.php.net/manual/en/function.fpassthru.php,那么您可能会有更好的运气。 - hafichuk
好的,我加入了mysql_unbuffered_query并且它完美地工作了。为了扩展mysql_unbuffered_query的功能,它在获取结果集时锁定表以防止任何写入/读取,但对于我需要的应用程序来说这是可以接受的。此外,针对您将文件移出数据库的建议,我有许多充分的理由将它们放在数据库中,并且(现在)没有使用文件系统的理由!我现在使用文件系统有许多原因,都与安全性、存储、整理、访问权限等有关。Hafichuk,再次感谢您的帮助! - Quantico773

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接