Отладка утечки памяти, загрузка файлов потоковой передачи больших двоичных объектов PHP и MySQL

#php #mysql #memory-leaks #blob #http-streaming

#php #mysql #утечки памяти #большой двоичный объект #http-потоковая передача

Вопрос:

использование MAMP версии 2.0 на Mac __ Apache/2.0.64 (Unix) — PHP/5.3.5 — DAV/2 mod_ssl/2.0.64 — OpenSSL/0.9.7l — MySQL 5.5.9

У меня есть скрипт, который я пытаюсь запустить, и, похоже, он вызывает серьезные утечки памяти, которые я пытался отладить и не могу решить, как исправить.

По сути, скрипт является частью модуля файлового менеджера. Он обрабатывает загрузку файла при задании идентификатора.

Весь файл хранится в таблице базы данных в виде большого двоичного объекта в виде блоков по 64 КБ (на запись) и передается клиенту по запросу.

База данных: 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)
Размер файла — bigint(20)
FileUploadDate — дата
-время FileUploadBy — int(5)

file_details:
FileDataID — int(10) АВТО_ИНКРЕМЕНТ
FileID — int(10)
fileData — BLOB

Ошибка, которую я на самом деле получаю, — это (из журнала ошибок php):

[31-Oct-2011 09:47:39] Неустранимая ошибка PHP: разрешенный объем памяти в 134217728 байт исчерпан (пытался выделить 63326173 байта) в /root/htdocs/file_manager/file_manager_download.php в строке 150

Теперь фактическая функция загрузки работает, если файл достаточно мал, в данном случае менее 40 МБ, однако, как только он превысит это, как файл размером 60 МБ в приведенной выше ошибке, произойдет сбой. Все, что он делает, это загружает файл размером 0 КБ.

Очевидно, что 134217728 байт больше, чем 63326173 байт (128 МБ против 60 МБ).

Допустимый размер памяти 134217728 байт — это директива в php.ini: «memory_limit = 128 МБ; Максимальный объем памяти, который может потреблять скрипт»

Если я установлю значение 256M, это позволит мне загрузить этот файл размером 60 МБ, а также файл размером до 80 МБ.

Кроме того, если я установлю значение 1024M, это позволит мне загрузить файл размером 260 МБ и, возможно, больше.

Итак, вы можете видеть, что проблема заключается в утечке где-то в скрипте, который съедает всю память.

Вот сценарий загрузки:

 

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

$strDB=mysql_connect("localhost","username","password")or die ("Error connecting to mysql.. Error: (" . mysql_errno() . ") " . mysql_error());
$database=mysql_select_db("file_management",$strDB);

if (isset($_GET["id"])) {

    // List of nodes representing each 64kb chunk
    $nodelist = array();

    // Pull file meta-data
    $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 ("No results for this FileID.Your Query: " . $sql_GetFileDetails . " Error: (" . mysql_errno() . ") " . mysql_error());

    if (mysql_num_rows($result_GetFileDetails) != 1) { die ("A MySQL error has occurred.Your Query: " . $sql_GetFileDetails . " Error: (" . mysql_errno() . ") " . mysql_error()); }

    // Set the file object to get details from
    $FileDetailsArray = mysql_fetch_assoc($result_GetFileDetails);

    // Pull the list of file inodes
    $sql_GetFileDataNodeIDs = "SELECT FileDataID FROM file_data WHERE FileID = ".$_GET["id"]." order by FileDataID";

    if (!$result_GetFileDataNodeIDs = mysql_query($sql_GetFileDataNodeIDs)) { die("Failure to retrive list of file inodesYour Query: " . $sql_GetFileDataNodeIDs . " Error: (" . mysql_errno() . ") " . mysql_error()); }

    while ($row_GetFileDataNodeIDs = mysql_fetch_assoc($result_GetFileDataNodeIDs)) {
        $nodelist[] = $row_GetFileDataNodeIDs["FileDataID"];
    }


    $FileExtension = explode(".",$FileDetailsArray["FileName"]);
    $FileExtension = strtolower($FileExtension[1]);

    // Determine Content Type 
    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; // This forces download, instead of viewing in browser.
        case "png":     $ctype="application/force-download"; break; // This forces download, instead of viewing in browser.
        case "jpeg":    $ctype="application/force-download"; break; // This forces download, instead of viewing in browser.
        case "jpg":     $ctype="application/force-download"; break; // This forces download, instead of viewing in browser.
        default:        $ctype="application/force-download";        // This forces download, instead of viewing in browser.
    } 

    // Send down the header to the client


    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("Cache-Control: private",false); // required for certain browsers
    header("Content-Description: File Transfer");
    header("Content-Disposition: attachment; filename="".$FileDetailsArray["FileName"].""");
    header("Content-Transfer-Encoding: binary");
    header("Content-Type: ".$FileDetailsArray["FileSize"]);

    ob_end_clean();
    ob_start();
    ob_start("ob_gzhandler");

        $sql_GetFileDataBlobs = "SELECT FileData FROM file_data WHERE FileID = ".$_GET["id"]." ORDER BY FileDataID ASC;";

        if (!$result_GetFileDataBlobs = mysql_query($sql_GetFileDataBlobs)) { die("Failure to retrive list of file inodesYour Query: " . $sql_GetFileDataBlobs . " Error: (" . mysql_errno() . ") " . mysql_error()); }

        while ($row_GetFileDataBlobs = mysql_fetch_array($result_GetFileDataBlobs)) {
            echo $row_GetFileDataBlobs["FileData"];
        }


    ob_end_flush();
    header('Content-Length: '.ob_get_length());
    ob_end_flush();
}

  

