[PHP] AWS S3から大容量ファイルをダウンロードする方法とメモリ使用量

PHPAWS

AWS SDK for PHP の公式ドキュメントにある getObject を使って 1G のファイルをダウンロードしようとしたらメモリ不足でできなかった。

AWS だと期限付きのダウンロード用 URL を発行し、 URL にアクセスするやり方があるようだけど、 Amazon S3 ストリームラッパーを使ってストリーミングでダウンロードさせる方法を使ってみた。
また getObject を使った場合と、ストリーミングする場合のメモリ使用量を計測した。

スポンサーリンク

エラーになるダウンロード方法

公式ドキュメント AWS SDK for PHP を使用したオブジェクトの取得 に載っている getObject メソッドを使った方法。

$bucket = 'BUCKET-NAME';
$keyname = 'KEY';
$filename = 'FILE-NAME';

$result = client->getObject([
    'Bucket' => $bucket,
    'Key'    => $keyname,
    'SaveAs' => filename
]);

header("Content-Type: {$result['ContentType']}");
header("Content-Length: {$result['ContentLength']}");
header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($filename));
echo $result['Body'];

900 MB のファイルをダウンロードしようとすると Out of memory エラーが発生する。
小さいサイズだとダウンロードできるが、サーバーにもファイルが保存される。。。

大容量ファイルのダウンロード方法

AWS SDK for PHP バージョン 3 は、 Amazon S3 ストリームラッパー というものを持っていて、 PHP の関数を使って Amazon S3 のデータを操作することができるようになる。

ファイルサイズを取ってくる関数はあるが、 Content-Type を取得する方法がわからなかったので 'application/octet-stream' を使用している。

Amazon S3 ストリームラッパ と fopen & fclose でダウンロード

公式ドキュメント AWS SDK for PHP バージョン 3 での Amazon S3 ストリームラッパー に載っている方法。

$client = new S3Client([
     'version' => 'latest',
     'region'  => 'us-east-1'
 ]);
 // Amazon S3 ストリームラッパーを登録
 $client->registerStreamWrapper();

 $key = "s3://BUCKET-NAME/KEY";
 $filename = 'FILE-NAME';
 $size = filesize($key);

 header('Content-Type: application/octet-stream');
 header('Content-Length: ' . $size);
 header('Content-Disposition: attachment; filename="'.rawurlencode($filename).'"');

 if ($stream = fopen($key, 'r')) {
     while (!feof($stream) and (connection_status() == 0)) {
         // Read 1,024 bytes from the stream
         echo fread($stream, 1024);
         ob_flush();
         flush();
     }
     ob_flush();
     fclose($stream);
 }
 ob_end_clean();
 exit;

Amazon S3 ストリームラッパ と readfile() でダウンロード

【PHP】正しいダウンロード処理の書き方 を参照した。

$client = new S3Client([
    'version' => 'latest',
    'region'  => 'us-east-1'
]);
// Amazon S3 ストリームラッパーを登録
$client->registerStreamWrapper();

$key = "s3://BUCKET-NAME/KEY";
$filename = 'FILE-NAME';
$size = filesize($key);

header('Content-Type: application/octet-stream');
header('Content-Length: ' . $size);
header('Content-Disposition: attachment; filename="'.rawurlencode($filename).'"');

while (ob_get_level()) { ob_end_clean(); }
readfile($key);
exit;

メモリ使用量を計測

「【PHP】正しいダウンロード処理の書き方」の投稿では、「 readfile() はメモリ不足になる。」という説が出回っているが、それは間違いであり、大容量ファイルの読み込みに使っても問題ないとある。なので 3 つの方法のメモリ使用量と最大使用量を計測した。

方法

計測するダウンロード方法
方法 1. getObject
方法 2. S3 ストリームラッパ と fopen & fclose
方法 3. S3 ストリームラッパ と readfile

PHP の設定
memory_limit = 1024M

計測には memory_get_usage (メモリ使用量)、 memory_get_peak_usage (メモリ最大使用量)を使った。 単位はバイト( Byte )

コード
// S3Client のインスタンス作成とか

$mem = memory_get_usage(FALSE);
file_put_contents("testlog.txt","\nStart : ${mem}\n",FILE_APPEND);

/*** オブジェクトの取得とダウンロード処理 ***/

$mem = memory_get_usage(FALSE);
$peak = memory_get_peak_usage();
file_put_contents("testlog.txt","End : ${mem}\npeak : ${peak}\n",FILE_APPEND);
exit;

結果

<30 MB のファイルをダウンロード>
方法 1
Start : 5282944 (5.0 MB)
End : 38455272 ( 36.7 MB)
peak :  70965280 ( 67.7 MB)

方法 2
Start : 5391328 (5.1 MB)
End : 6090472 (5.8 MB)
peak : 7147656 (6.8 MB)

方法 3
Start : 5390240 (5.1 MB)
End : 6089352 (5.8 MB)
peak : 7138320 (6.8 MB)
<100 MB のファイルをダウンロード>
方法 1
Start : 5281088 (5.0 MB)
End : 110804840 (105.7 MB)
peak : 215666592 ( 205.6 MB)

方法 2
Start : 5389960 (5.1 MB)
End : 6088768 (5.8 MB)
peak : 7145864 (6.8 MB)

方法 3
Start : 5389224 (5.1 MB)
End : 6088008 (5.8 MB)
peak : 7136888 (6.8 MB)
<500 MB のファイルをダウンロード>
方法 1
エラーでもはやダウンロード不可
PHP Fatal error:  Out of memory (allocated 528486400) (tried to allocate 524292096 bytes) in ...

方法 2
Start : 5391280 (5.1 MB)
End : 6090048 (5.8 MB)
peak : 7147144 (6.8 MB)

方法 3
Start : 5390544 (5.1 MB)
End : 6089288 (5.8 MB)
peak : 7138168 (6.8 MB)

結論

方法 1 は、ダウンロードするファイルサイズの 2 倍以上のメモリを消費する。ファイルの中身全部がメモリにロードされるようだ。

方法 2 と方法 3 のメモリ消費量は変わらない。( readfile を使ってもメモリを大量消費しない。)そして、ファイルサイズが大きくなっても消費メモリが変わらない。
コードが 2 行で済むので、方法 3 を使う。

まとめ

getObject を使うと、ファイルサイズの倍以上のメモリを消費するため、大容量ファイルのダウンロードには向かない。
AWS S3 から大容量ファイルをダウンロートするなら、 Amazon S3 ストリームラッパ を使って readfile を使う。
これでもメモリ不足になるようであれば、 ZIP にしてダウンロードする。

Posted by Agopeanuts