PHPを使用して大きなファイルを読み取る方法(サーバーを同時にクラッシュさせることなく)

クリストファーピット による記事の翻訳。







PHP開発者は、アプリケーションのメモリ消費を監視する必要はほとんどありません。 PHPエンジン自体は、背後にあるゴミをきれいにクリーンアップします。また、各リクエストの実行後に実行コンテキストが「死んで」いるWebサーバーモデルでは、最悪のコードでも大きな長い問題は発生しません。







ただし、状況によっては、RAMが不足する問題が発生する可能性があります。たとえば、小さなVPSで作曲家を実行しようとしたり、リソースが豊富でないサーバーで大きなファイルを開いたりする場合です。







断片化された地形







このレッスンでは、最後の問題について説明します。







すべてのコードはhttps://github.com/sitepoint-editors/sitepoint-performant-reading-of-big-files-in-phpで入手できます







成功の尺度



コードの最適化を行う場合、最適化の有効性(または破壊性)を評価するために、実行前後の結果を常に測定する必要があります。







通常、CPU負荷とメモリ使用量を測定します。 一方を保存すると、他方のコストが増加し、逆もまた同様です。







非同期アプリケーションモデル(マルチプロセッサおよびマルチスレッド)では、プロセッサとメモリの両方を常に監視することが非常に重要です。 従来のアプリケーションでは、サーバー制限に近づいたときにのみリソース制御が問題になります。







PHP内のCPU使用率を測定するのは悪い考えです。 UbuntuやmacOSのtopのようなユーティリティを使用することをお勧めします。 Windowsを使用している場合、Linuxサブシステムを使用してtopにアクセスできます。







このレッスンでは、メモリ使用量を測定します。 従来のスクリプトでメモリがどのように消費されるかを確認し、最適化のためにいくつかのチップを適用して結果を比較します。 この記事の終わりまでに、読者が大量のデータを読むときのメモリ消費を最適化する基本原則の基本的な理解を得ることを願っています。







このようにメモリを測定します。







// formatBytes is taken from the php.net documentation memory_get_peak_usage(); function formatBytes($bytes, $precision = 2) { $units = array("b", "kb", "mb", "gb", "tb"); $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . " " . $units[$pow]; }
      
      





各スクリプトの最後でこの関数を使用し、取得した値を比較します。







オプションは何ですか?



効率的なデータ読み取りのためのさまざまなアプローチがありますが、それらはすべて条件付きで2つのグループに分けることができます: データの読み取り部分を読み取り、 すぐに処理ます (最初にすべてのデータをメモリにロードせずに)。コンテンツ。







最初のオプションとして、ファイルを読み取り、10,000行ごとに個別に処理することを想像してみましょう。 少なくとも10,000行をメモリに保持し、それらを実装する形式に関係なくキューに渡す必要があります。







2番目のシナリオでは、非常に大きなAPI応答のコンテンツを圧縮するとします。 どんな種類のデータが含まれているかは重要ではありません。圧縮された形式で返すことが重要です。







どちらの場合も、大量の情報を考慮する必要があります。 前者ではデータ形式がわかり、後者では形式は関係ありません。 両方のオプションを検討してください。







行ごとにファイルを読む



ファイルを操作するための多くの機能があります。 彼らの助けを借りて読者を書きましょう:







 // from memory.php function formatBytes($bytes, $precision = 2) { $units = array("b", "kb", "mb", "gb", "tb"); $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . " " . $units[$pow]; } print formatBytes(memory_get_peak_usage());
      
      





 // from reading-files-line-by-line-1.php function readTheFile($path) { $lines = []; $handle = fopen($path, "r"); while(!feof($handle)) { $lines[] = trim(fgets($handle)); } fclose($handle); return $lines; } readTheFile("shakespeare.txt"); require "memory.php";
      
      





ここでは、シェークスピアの作品を含むファイルを読みます。 ファイルサイズは約5.5MBで、ピークメモリ使用量は12.8MBです。







そして、 ジェネレーターを使用しましょう:







 // from reading-files-line-by-line-2.php function readTheFile($path) { $handle = fopen($path, "r"); while(!feof($handle)) { yield trim(fgets($handle)); } fclose($handle); } readTheFile("shakespeare.txt"); require "memory.php";
      
      





ファイルは同じですが、ピークメモリ使用量は393 KBに減少します ! しかし、読み取り中のデータを使用して操作を実行していませんが、これは実用的ではありません。 たとえば、2つの空行に遭遇した場合、ドキュメントを断片に分割できます。







 // from reading-files-line-by-line-3.php $iterator = readTheFile("shakespeare.txt"); $buffer = ""; foreach ($iterator as $iteration) { preg_match("/\n{3}/", $buffer, $matches); if (count($matches)) { print "."; $buffer = ""; } else { $buffer .= $iteration . PHP_EOL; } } require "memory.php";
      
      