I have used Xdebug and output the results for peak memory usage, but nothing appears to be going anywhere near the limits, in total the peak memory usage was something like 900kb for the page.

Итак, я думаю, что он объединяет фрагменты файлов в память и не позволяет им удаляться или что-то подобное, но фрагменты файлов — это единственное, что может достичь такого объема памяти, что приведет к сбою скрипта.

Я могу предоставить скрипт для загрузки файла в базу данных, чтобы вы могли протестировать мой скрипт, если хотите, просто дайте мне знать

Приветствую любую помощь!

Мик


* ///////// РЕШАЕМАЯ ///////// *

Я просто хочу сказать спасибо hafichuk, отличный ответ и решил всю мою проблему.

Проблема была двоякой.

1 — Я не использовал ob_flush() внутри цикла while. Я добавил это, и, похоже, это освободило много памяти, позволяя загружать больше, но не неограниченно.

Например, с memory_limit = 128M я мог бы теперь загрузить более 40 МБ, фактически я мог бы теперь получить около 200 МБ. Но здесь снова произошел сбой. Однако первая проблема с памятью решена.

УРОК 1. Очистите свои объекты!

2 — Я использовал mysql_query для получения результатов для моего SQL-запроса. Проблема в том, что он буферизует эти результаты, и это усугубляло мою проблему с ограничением памяти.

Вместо этого я использовал mysql_unbuffered_query, и теперь это работает безупречно.

Однако это связано с некоторыми ограничениями, заключающимися в том, что ваша таблица блокируется при чтении результатов.

УРОК 2. Не буферизуйте результаты mysql, если они не требуются! (в рамках программных ограничений)

ЗАКЛЮЧИТЕЛЬНЫЙ УРОК:

Все эти исправления работают, однако требуется дополнительное тестирование, чтобы убедиться, что с их сочетанием проблем нет.

Кроме того, я узнал намного больше об объектах и распределении памяти php, я просто хотел бы, чтобы был способ визуальной отладки процесса немного лучше, чем то, что предлагает xdebug. Если у кого-нибудь есть какие-либо идеи о том, как xdebug мог пролить свет на этот процесс, пожалуйста, дайте мне знать в комментариях.

Надеюсь, это поможет кому-то еще в будущем.

Приветствия

Мик

Комментарии:

1. Вы пробовали php.net/manual/en/function.ob-flush.php в вашем цикле while?

2. хафичук, ты легенда! Это работает (почти) идеально. Просто чтобы вы знали, похоже, что он работает в полном соответствии с директивой memory_limit, поэтому, если я установил для него значение 128 МБ и попробовал файл размером 140 МБ, он работает, но если я попытаюсь загрузить файл размером 250 МБ, он получит поток размером около 200 МБ и завершится неудачей с ошибкой php: [31-Oct-2011 11:46:01] Неустранимая ошибка PHP: разрешенный объем памяти 134217728 байт исчерпан (пытался выделить 132392961 байт) в /root/htdocs/file_manager/file_manager_download.php в строке 125, которая является строкой ob_flush();.

3. mysql_unbuffered_query спас мой день, спасибо.

4. Чтобы избавиться от проблемы с блокировкой таблицы, вы можете выбрать CHAR_LEN большого двоичного объекта в mysql, а затем последовательно вызывать SUBSTR для поля порциями по X байт. Это похоже на буферизацию бедняков внутри PHP, но это должно освободить вашу блокировку таблицы на микросекунды за раз, пока ob_flush() делает свое дело. Это может даже помочь вам контролировать скорость загрузки с помощью sleep () между вызовами DB. Вот как я передаю изображения из моей CMS.

Ответ №1:

Вам просто нужно выполнить «ob_flush()» в цикле while. Это очистит буфер для страницы. Ваш последний заголовок, в котором указана длина содержимого, необходимо будет удалить, поскольку вы не можете отправить заголовок после запуска данных. Это не должно быть проблемой при загрузке файла, только обновление индикатора выполнения для загрузки.

Комментарии:

1. Привет, хафичук, я добавил эту строку в цикл while, и, как я ответил в комментарии выше, похоже, она работает, но только до предела директивы memory_limit . Если я должен загружать файлы, которые могут быть, скажем, 500 МБ, нужно ли мне увеличивать директиву memory_limit в php.ini? или ob_flush способен обрабатывать любой размер файла, независимо от директивы, но только потому, что мой код неверен в другом месте? Большое спасибо за вашу помощь.

2. Ваш mysql_fetch_X буферизует данные, поэтому вам может потребоваться изучить использование php.net/manual/en/function.mysql-unbuffered-query.php .

3. Возможно, вам повезет больше, если вы посмотрите на сохранение файлов вне базы данных и использование php.net/manual/en/function.fpassthru.php .

4. Хорошо, я ввел mysql_unbuffered_query, и он работает отлично. Просто чтобы расширить функцию mysql_unbuffered_query, он блокирует таблицу для любых операций записи / чтения во время получения набора результатов, но это нормально для приложения, которое мне требуется. Кроме того, в ответ на ваш совет переместить файлы из базы данных, у меня есть много веских причин для их размещения в базе данных, и (сейчас) ни одной причины для использования файловой системы! Однако у меня есть много причин для использования файловой системы. Все это связано с безопасностью, хранением, сопоставлением, привилегиями доступа и т. Д.. Хафичук, еще раз большое спасибо за вашу помощь!