Webアプリケーションで画像やファイルをアップロードする機能はよくあります。
通常の画像ファイルであれば、それほど複雑なことを考えなくても実装できます。
しかし、CADデータなど数GBを超えるような大容量ファイルを扱う場合、同じ設計のままで本当に問題ないのでしょうか?
今回は大容量データをアップロードの実装で得た学びをブログにしました。
一般的なファイルアップロード
画像などの比較的小さなファイルであれば、multipart/form-data を使ってバックエンドへ送信する方法が一般的です。
Browser
│
│ multipart/form-data
▼
Backend API
│
│ ファイルを保存
▼
Amazon S3
バックエンドでは、multipart/form-data として送信されたリクエストからファイルを取得し、S3などのオブジェクトストレージへ保存します。
比較的小さなファイルであれば、このような構成でも大きな問題にはなりません。
大容量ファイルのアップロード
大容量ファイルでは何が問題になるのか
では、例えば5GBのCADデータをアップロードするとします。
先ほどと同じ設計なら、
Browser
│
│ 5GB
▼
WAF / CDN / Load Balancer
│
│ 5GB
▼
Backend
│
│ 5GB
▼
S3
となります。
ユーザーからバックエンドを経由してS3へ、という経路を巨大なデータが通過します。
ここからいくつかの問題が発生します。
1. バックエンドのリソースを消費する
実装方法によっては、アップロードされたファイルを一度メモリ上に展開してからS3へ送信することがあります。
1ファイルが5GBなら、それだけで大きな負荷です。複数ユーザーが同時にアップロードすれば、影響はさらに大きくなります。
ストリーミング処理を利用すれば、ファイル全体をメモリへ載せる必要はありません。それでも、巨大な通信をアプリケーションサーバーが中継すること自体が、ネットワーク帯域、接続数、CPUなどのリソースを消費します。
2. タイムアウトやサイズ制限の影響を受ける
大容量ファイルはアップロード完了まで時間がかかります。
ブラウザとバックエンドの間には、CDN、WAF、Load Balancer など複数のサービスが挟まることがあります。それぞれについて、
* リクエストサイズ制限
* タイムアウト
* 接続維持時間
などを確認する必要があります。
バックエンドの実装だけが大容量ファイルに対応していればよい、というわけではありません。
3. バックエンドがファイルの中継地点になる
本来バックエンドが担当したいのは、
* 認証
* 認可
* DB更新
* ビジネスロジック
などです。
大容量ファイルをバックエンド経由でアップロードすると、巨大なデータをS3へ転送する役割まで担当することになります。
言ってしまえば、5GBのデータを右から左へ流しているだけの処理に、バックエンドのリソースを使うことになります。
ここで、実データはブラウザから直接S3へ送ればよいのでは、という考えが出てきます。
4. 経路上のサービスごとに料金がかかる
大容量ファイルでは、そのデータがどのサービスを通過するのかも見ます。
Browser
│
▼
CloudFront
│
▼
WAF
│
▼
ALB
│
▼
ECS / EC2
│
▼
NAT Gateway
│
▼
S3
CloudFront、WAF、ALB、NAT Gateway は、それぞれ別の料金がかかります。S3に保存する料金だけを見ればよいわけではありません。
付き方はサービスごとに違います。データ量で効きやすいのは NAT Gateway です。S3へ出す通信は Gateway 型の VPC Endpoint にすれば、NAT Gateway を経由せずに済みます。
どのようなアプローチがいいか?
Presigned URLで直接送る
巨大な実データをバックエンドへ送らない方法として、S3の Presigned URL があります。
① アップロード許可を要求
Browser
│
│ 「ファイルをアップロードしたい」
▼
Backend
│
│ 認証・認可
▼
Presigned URLを発行
② 実際のファイルをアップロード
Browser
│
│ ファイル本体
▼
S3
バックエンドが担当するのは、このユーザーがこのファイルをアップロードしてよいか、という制御です。許可できれば Presigned URL を発行します。
実データはブラウザからS3へ直接送ります。
制御の通信は Browser → Backend、データの通信は Browser → S3、と分けられます。大容量データをバックエンドやその手前のサービスに通す必要がなくなります。
ただし、S3へ1リクエストで送れるサイズには上限があります。単一PUTは5GBまでです。5GBを超えるファイルは、このままでは送れません。
Multipart Uploadに関して
そこで使うのが Multipart Upload です。ファイルをパートに分け、パートごとに Presigned URL を使って送ります。
10GB
↓ 分割
Part 1 ──→ S3
Part 2 ──→ S3
Part 3 ──→ S3
...
途中で失敗しても、失敗したパートだけ再送できます。パートを並列に送れば、速くなる場合もあります。
数GBのファイルなら、Presigned URL と Multipart Upload を組み合わせるのが現実的です。
⸻
バックエンドは完了を観測できない
直送にすると、別の問題が出ます。バックエンドは、アップロードが終わったことを自分では知りません。
Presigned URL を発行した時点で分かるのは、このユーザーがこのキーへアップロードしてよい、ということまでです。S3へ届いたか、途中で切れたかは、発行したリクエストの中では分かりません。
完了を知るには、別の経路が必要です。ブラウザから「アップロードし終わった」と通知してもらうか、S3のイベントで検知するか、です。
Multipart Upload では、パートを送っただけではオブジェクトは確定しません。すべてのパートが揃ったあと、完了要求が必要です。この完了をバックエンド経由にすれば、アップロード完了の把握にも使えます。
直送は、データを中継する役割をやめる代わりに、完了の観測を別の経路で取り戻す設計になります。
まとめ
小さなファイルなら、ブラウザからバックエンドを経由してS3へ保存する構成で十分な場合があります。
ファイルが大きくなると、その経路自体を疑います。バックエンドのリソース、途中のタイムアウトやサイズ制限、経路上のサービスごとの料金が、フォームアップロードのままでは効いてきます。S3へ1リクエストで送れるサイズにも、5GBという上限があります。
選択肢のひとつは、バックエンドが認証・認可と URL の発行を担当し、実データは Presigned URL でS3へ直接送ることです。5GBを超える場合は Multipart Upload と組み合わせます。
その代わり、バックエンドは完了を自分では観測できません。届いたかどうかは、クライアントの通知かS3のイベントで別途知る必要があります。
大容量ファイルを扱うときは、「アップロードできるか」だけではなく、そのデータを誰が受け取り、どのサービスを通り、どこまで再送でき、完了を誰が知るのかまで考えて設計します。