[PHP] AWS S3から大容量ファイルをダウンロードする方法とメモリ使用量
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;
結果
方法 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 にしてダウンロードする。