ЛАМПА: Как создать .Zip больших файлов для пользователя на лету, без перегрузки диска / процессора - PullRequest
43 голосов
/ 05 декабря 2010

Часто веб-сервису необходимо заархивировать несколько больших файлов для загрузки клиентом.Наиболее очевидный способ сделать это - создать временный zip-файл, затем либо echo передать его пользователю, либо сохранить его на диск и перенаправить (удалив его в будущем).

Однако, делая этоу этого пути есть недостатки:

  • начальная фаза интенсивной работы процессора и диска, что приводит к ...
  • значительной начальной задержке для пользователя во время подготовки архива
  • очень большой объем памяти на запрос
  • использование значительного временного дискового пространства
  • если пользователь отменяет загрузку на полпути, все ресурсы, использованные на начальном этапе (ЦП, память,диск) будет потрачено впустую

Решения, такие как ZipStream-PHP , улучшат эту ситуацию, перенося данные в файл Apache за файлом.Однако в результате все еще остается высокий уровень использования памяти (файлы загружаются полностью в память) и большие скачкообразные скачки в использовании диска и процессора.

Для сравнения рассмотрим следующий фрагмент кода bash:

ls -1 | zip -@ - | cat > file.zip
  # Note -@ is not supported on MacOS

Здесь zip работает в потоковом режиме, что приводит к снижению занимаемой памяти.Канал имеет встроенный буфер - когда буфер заполнен, ОС приостанавливает запись программы (программа слева от канала).Это гарантирует, что zip работает только так быстро, как его выходные данные могут быть записаны с помощью cat.

Оптимальным способом будет то же самое: заменить cat процессом веб-сервера потоковое ZIP-файл для пользователя, созданного на лету.Это приведет к небольшим издержкам по сравнению с простой потоковой передачей файлов и будет иметь беспроблемный, не колючий профиль ресурса.

Как этого добиться в стеке LAMP?

Ответы [ 6 ]

49 голосов
/ 05 декабря 2010

Вы можете использовать popen() (документы) или proc_open() (документы) для выполнения команды Unix (например, zip или gzip) и получить обратно стандартный вывод в видеPHP поток.flush() (docs) сделает все возможное, чтобы передать содержимое буфера вывода php в браузер.

Объединение всего этого даст вам то, что вы хотите (при условии, что ничего большевстает на пути - см. предостережения на странице документации для flush()).

( Примечание : не используйте flush(). Подробности смотрите ниже в обновлении.)

Что-то вроде следующего может помочь:

<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/x-gzip');

// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to 
// control the input of the pipeline too)
//
$fp = popen('tar cf - file1 file2 file3 | gzip -c', 'r');

// pick a bufsize that makes you happy (64k may be a bit too big).
$bufsize = 65535;
$buff = '';
while( !feof($fp) ) {
   $buff = fread($fp, $bufsize);
   echo $buff;
}
pclose($fp);

Вы спрашивали о «других технологиях»: на что я скажу «все, что поддерживаетблокировка ввода / вывода на весь жизненный цикл запроса ".Вы могли бы построить такой компонент, как автономный сервер на Java или C / C ++ (или любом из многих других доступных языков), , если , вы были готовы попасть в «грязный»блокировка доступа к файлам и еще много чего.

Если вы хотите неблокирующую реализацию, но вы бы предпочли избежать «грязного и грязного», самый простой путь (IMHO) - использовать nodeJS ,В существующей версии nodejs имеется достаточно поддержки для всех функций, которые вам нужны: используйте модуль http (конечно) для http-сервера;и использовать модуль child_process для запуска tar / zip / любого конвейера.

Наконец, если (и только если) вы используете многопроцессорный (или многоядерный) сервер, и вы хотитебольшинство из nodejs можно использовать Spark2 для запуска нескольких экземпляров на одном и том же порту.Не запускайте более одного экземпляра nodejs на каждое ядро ​​процессора.


Обновление (из превосходных отзывов Бенджи в разделе комментариев к этому ответу)

1. Документы для fread() указывают, что функция будет одновременно считывать до 8192 байтов данных из всего, что не является обычным файлом.Следовательно, 8192 может быть хорошим выбором размера буфера.

[примечание редакции] 8192 почти наверняка зависит от платформы - на большинстве платформ fread() будет считывать данные до тех пор, пока внутренний буфер операционной системы не опустеет, и в этот момент он вернется, что позволит операционной системезаполнить буфер снова асинхронно.8192 - это размер буфера по умолчанию во многих популярных операционных системах.

Существуют и другие обстоятельства, которые могут заставить fread возвращать даже менее 8192 байт - например, «удаленный» клиент (или процесс) медленно заполняет буфер - в большинстве случаев fread() будетвернуть содержимое входного буфера как есть, не дожидаясь его заполнения.Это может означать где-нибудь от 0..os_buffer_size возвращаемых байтов.

Мораль такова: значение, которое вы передаете fread() как buffsize, следует считать «максимальным» размером - никогда не думайте, что вымы получили количество байтов, которые вы запрашивали (или любое другое число по этому вопросу).

2. Согласно комментариям к fread docs, несколько предостережений: магические кавычки могут мешать и должны быть выключены .