ドキュメントを1,216個に分割しましたが、使用したメモリは459KBのみでした 。 これらはすべて、ジェネレーターの機能のおかげです-作業用のメモリ量は、反復可能な最大部分のサイズに等しくなります。 この場合、最大部分は101,985文字で構成されます。







ジェネレーターは他の状況でも使用できますが、この例は大きなファイルを読み取る際の優れたパフォーマンスを示しています。 ジェネレーターは、データ処理の最適なオプションの1つです。







ファイル間のパイピング



データ処理が不要な状況では、あるファイルから別のファイルにデータを転送できます。 これはパイピングと呼ばれます(パイプ-パイプ。パイプ内で何が起こっているかはわかりませんが、パイプが出入りするのがわかります )。 これは、ストリーミング方式を使用して実行できます。 ただし、最初に、あるファイルから別のファイルにデータを愚かに転送する古典的なスクリプトを作成しましょう。







 // from piping-files-1.php file_put_contents( "piping-files-1.txt", file_get_contents("shakespeare.txt") ); require "memory.php";
      
      





当然のことながら、このスクリプトはコピーされたファイルが使用するよりも多くのメモリを使用します。 これは、ファイルが完全にコピーされるまで、ファイルの内容を読み取ってメモリに保存する必要があるためです。 小さなファイルの場合、これは大したことではありませんが、大規模なものではありません...







ファイルを1つずつストリーミング(またはパイプ)してみましょう。







 // from piping-files-2.php $handle1 = fopen("shakespeare.txt", "r"); $handle2 = fopen("piping-files-2.txt", "w"); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
      
      





コードはかなり奇妙です。 最初のファイルを読み取り、2番目のファイルを書き込みます。 次に、最初のファイルを2番目のファイルにコピーしてから、両方のファイルを閉じます。 驚きかもしれませんが、私たちはわずか393KBしか使いませんでした







おなじみの何か。 すべての行を読み取るジェネレーターのようではありませんか? これは、2番目のfgets



引数が読み取る各行のバイト数を決定するためです(デフォルトは-1、つまり行の最後)。 オプションで、3番目のstream_copy_to_stream stream_copy_to_stream



も同じことを行います。 stream_copy_to_stream



は、最初のストリームを一度に1行ずつ読み取り、2行目に書き込みます。







このテキストをパイプすることは、私たちにとって特に有用ではありません。 実際の例を考えてみましょう。 CDNから画像を取得して、ファイルまたはstdout



転送するとします。 次のようにできます。







 // from piping-files-3.php file_put_contents( "piping-files-3.jpeg", file_get_contents( "https://github.com/assertchris/uploads/raw/master/rick.jpg" ) ); // ...or write this straight to stdout, if we don't need the memory info require "memory.php";
      
      





この方法で計画を実装するには、 581KB必要でした 。 では、スレッドでも同じことを試してみましょう。







 // from piping-files-4.php $handle1 = fopen( "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" ); $handle2 = fopen( "piping-files-4.jpeg", "w" ); // ...or write this straight to stdout, if we don't need the memory info stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
      
      





同じ結果で、少し少ないメモリ( 400KB )を使用しました。 また、画像をメモリに保存する必要がない場合は、すぐにstdout



に保存できます。







 $handle1 = fopen( "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" ); $handle2 = fopen( "php://stdout", "w" ); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); // require "memory.php";
      
      





その他のスレッド



ストリーミングできるストリームは、他にもあります。









フィルター



使用できるチップがもう1つあります-これらはフィルターです。 内容を詳しく調べる必要なく、ストリームを少し制御できる中間オプション。 ファイルを圧縮したいとします。 zip拡張子を適用できます。







 // from filters-1.php $zip = new ZipArchive(); $filename = "filters-1.zip"; $zip->open($filename, ZipArchive::CREATE); $zip->addFromString("shakespeare.txt", file_get_contents("shakespeare.txt")); $zip->close(); require "memory.php";
      
      





良いコードですが、ほぼ11MBを消費します 。 フィルターを使用すると、次のようになります







 // from filters-2.php $handle1 = fopen( "php://filter/zlib.deflate/resource=shakespeare.txt", "r" ); $handle2 = fopen( "filters-2.deflated", "w" ); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
      
      





ここでは、 php://filter/zlib.deflate



を使用します。これは、着信データを読み取り、圧縮します。 圧縮データをファイルまたは他の場所にパイプで送ることができます。 このコードは896KBのみを使用しました







これは、zipアーカイブとまったく同じ形式ではないことを知っています。 しかし、別の圧縮形式を選択する機会があり、メモリを12分の1しか消費しない場合、それを考える価値がありますか?







データを解凍するには、別のzipフィルターを適用します。







 // from filters-2.php file_get_contents( "php://filter/zlib.inflate/resource=filters-2.deflated" );
      
      





