S3のGETリクエストでRangeヘッダーを使う

S3のGETリクエストでRangeヘッダーを使う

  • Post Author:

久しぶりの投稿です。

先日とあるプロジェクトで、Amazon S3に保存されているCSVファイルをデータベースにインポートする機能をPHPで作りました。
PHPではCSVを fopen と言う関数を使う事で、ストリームからCSVの行を1行ずつ配列として受け取る事ができます。

さて、今回要件として、CSVファイルは最大数GBまで許容されると言う事と、ジョブは並列で動作するのでローカルディスクにこのデータ全体を一旦DLするのはNG [1] と言う条件がありました。
ローカルディスク上に保存されているファイルであれば fopen で開いたストリームを使って問題なく fgetcsv が処理できますが、このような事情の為、何か良い方法が無いか調べた所、S3のGETリクエストは  Range (RFC 2616) が使える事が分かりました。

これは、HTTPのリクエストヘッダーに Content-Range: bytes 0-100000 の様に指定すると、 先頭の100KBまでのチャンクをダウンロードできると言う物です。
これを使う事で、部分的にCSVのデータを取得してそれをパースしていけば、メモリもディスクスペースも使わずにインポートが実現できます。

因みに、Content-Rangeに指定する値のフォーマットは色々あるので詳しくは RFC 2616 を参照の事。

実際に使ってみる

今回はPHPで書いているので、aws-sdk-php を使います。
実はContent−Rangeの指定には特殊な処理は必要なく、単純に GetObject のoptions にRange として渡せばOKです。

$object = $s3->getObject(['Bucket' => 'test', 'Key' => 'awesome.csv', 'Range' => sprintf('bytes=%s-%s', 0, 1024 * 1024 * 10)]));

echo $object->__toString(); // CSVの最初の10MBを取得

後は、チャンクごとにCSVを読み込んでいく訳ですが、これにはちょっとした下処理が必要で、1つのチャンクで完全なCSV行が読み取れない場合があるので、その場合はそのチャンクをバッファリングしておいて次のチャンクと連結して再度パースする必要がある為です。

詳細は長くなるので割愛しますが、GitHubにサンプルコードを用意したので興味のある方は見てみて下さい。
そこでは最後の3行でCSV行をループで検証していますが、1GB超の巨大なCSVファイルであってもメモリは12MB前後しか消費されず、検証は成功と言えます。

foreach ($csvRows as $v) {
    echo \sprintf('%s, %s / %.4f MB', $v[0], $v[4], \memory_get_usage(true) / 1024 / 1024), "\n";
}

  1. AWS SDK for PHPのS3 GetObject のBodyはPSR-7のStreamで表現されていますが、実態は php://temp なので、ファイルサイズがでかいとディスク容量を圧迫します ↩︎
we are hiring

優秀な技術者と一緒に、好きな場所で働きませんか

株式会社もばらぶでは、優秀で意欲に溢れる方を常に求めています。働く場所は自由、働く時間も柔軟に選択可能です。

現在、以下の職種を募集中です。ご興味のある方は、リンク先をご参照下さい。

コメントを残す