web-dev-qa-db-ja.com

LAMP:ディスク/ CPUスラッシングなしで、ユーザーがオンザフライで大容量ファイルの.Zipを作成する方法

多くの場合、Webサービスは、クライアントによるダウンロードのためにいくつかの大きなファイルを圧縮する必要があります。これを行う最も明白な方法は、一時Zipファイルを作成してから、それをユーザーにechoするか、ディスクに保存してリダイレクトします(後で削除します)。

ただし、その方法で行うことには欠点があります。

  • 集中的なCPUとディスクのスラッシングの初期段階、その結果...
  • アーカイブの準備中にユーザーにかなりの初期遅延
  • リクエストごとの非常に高いメモリフットプリント
  • 大量の一時ディスク領域の使用
  • ユーザーが途中でダウンロードをキャンセルすると、最初のフェーズで使用されたすべてのリソース(CPU、メモリ、ディスク)が無駄になります。

ZipStream-PHP のようなソリューションは、データをファイルごとにApacheに分割することでこれを改善します。ただし、その結果、メモリの使用率が高くなり(ファイルは完全にメモリに読み込まれます)、ディスクとCPUの使用率が大幅に上昇します。

対照的に、次のbashスニペットを検討してください。

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

ここで、Zipはストリーミングモードで動作するため、メモリフットプリントが低くなります。パイプには統合バッファーがあります。バッファーがいっぱいになると、OSは書き込みプログラム(パイプの左側のプログラム)を中断します。これにより、Zipcatで出力を書き込める速度でのみ動作するようになります。

次に、最適な方法は同じことです。catをWebサーバープロセスに置き換え、streamingZipファイルをその場で作成されたユーザー。これにより、ファイルをストリーミングするだけの場合に比べてオーバーヘッドがほとんど発生せず、問題のない、とがっていないリソースプロファイルが作成されます。

LAMPスタックでこれをどのように実現できますか?

43
Benji XVI

popen()(docs) またはproc_open()(docs) を使用して、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);
_

「その他のテクノロジー」について質問しましたが、「リクエストのライフサイクル全体でノンブロッキングI/Oをサポートするものなら何でも」と言います。 JavaまたはC/C++(または他の多くの利用可能な言語)でスタンドアロンサーバーのようなコンポーネントを構築できます。ifノンブロッキングファイルアクセスの「ダウンアンドダーティー」に陥る。

非ブロッキング実装が必要であるが、「ダウンアンドダーティー」を回避したい場合、最も簡単なパス(IMHO)は nodeJS を使用することです。 nodejsの既存のリリースで必要なすべての機能が十分にサポートされています。httpサーバーには(もちろん)httpモジュールを使用してください。そして、_child_process_モジュールを使用して、tar/Zip/whateverパイプラインを生成します。

最後に、マルチプロセッサ(またはマルチコア)サーバーを実行していて、nodejsを最大限に活用したい場合は、 Spark2 を使用して同じインスタンスで複数のインスタンスを実行できますポート。プロセッサコアごとに複数のnodejsインスタンスを実行しないでください。


Update(この回答のコメントセクションにあるBenjiの優れたフィードバックから)

1。fread()のドキュメントは、関数が一度に最大8192バイトのデータのみを読み取ることを示しています。通常のファイルではありません。したがって、8192がバッファサイズの適切な選択になる場合があります。

[社説] 8192はほぼ確実にプラットフォームに依存する値です。ほとんどのプラットフォームでは、オペレーティングシステムの内部バッファーが空になるまでfread()がデータを読み取り、その時点でデータが返されるため、OSは再び非同期でバッファリングします。 8192は、多くの一般的なオペレーティングシステムのデフォルトバッファのサイズです。

他にも、freadが8192バイト未満を返す可能性のある状況があります。たとえば、「リモート」クライアント(またはプロセス)がバッファを埋めるのに時間がかかる-ほとんどの場合、fread()は入力バッファーの内容がいっぱいになるのを待たずに、そのままの状態です。これは、0..os_buffer_sizeバイトから返されることを意味します。

道徳は、buffsizeとしてfread()に渡す値を「最大」サイズと見なす必要があります。要求したバイト数(またはその件に関する他の番号)。

2。fread docsのコメントによると、いくつかの警告: magic quotes は干渉する可能性があり、 オフ

