Webサービスに何かしらのファイルをアップロードしたい場面は多い。需要がある処理なのでやりようは幾らでもある。
幾らでもあると逆にどうするのがいいのか迷ったりする。
そんなわけでどのようなファイルアップロードの構成があるのか整理しておく。
前提条件
要件はざっくり以下のようなもので考える。
- ユーザーは任意のファイルをWebサービスにアップロードできる
- ユーザーはアップロードが完了したことを知ることができる
- Webサービスはアップロードされたファイルを公に提供したり、限られたユーザーに提供したりする
- Webサービスはアップロードされたファイルに何かしらの保存前処理を行う
今回はファイルのアップロードに関心を置くので、登場人物のうちDBはあまり気にしない。だれがどんなファイルをアップロードしたのか? などを保存する場所であればよい。ファイルシステム上のCSVファイルだったり、RDBMSだったりするのだろう。
整理した構成
- レンタルサーバ、1サーバ時代
- オブジェクトストレージ直列
- オブジェクトストレージアップロードだけ切り出し
- オブジェクトストレージイベント伝播
古代と後クラウドという感じだが、その中間もフォローするとパターン数が爆発するので忘れることにした。
レンタルサーバ、1サーバ時代
サービスが1つのサーバで完結している時代は、単にそのサーバにファイルを送り付けて、サーバは自分のローカルファイルシステムにファイルを配置すればいい。後は自分の手元にあるファイルをいかようにも処理できる。
sequenceDiagram title レンタルサーバ、1サーバ時代 actor User participant WebServer participant CGI Script participant File Storage participant DB note over User, WebServer: Upload User->>WebServer: Post file WebServer->>CGI Script: Execute CGI Script->>CGI Script: Process file CGI Script->>File Storage: Write file CGI Script->>DB: Save data CGI Script->>WebServer: End WebServer->>User: Complete upload note over User, WebServer: Download(public) User->>WebServer: Get public file WebServer->>File Storage: Read file WebServer->>User: file note over User, WebServer: Download(private) User->>WebServer: Get private file WebServer->>CGI Script: Execute CGI Script->>DB: Check permission CGI Script->>File Storage: Read file CGI Script->>WebServer: file WebServer->>User: file
オブジェクトストレージ直列
ファイルの保存先をS3などのオブジェクトストレージにする場合でも、アップロード時の保存先を単に置き換えることで処理できる。
参照も同様に行うこともできるが、オブジェクトストレージにはいい感じにファイル配信するための機能が設けられているので、それを利用することでアプリケーションの負荷(特に占有時間)を下げることができる。
sequenceDiagram title オブジェクトストレージ直列 actor User participant Proxy participant Application participant Object Storage participant DB note over User, Proxy: Upload User->>Proxy: Post file Proxy->>Application: Execute Application->>Application: Process file Application->>Object Storage: Put file Application->>DB: Save data Application->>Proxy: End Proxy->>User: Complete upload note over User, Proxy: Download(public) User->>Object Storage: Get public file Object Storage->>User: file note over User, Proxy: Download(private) 1 User->>Proxy: Get private file Proxy->>Application: Execute Application->>DB: Check permission Application->>Application: Generate signed URL Application->>Proxy: signed URL Proxy->>User: signed URL User->>Object Storage: Get signed URL Object Storage->>User: file note over User, Proxy: Download(private) 2 User->>Proxy: Get private file Proxy->>Application: Execute Application->>DB: Check permission Application->>Application: Generate signed URL Application->>Proxy: signed URL Proxy->>Object Storage: Get signed URL Proxy->>User: file
オブジェクトストレージアップロードだけ切り出し
オブジェクトストレージにはいい感じにファイルをアップロードするための機能が設けられていることが多いので、取り合えずそれを使おうとするとこんな感じになる。
ダウンロードは先の例と変わらないので省いた。
が、これはあんまりよくない構成だと思う。
ファイルアップロードの処理だけをアプリケーションから切り出しても、結局はユーザーからのリクエストに対して同期的にファイルを処理している。単に実装が複雑化しただけで得られるものが無い。
sequenceDiagram title オブジェクトストレージアップロードだけ切り出し actor User participant Proxy participant Application participant Object Storage participant DB note over User, Proxy: Upload User->>Proxy: Post upload file URL Proxy->>Application: Execute Application->>DB: Save data(uploading) Application->>Application: Generate signed URL Application->>Proxy: signed URL Proxy->>User: signed URL User->>Object Storage: Put file to signed URL User->>Proxy: Post upload result Proxy->>Application: Execute Application->>Object Storage: Get file Application->>Application: Process file Application->>Object Storage: Put file Application->>DB: Save data(complete) Application->>Proxy: End Proxy->>User: Complete upload
オブジェクトストレージイベント伝播
オブジェクトストレージにはいい感じにファイルをアップロードするための機能に加えて、アップロードされたことを他の処理に伝えるための機能も設けられていることが多いので、それを活用してファイル保存の前処理などをイベント処理してやる。
クラウドプロバイダーが提示するいわゆるベストプラクティスはこんな形だろうと思う。
ポイントは、ファイルのアップロードやその処理という時間がかかることをやる際に、ユーザーとアプリケーションが直接つながっていない点にあるのだろうか。これによってサービスにかかる負荷が適切に分散されてスケールしやすくなるのだろう。
sequenceDiagram title オブジェクトストレージイベント伝播 actor User participant Proxy participant Application participant Object Storage participant DB note over User, Proxy: Upload User->>Proxy: Post upload file URL Proxy->>Application: Execute Application->>DB: Save data(uploading) Application->>Application: Generate signed URL Application->>Proxy: signed URL Proxy->>User: signed URL User->>Object Storage: Put file to signed URL Object Storage-)EventSystem: file created EventSystem-)Application: file created Application->>Object Storage: Get file Application->>Application: Process file Application->>Object Storage: Put file Application->>DB: Save data(complete) loop while complete upload User->>Proxy: Get upload status Proxy->>Application: Execute Application->>DB: Check status Application->>Proxy: status Proxy->>User: status end
整理まとめ
整理とは言ったものの、今回自分の中での結論は出ている状態でパターンを出していた。
実装をシンプルにしたいなら「オブジェクトストレージ直列」の構成をとる。
サービスが大規模になって多量のファイルを処理しなくてはならないことが見込まれるなら「オブジェクトストレージイベント伝播」の構成をとってスケールしやすくする。
「レンタルサーバ、1サーバ時代」は過去を懐かしみたかっただけ*1で、「オブジェクトストレージアップロードだけ切り出し」に関しては「中途半端な妥協でこういう構成をとらないようにしたい」という気持ちを新たにするために作図した形になる。
正直、イベント伝播でファイルを処理していくのは難しい。登場人物が多く*2、一見よく分からん線がシーケンス上を飛び交っている。これをサッと理解できる人はいい感じのITエンジニアであると自信を持って欲しい。
大多数のWebサービスはそんなにシステムへの負荷なんてかからないので、できる限りシンプルな構成をとって関わる人にやさしくするのがいい場面が多いのではないかと思う。
この気持ちを整理したかった次第。