3. Настройка mb_http_output('pass') (документы) может быть хорошей идеей.Хотя 'pass' уже является настройкой по умолчанию, вам может потребоваться указать ее явно, если ваш код или конфигурация ранее изменили ее на что-то другое.

4. Если вы создаетеzip (в отличие от gzip), вы можете использовать заголовок типа контента:

Content-type: application/zip

или ... вместо него можно использовать application / octet-stream.(это общий тип контента, используемый для бинарных загрузок всех видов):

Content-type: application/octet-stream

и если вы хотите, чтобы пользователю предлагалось загрузить и сохранить файл на диск (вместо того, чтобы попытаться запустить браузер)чтобы отобразить файл в виде текста), тогда вам понадобится заголовок content-disposition.(где имя файла указывает имя, которое следует предложить в диалоге сохранения):

Content-disposition: attachment; filename="file.zip"

Также следует отправить заголовок Content-length, но это сложно сделать с помощью этой техники, поскольку вы заранее не знаете точный размер почтового индекса. Есть ли заголовок, который можно установить, чтобы указать, что контент "потоковый" или имеет неизвестную длину?Кто-нибудь знает?


Наконец, вот пересмотренный пример, который использует все предложения Benji (и создает файл ZIP вместо файла TAR.GZIP):

<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="file.zip"');

// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to 
// control the input of the pipeline too)
//
$fp = popen('zip -r - file1 file2 file3', 'r');

// pick a bufsize that makes you happy (8192 has been suggested).
$bufsize = 8192;
$buff = '';
while( !feof($fp) ) {
   $buff = fread($fp, $bufsize);
   echo $buff;
}
pclose($fp);

Обновление : (2012-11-23) Я обнаружил, что вызов flush() в цикле чтения / эхо может вызвать проблемы при работе с очень большимифайлы и / или очень медленные сети.По крайней мере, это верно при запуске PHP как cgi / fastcgi за Apache, и кажется вероятным, что та же проблема возникнет и при работе в других конфигурациях.Проблема возникает, когда PHP сбрасывает вывод в Apache быстрее, чем Apache может фактически отправить его через сокет.Для очень больших файлов (или медленных соединений) это в конечном итоге приводит к переполнению внутреннего выходного буфера Apache.Это заставляет Apache завершить процесс PHP, что, конечно, приводит к зависанию или преждевременному завершению загрузки, при этом происходит только частичная передача.

Решение - , а не для вызова flush() на всех.Я обновил приведенные выше примеры кода, чтобы отразить это, и поместил примечание в текст в верхней части ответа.

3 голосов
/ 30 мая 2011

Другим решением является мой модуль mod_zip для Nginx, написанный специально для этой цели:

https://github.com/evanmiller/mod_zip

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

2 голосов
/ 05 августа 2015

В прошлые выходные я написал этот микросервис zipper с парящим файлом s3 - может быть полезным: http://engineroom.teamwork.com/how-to-securely-provide-a-zip-download-of-a-s3-file-bundle/

2 голосов
/ 13 февраля 2012

Пытаясь реализовать динамически генерируемую загрузку с большим количеством файлов разных размеров, я сталкивался с этим решением, но сталкиваюсь с различными ошибками памяти, такими как «Разрешенный объем памяти 134217728 байт исчерпан при ...».

После добавления ob_flush(); прямо перед flush(); ошибки памяти исчезают.

Вместе с отправкой заголовков мое окончательное решение выглядит следующим образом (Просто храните файлы внутри zip без структуры каталогов):

<?php

// Sending headers
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');
header('Content-Transfer-Encoding: binary');
ob_clean();
flush();

// On the fly zip creation
$fp = popen('zip -0 -j -q -r - file1 file2 file3', 'r');

while (!feof($fp)) {
    echo fread($fp, 8192);
    ob_flush();
    flush();
}

pclose($fp);
1 голос
/ 05 декабря 2010

Согласно руководству по PHP , расширение ZIP предоставляет упаковку zip:.

Я никогда не использовал его и не знаю его внутренних компонентов,но по логике он должен быть в состоянии сделать то, что вы ищете, предполагая, что ZIP-архивы могут быть потоковыми, в чем я не совсем уверен.

Что касается вашего вопроса о "стеке LAMP", то он не долженне будет проблемой, если PHP не настроен на буферизацию вывода .


Редактировать: я пытаюсь поставить доказательствоКонцепция вместе, но, кажется, не тривиально.Если у вас нет опыта работы с потоками PHP, это может оказаться слишком сложным, если это вообще возможно.


Edit (2): перечитав ваш вопрос после взгляда на ZipStream, я обнаружил, что будетбыть вашей главной проблемой здесь, когда вы говорите (выделение добавлено)

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

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

0 голосов
/ 16 мая 2017

Кажется, вы можете устранить любые проблемы, связанные с выходным буфером, используя fpassthru () . Я также использую -0 для экономии процессорного времени, так как мои данные уже компактны. Я использую этот код для обслуживания целой папки, сжатой на лету:

chdir($folder);
$fp = popen('zip -0 -r - .', 'r');
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="'.basename($folder).'.zip"');
fpassthru($fp);
...