3。mb_http_output('pass')(docs) を設定することをお勧めします。 _'pass'_はすでにデフォルト設定ですが、コードまたは構成で以前に他の何かに変更した場合は、明示的に指定する必要がある場合があります。

4。(gzipではなく)Zipを作成する場合は、コンテンツタイプヘッダーを使用します。

_Content-type: application/Zip
_

または... 'application/octet-stream'を代わりに使用できます。 (これは、あらゆる種類のバイナリダウンロードに使用される一般的なコンテンツタイプです):

_Content-type: application/octet-stream
_

また、ユーザーにファイルをダウンロードしてディスクに保存するように求めるメッセージが表示されるようにしたい場合(ブラウザーにファイルをテキストとして表示させようとするのではなく)、content-dispositionヘッダーが必要になります。 (ファイル名は、保存ダイアログで推奨される名前を示します):

_Content-disposition: attachment; filename="file.Zip"
_

Content-lengthヘッダーも送信する必要がありますが、Zipの正確なサイズが事前にわからないため、この手法では困難です。 コンテンツが「ストリーミング」または長さが不明であることを示すために設定できるヘッダーはありますか?誰か知っていますか?


最後に、@ Benji's の提案をすべて使用する(TAR.GZIPファイルの代わりにZipファイルを作成する)変更された例を次に示します。

_<?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);
_

Update:(2012-11-23)読み取り/エコーループ内でflush()を呼び出すと、作業中に問題が発生する可能性があることを発見しました非常に大きなファイルや非常に遅いネットワーク。少なくとも、これは、Apacheの背後でPHPをcgi/fastcgiとして実行する場合に当てはまり、他の構成で実行する場合にも同じ問題が発生する可能性があります。この問題は、PHPがApacheが実際にソケットを介して送信できるよりも速く出力をApacheにフラッシュするときに発生するようです。非常に大きなファイル(または低速の接続)の場合、これは最終的にApacheの内部出力バッファのオーバーランを引き起こします。これにより、ApacheはPHPプロセスを強制終了します。これにより、ダウンロードがハングするか、途中で転送が完了するだけで途中で完了します。

解決策は、flush()を呼び出すnotです。上記のコード例を更新してこれを反映し、回答の上部のテキストにメモを入れました。

49
Lee

別の解決策は、この目的のために特別に記述されたNginx用のmod_Zipモジュールです。

https://github.com/evanmiller/mod_Zip

これは非常に軽量で、別個の「Zip」プロセスを呼び出したり、パイプ経由で通信したりしません。含めるファイルの場所を一覧表示するスクリプトを指定するだけで、あとはmod_Zipが行います。

3
Emiller

私は先週末、このs3スチーミングファイルジッパーマイクロサービスを書きました-役に立つかもしれません: http://engineroom.teamwork.com/how-to-securely-provide-a-Zip-download-of-a-s3-file -bundle /

2
user3665185

さまざまなサイズの多くのファイルを使用して動的に生成されたダウンロードを実装しようとすると、このソリューションに出くわしましたが、「許可されたメモリサイズ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);
2
Rico Sonntag

the PHP manual によると、 Zip拡張 はZip:ラッパーを提供します。

私はこれを使用したことがなく、その内部もわかりませんが、Zipアーカイブをストリーミングできると仮定すると、論理的にはあなたが探していることを実行できるはずです。

「LAMPスタック」についての質問については、PHPがでない限りconfigured toバッファ出力


編集:私は概念実証をまとめようとしていますが、それは簡単ではないようです。 PHPのストリームに慣れていない場合、それが可能であるとしても、複雑すぎる可能性があります。


Edit(2):ZipStreamを確認した後、質問をもう一度読んで、あなたが言うとき、ここであなたの主な問題になるものを見つけました(強調を追加)

有効な圧縮はストリーミングモードで動作する必要があります。つまり、ファイルを処理し、ダウンロードの速度でデータを提供します。

PHPは、Apacheのバッファがどれだけフルであるかを判断する方法を提供するので、その部分の実装は非常に困難です。したがって、あなたの質問に対する答えは「いいえ」ですPHPでそれを行うことができます。

1
Josh Davis

fpassthru() を使用すると、出力バッファに関連する問題を排除できるようです。また、私のデータは既にコンパクトなので、-0を使用してCPU時間を節約しています。このコードを使用して、オンザフライで圧縮されたフォルダー全体を提供します。

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