koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

ファイルアップロードのパターン整理

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サービスはそんなにシステムへの負荷なんてかからないので、できる限りシンプルな構成をとって関わる人にやさしくするのがいい場面が多いのではないかと思う。

この気持ちを整理したかった次第。

*1:とは言え、現代でもCMSを1サーバで運用などでは採られる構成だろうとは思う

*2:作図をシンプルにするためにアプリケーションは1つしか配置していないが、実際にはWebアプリケーションと関数アプリケーションで別れることになるはず