見出し画像

大容量ファイルのアップロードってどうやるの?【署名付きURL×マルチパートアップロード】

こんにちは。メディア研究開発センター(M研)の新美です。
最近イベント用のグッズでTシャツを購入しましたが、肝心のイベントのチケットが外れて日の目を浴びられないことが確定したため少しやさぐれております。

さて、そんなやさぐれ女が今回ご紹介するのは署名付きURLを用いた大容量ファイルのアップロード方法についてです。私が開発に携わっている社内向け文字起こしサービス「YOLO」では、長時間の録音ファイルや動画ファイルなど大容量のファイルのアップロードが求められるようになってきました。
そこで従来採用していたアップロード方法を見直し、より大容量のファイルを高速にアップロードできる手法を検討しました。詳細な実装方法や速度比較の結果について、以下で詳しく解説していきます。

YOLOについての詳細は下記の記事をご覧ください。


従来のアップロード方法

現行のYOLOでは、フロントエンドから実体ファイルを含んだAPIリクエストをバックエンドに送信し、バックエンドがS3へアップロードを行っていました。しかしこの方法ではアップロードに時間がかかり、大容量ファイルのアップロードには適さないためアップロード可能な最大ファイル容量を2GBに制限して運用していました。

そこで今回はアップロード時間を短縮し、アップロード可能な最大ファイル容量を20GBまで拡張できるようにマルチパートアップロードを検討しました。

マルチパートアップロード

Amazon S3では、単一のオブジェクトを分割して送信できるようにするマルチパートアップロードと呼ばれる機能があります。マルチパートアップロードを使用すると、PutObjectの制限を受けることなく最大 5 TB サイズの単一の大容量オブジェクトをアップロードすることができます。

メリット

マルチパートアップロードを用いるメリットは下記が挙げられます。

  • スループットの向上

  • ネットワークエラーの影響を最小限に抑えることが可能

  • アップロードの一時停止と再開が可能

  • オブジェクトの最終的なサイズが不明な状態でもアップロード開始可能

アップロードの流れ

マルチパートアップロードの主な流れは下記の通りです。

  1. マルチパートアップロードの開始

  2. 分割されたファイルのアップロード

  3. マルチパートアップロードの終了

まず初めにマルチパートアップロードの開始通知をリクエストすると、アップロードIDを含むレスポンスがS3から返されます。このIDはアップロードやリストの取得、アップロードの完了・停止を行う際に必要となります。
次に取得したIDとパート番号を指定して分割されたファイルのアップロードを行い、完了通知をリクエストしアップロードは終了となります。

新しいアップロード方法

実際にはこのような流れでマルチパートアップロードを実現しました。

  1. ファイルバリデーション、ファイル分割処理を行う

  2. 分割したファイルのパート数を元にマルチパートアップロードの開始通知を行う

  3. アップロードID、パート数分の署名付きURLを発行する

  4. マルチパートアップロードを行う

  5. Etagを取得する

  6. アップロード成功/失敗通知を行う

まず初めにフロントでファイルのバリデーションと分割処理を行います。この時アップロードあたりの最大パート数が10,000であることに注意してパートサイズを決定します。1パートあたりのサイズ上限は5MiB〜5GiBです。(最後のパートには最小サイズの制限なし)

/* ファイル分割 */

export const generateChunks = async (file: File, chunkSizeInBytes: number): Promise<Chunk[]> => {
  const allSize = file.size;
  const chunkIndices = Array.from({ length: Math.ceil(allSize / chunkSizeInBytes) }, (_, index) => index);

  const chunks: Promise<Chunk>[] = chunkIndices.map(async (index) => {
    const rangeStart = index * chunkSizeInBytes;
    const rangeEnd = Math.min(rangeStart + chunkSizeInBytes, allSize);
    const blobChunk = file.slice(rangeStart, rangeEnd);

    return new Promise<Chunk>((resolve) => {
      const fileReader = new FileReader();
      fileReader.onload = (event) => {
        const data = event.target!.result;
        if (data instanceof ArrayBuffer) {
          resolve({ index, data: new Uint8Array(data) });
        }
      };
      fileReader.readAsArrayBuffer(blobChunk);
    });
  });

  return Promise.all(chunks);
};

次にバックエンドでマルチパートアップロードの開始通知を行い、アップロードIDの取得と各パートごとの署名付きURLの発行を行います。

# アップロードIDの取得

        response = s3.create_multipart_upload(
            Bucket=BUCKET_NAME, Key=key_name, ContentType="multipart/form-data"
        )
        upload_id = response["UploadId"]

アップロードIDと署名付きURLを用いてフロントからマルチパートアップロードを実行し、パートのアップロードごとにS3から返却されるエンティティタグ(ETag)を取得します。

/* 分割したデータをアップロード */    
const resp = await axios.put(signedUrl, sendData, {
       headers: {
         'Access-Control-Allow-Origin': '*',
         'Access-Control-Allow-Headers': '*',
         'Content-Type': 'multipart/form-data',
       },
     });

     partETags[index] = resp.headers.etag;
}

最後にアップロードIDとETagを用いてアップロード完了通知を行います。成功なら1ファイルとして集約されてS3に保存され、失敗なら途中まで送られてきた分割ファイルを削除します。
これでマルチパートアップロードは完了です。

アップロード時間の比較

現行の手法と今回新たに実装した手法でどのくらいアップロード時間が短縮されたのか比較してみました。

従来の方法でアップロードした結果
マルチパートアップロードの結果

従来の方法でアップロード可能なファイル容量が2GBのため上記のデータサイズでの比較となりましたが、いずれもアップロード時間が大幅に短縮されることが確認できました。また複数ファイル同時にアップロードすると更に短縮率が上がり、従来の方法ではアップロードできなかった2GB以上のファイルもアップロードすることができました!

ファイル容量が大きくなるにつれてファイル分割に時間がかかるため、分割処理の時間短縮を図るなどまだまだ改善点はありますが、マルチパートアップロードを組み込んだ新しいアップロード方法を採用していこうと思います。

おわりに

今回は大容量ファイルのアップロード方法について検証し、従来の方法よりもはるかにアップロード時間の短縮と容量上限の増加を実現することができました。
今後はよりユーザー体験を向上できるよう更なる処理時間の短縮や、中断箇所から再開可能なアップロード機能などを実現できるように調査・実装を進めていきたいと思います。

(メディア研究開発センター・新美茜)