スレッドのテーマを深く掘り下げたい人のための記事をいくつか紹介します:「 PHPのストリームを理解する 」と「 PHPストリームを効果的に使用する 」。







スレッドのカスタマイズ



fopen



file_get_contents



には多くの事前定義オプションがありますが、必要にfile_get_contents



て変更できます。 これを行うには、新しいスレッドコンテキストを作成します。







 // from creating-contexts-1.php $data = join("&", [ "twitter=assertchris", ]); $headers = join("\r\n", [ "Content-type: application/x-www-form-urlencoded", "Content-length: " . strlen($data), ]); $options = [ "http" => [ "method" => "POST", "header"=> $headers, "content" => $data, ], ]; $context = stream_content_create($options); $handle = fopen("http://example.com/register", "r", false, $context); $response = stream_get_contents($handle); fclose($handle);
      
      





この例では、APIに対してPOST



リクエストを作成しようとしています。 いくつかのヘッダーを作成し、ファイル記述子でAPIを参照します。 カスタマイズには他にも多くのオプションがありますので、この問題に関するドキュメントを熟知することは不必要ではありません。







独自のプロトコルとフィルターを作成する



終了する前に、カスタムプロトコルの作成について話しましょう。 ドキュメントを見ると、例を見ることができます:







 Protocol { public resource $context; public __construct ( void ) public __destruct ( void ) public bool dir_closedir ( void ) public bool dir_opendir ( string $path , int $options ) public string dir_readdir ( void ) public bool dir_rewinddir ( void ) public bool mkdir ( string $path , int $mode , int $options ) public bool rename ( string $path_from , string $path_to ) public bool rmdir ( string $path , int $options ) public resource stream_cast ( int $cast_as ) public void stream_close ( void ) public bool stream_eof ( void ) public bool stream_flush ( void ) public bool stream_lock ( int $operation ) public bool stream_metadata ( string $path , int $option , mixed $value ) public bool stream_open ( string $path , string $mode , int $options , string &$opened_path ) public string stream_read ( int $count ) public bool stream_seek ( int $offset , int $whence = SEEK_SET ) public bool stream_set_option ( int $option , int $arg1 , int $arg2 ) public array stream_stat ( void ) public int stream_tell ( void ) public bool stream_truncate ( int $new_size ) public int stream_write ( string $data ) public bool unlink ( string $path ) public array url_stat ( string $path , int $flags ) }
      
      





この実装を作成するには、別の記事を参照してください。 しかし、まだ困惑してそれをやるなら、ストリームのラッパーを簡単に登録できます:







 if (in_array("highlight-names", stream_get_wrappers())) { stream_wrapper_unregister("highlight-names"); } stream_wrapper_register("highlight-names", "HighlightNamesProtocol"); $highlighted = file_get_contents("highlight-names://story.txt");
      
      





同様に、カスタムフローフィルタを作成できます。 ドックからのフィルタークラスの例:







 Filter { public $filtername; public $params public int filter ( resource $in , resource $out , int &$consumed , bool $closing ) public void onClose ( void ) public bool onCreate ( void ) }
      
      





また、登録も簡単です。







 $handle = fopen("story.txt", "w+"); stream_filter_append($handle, "highlight-names", STREAM_FILTER_READ);
      
      





新しいフィルタークラスのfiltername



プロパティはhighlight-names



と等しくなければなりません。 インラインphp://filter/highligh-names/resource=story.txt



フィルターphp://filter/highligh-names/resource=story.txt



使用することもできます。 フィルターの作成は、プロトコルよりもはるかに簡単です。 ただし、プロトコルには、より柔軟なオプションと機能があります。 たとえば、フィルターが適切ではないがプロトコルが必要な理由の1つは、各データを処理するためにフィルターが必要なディレクトリの操作です。







独自のプロトコルとフィルターを作成することを強くお勧めします。 stream_copy_to_stream



関数にフィルターを適用すると、大量のデータを処理する際にメモリを大幅に節約できます。 イメージのサイズを変更するためのフィルター、または暗号化のためのフィルター、あるいはさらに突然のフィルターがあるとします。







まとめ



これは私たちが苦しんでいる最も一般的な問題ではありませんが、大きなファイルを操作するときは非常に簡単に台無しになります。 非同期アプリケーションでは、スクリプトでメモリ使用量を制御しない場合、一般的にサーバー全体を配置するのは非常に簡単です







このレッスンで、いくつかの新しいアイデアが得られた(またはメモリ内で更新された)ことで、大きなファイルをより効率的に操作できるようになることを願っています。 ジェネレーターとストリームに精通している(そしてfile_get_contents



ような関数の使用をやめている)ので、アプリケーションをfile_get_contents



クラスのエラーから救うことができます。 それは目指すべき良いことのように思えます!








All Articles