Open1

[Ruby] AWS S3 で大容量ファイルを扱う

nmakinmaki

本記事について

rubyコードからAWSのS3にファイルを書き出したいけど、大容量だと失敗してしまう・・・身近にそんなケースがありました。公式ドキュメントなどを調べると、MultipartUploadという方法が推奨されており、その手順はかなり煩雑なものでした。

時間をかけて調べると、MultipartUploadを簡単に実装できる方法がありました。
大事なデータを取り扱うケースだったため、最終的にはS3のライブラリソースも読むことになり、ドキュメントに記載されていない重要な情報もいくつか発見しました。

本記事では、得られた情報をもとに、AWSのrubyライブラリ(sdk-for-ryby/v3)を使用して大容量ファイルのアップロードする方法について紹介します。

結論(急ぎの方向け)

10MBを超えたら5MBずつ分割して、同時3スレッドでアップロードする場合は、以下のようにします。

options = { multipart_threshold: 10.megabytes, thread_count: 3 }
bucket = Aws::S3::Bucket.new("S3で作成したバケット名")
bucket.object("配置先ファイル名").upload_file("ローカルファイル", options)

以下では、何故こうしておくか説明します。

NGコード例

改善前のコードです。ファイルの容量を確認せず、以下のような処理にしていました。

body = File.read("ローカルファイル")
S3Client.put_object(
  bucket: "S3で作成したバケット名",
  key: "配置先ファイル名",
  body: body
)
  • 全てbody変数(メモリ)に載せている
  • put_objectを使い一括で書き出している

十分に軽量なファイルであれば問題のないコードですが、AWSの公式では100MBを超えるファイルについてMultipartUploadが推奨されています。

MultipartUpload を直接使う方法

推奨されているMultipartUploadですが、以下の4つを組み合わせて実装する必要があります。
MultipartUploadオブジェクトをcreateし、パート毎にupload_partを呼び出し、成否に応じてMultipartUploadオブジェクトをcompleteまたはabortする流れになります。キャッシュ効率やネットワーク使用状況の観点でスレッドごとに可変なパートサイズで最適化したい場合などは自前実装していくことになりますが、単にアップロードを安定させたい場合はお勧めできません。

https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#create_multipart_upload-instance_method
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#upload_part-instance_method
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#complete_multipart_upload-instance_method
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#abort_multipart_upload-instance_method

MultipartUploadを間接的に使う方法=Object.upload_fileを使う

冒頭で紹介したメソッドを使います。ファイルサイズが閾値のサイズを上回ったときに、自動でMultipartUploadを作成して、完了までやってくれます。
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#upload_file-instance_method

ドキュメントに記載はありませんが、MultipartUploadになった場合は基本的に1スレッドにつき5MBずつアップロードされます。対象ファイルが50GBを超える場合、対象ファイルのサイズ/10,000が1スレッドごとのサイズになります。すなわち、パート数の上限は10,000です。

スレッド間の排他はmutexを使い実現しているようです。当然期待することですが、実行中のスレッドのみファイル読み込みで5MB * スレッド数分のメモリを消費します。

Object.upload_file のオプション

  • multipart_threshold: 分割アップロードに振り分ける閾値を低めにする
    デフォルトでは100MBに設定されています。実行環境のネットワーク設備によりますが、かなり大きな値です。実行環境とS3の物理距離やネットワークの実情に合わせて現実的なサイズを設定しましょう。

  • thread_count: スレッド数を控えめにする
    デフォルトでは10並列となっています。この設定値もネットワークリソースによりますが、一般的に見ると大きな数値のようです。実行環境とS3の両方で保証されているコネクション数や負荷率において、それらを圧迫しない設定にしましょう。

まとめ

sdk-for-ryby/v3を用いて大きなファイルを安全にアップロードする方法について検討した内容を紹介しました。デフォルトの設定に比べて速度面では遅くなってしまいますが、リソース消費を抑えて成功率の高いアップロードを実現したいケースではおすすめの方法です。アップロードが安定しない場合にお試しください。