🆓

画像アップロードサービスの設計

要件

機能要件

  • ユーザーはブラウザ/モバイルアプリから画像ファイルをアップロードできる
  • アップロードした画像は多様なサイズにリサイズされ変換される
  • 元画像と変換された画像のURLは画像のIDを用いて取得できる
  • 取得したURLから画像を高速に配信できる

非機能要件

  • 高スケーラビリティ、特に画像変換
  • 高信頼なサービス、画像データは失われてはいけない
  • 画像配信は低レイテンシ、全世界のユーザーから利用される
  • 高可用なサービス

想定質問

❓️画像のファイル形式は?
→ 基本的にjpegとpngを想定すればいい
❓️多様なサイズとはどのくらいバリエーションがある?
→ 3種類
❓️アップロードする画像の上限のサイズなどはある?
→ サイズの上限は5MB
❓️画像アップロードリクエストのリクエスト数はどのくらいを想定すればよい?
→ ピーク時には毎秒100件程度だが、普段はそこまでのリクエストは来ない

データモデル

Users
  • id: ユニークID
  • name: ユーザー名
💡
ユーザーに関する要件はあまりないのでシンプルな構造にしておきます。
Images
  • id: ユニークID
  • userId: ユーザーID
  • imagePath: 画像のパス
  • width: 画像の横の長さ
  • height: 画像の縦の長さ
  • status: ドラフト、処理中、完了、失敗
💡
画像データで最も大事なのは画像のパスの情報です、imagePathはストレージへのパスのデータを保持しておきます。S3などのオブジェクトストレージに保存している場合、キーの名前が入ります。サイズ(widthheight)は画像のメタデータから取得も可能ですが、クライアント表示時に必要なデータなのでデータとして保持しておきます。
ImageVariants
  • id: ユニークID
  • originalImageId: 元画像のID
  • imagePath: 画像のパス
  • width: 画像の横の長さ
  • height: 画像の縦の長さ
  • status: ドラフト、処理中、完了、失敗
💡
派生画像(サムネイルやリサイズ済み画像)専用のスキーマ。originalImageId で元画像にひもづけることで、1 枚のオリジナルに対して複数サイズを柔軟に管理できます。statusは画像処理の状態を保持しておきます。imagePathwidthheight

API

画像をアップロードする

アプリケーションサーバーがmultipart/form-dataを受け取り、オブジェクトストレージへ転送する。完了後に画像メタデータを登録し、レスポンスを返す。
POST /images Content-Type: multipart/form-data
リクエストパラメータ
  • file: 画像バイナリ
  • fileName: 元ファイル名(任意)
💡
Pre-Signed URLを使う手法もあります。今回はアプリケーションサーバー経由で画像を転送しましたが、Pre-Signed URLを用いるとクライアント側から直接ファイルをオブジェクトストレージに転送できます。

画像メタデータと派生画像を取得する

指定した imageId に対するオリジナル画像と派生画像(ImageVariants)の URL を返します。
GET /images/{imageId}
リクエストパラメータ
  • imageId: 画像のID
レスポンス
{ "id": "uuid-1234", "userId": "user_f7b3c4", "url": "https://interviewcat.com/uuid-1234/img_s2ds.jpg", "width": 4000, "height": 3000, "variants": [ { "id": "uuid-sus1", "url": "https://interviewcat.com/uuid-sus1/img_200x150_d8f1.jpg", "width": 200, "height": 150 }, { "id": "uuid-1f3i", "url": "https://interviewcat.com/uuid-1f3i/img_800x600_5e91.jpg", "width": 800, "height": 600 }, { "id": "uuid-9ks7", "url": "https://interviewcat.com/uuid-9ks7/img_1920x1080_s892.jpg", "width": 1920, "height": 1080 } ] }

アーキテクチャ図

  1. クライアントはUpload Serviceへmultipartで画像を送信
  1. ロードバランサーはリクエストをアプリケーションサーバーへ振り分ける
  1. Upload Serviceはmultipartのデータ(オリジナル画像)をそのままオブジェクトストレージへ転送し、メタデータ(Images)をDBに保存
  1. 同時に「画像変換リクエスト」をWorkerへ転送
  1. 画像変換Workerが各種サイズに変換しストレージへ保存、ImageVariantsもDBに保存
  1. クライアントはImage Metadata Serviceで画像のメタデータ(Images & ImageVariants)を取得し、CDN経由で画像をダウンロードする
  1. CDNはストレージをオリジンとしてキャッシュし、世界中へ低レイテンシ配信
notion image

設計の深堀りについて

1. 画像変換のスケーラビリティと非同期処理について

非機能要件には画像変換処理のスケーラビリティについて書かれています。画像変換はCPUやメモリを多く消費する計算負荷の高いジョブであり、画像をアップロードを受け付けるアプリケーションと同じサーバーで処理を行うとリソースが大きく消費され、結果的にシステム全体の可用性やスケーラビリティに影響を与えます。そこで画像変換処理を分離し、別サーバーで非同期処理を行う仕組みが必須でしょう。
非同期化により、Webアプリケーションは一旦202 Acceptedを即座に返し、ユーザー側はポーリングやSSEなどで進捗を取得できます。
別サーバーでスケーラブルな画像変換処理を行うにはどのようにすればよいでしょうか?

ジョブスケジューラによるオンラインバッチ

メッセージキューを導入せず、アプリケーションやDBだけでオンラインバッチを組むことを考えてみましょう。この方法では、画像アップロードがされた時に画像変換ジョブをDBに保存します。
その後、一定時間ごとにテーブルをスキャンしてジョブを処理する仕組みを構築します。決められた間隔で起動するワーカーが未処理レコードを探して変換を実行し、完了後に状態を更新することで、メッセージキューなしでの非同期処理を実現できます。

課題

しかしこれはスケーラビリティの観点で大きな課題が残ります。スキャン方式では複数のワーカーがDBからジョブのレコードを取得する際にデータ競合(data race)を避けるロックが必要になるもしくは複数のワーカーが互いに独立したジョブにアクセスできるようにしなければなりません。このような実装も可能ですが仕組みが複雑化することは避けられません。

メッセージキューとワーカーにより非同期処理(オススメ)

画像変換リクエストをメッセージとしてキューに投入し、ワーカープールが取り出して実行するパターンはどうでしょうか?メッセージの単位はVariantごとに用意しましょう。
この方法ではスケーラビリティが大きな利点となります。メッセージキューがバッファとして機能するため、アップロードピーク時にメッセージがたまってもアプリケーションサイドは影響を受けません。ワーカーはメッセージキュー内のメッセージの数のメトリクスをに対してコンテナもしくはVMをオートスケールで増減でき、スループットを動的に確保できます。これにより、ピーク時にリクエストが増えても対処可能です。
関連
notion image
メッセージキューを使った後のデザインはこのようになります。
notion image

2. オブジェクトストレージによる画像データの信頼性について

オブジェクトストレージに保存されている画像を冗長化やデータ破損の検知を自動化させることで、画像データ損失を防ぐ高信頼なサービスを実現することができます。

ジオレプリケーション

ジオレプリケーションでは、ファイルデータを地理的に離れた複数のデータセンターに複製保存します。例えば、東京、シンガポール、オレゴンの三拠点に同じファイルを保存することで、自然災害や大規模障害による単一拠点の損失に対応できます。非同期レプリケーションを採用することで、書き込み性能への影響を最小限に抑えつつ、結果整合性を保証します。

Erasure Coding

Erasure Codingは、数学的アルゴリズムによる高効率な冗長化技術です。ファイルを複数のデータブロックとパリティブロックに分割し各サーバーで保管、一部のブロックが損失しても元データを完全復元できます。従来の単純なレプリケーションと比較して、約50%のストレージオーバーヘッドで同等以上の耐久性を実現し、運用コストを大幅に削減できます。

チェックサム検証

チェックサム検証では、データ破損の検出と修復を実現します。大規模システムでは、メモリ内でのデータ破損、ネットワーク転送中のビット反転、ストレージデバイスでの部分的エラーなど、「サイレント」な破損が日常的に発生し、検出が困難です。
各チャンクにSHA-256などのハッシュ値を付与し、データ読み取り時に再計算したハッシュ値と比較することで破損を検出します。チェックサム不一致が検出された場合は、Erasure Codingと組み合わせて他の障害ドメインから正常なデータを取得し、自動的に修復してクライアントに返却します。この継続的な検証により、サイレントデータ破損を防止できます。

3. CDNによるグローバルでの低レイテンシな画像配信

画像を世界中に低レイテンシで届けるにはCDNを用いてエッジサーバーでキャッシュすることが効果的です。ユーザーは地理的に最も近いエッジロケーションへ接続するためRTTが短縮され、オリジンサーバーの負荷も大きく下がります。

プル型CDNキャッシュ

画像がリクエストされた時点でCDNがオリジンから取得し、TTLが切れるまでエッジに保存します。多くのCDNのサービスはレスポンスのCache-Control ヘッダーをカスタマイズ可能なのでユースケースに応じてどのくらいのTTLに設定するかを見極めましょう。プル型CDNの短所は初回アクセスのキャッシュミスによりレイテンシが伸びる点ですが、アクセス頻度の高い画像が増えるほどヒット率が向上します。

画像のバージョン管理

キャッシュの欠点としてデータの更新をしなければならない点がありますが、URLにサイズ情報やファイルのハッシュ値を含めファイル単位でキャッシュキーを分離すれば、バージョン管理も容易です。何らかの要因で画像を更新しなければならない時も再生成した画像に新たにサイズ情報やファイルのハッシュ値を含めれば違うキャッシュキーとなり新しくアクセスした際にも新規生成された画像を参照することができます。

4. メタデータデータベースの可用性について

画像配信においてはCDNやオブジェクトストレージにより極めて高い可用性が実現できる一方で、メタデータを管理するデータベースは適切な冗長化設計が必要です。メタデータデータベースが単一障害点となることを避け、システム全体の可用性を確保するための設計について検討していきます。
最も一般的で実装が容易なアプローチは、プライマリ・セカンダリ構成によるシングルリーダーレプリケーションです。書き込みはプライマリデータベースでのみ受け付け、読み取りはプライマリまたは複数のレプリカから実行します。
プライマリに障害が発生した場合、フェイルオーバーによりセカンダリがプライマリに昇格します。フェイルオーバー中の短時間(通常30秒から数分)は書き込みが停止しますが、画像変換リクエストはメッセージキューに保持されるため、データベースが復旧すれば再度リトライする事で処理を継続できます。読み取りに関してはレプリカが稼働している限りは中断されることはありません。
例えば、AWSのRDSマルチAZ構成では、プライマリインスタンスに障害が発生すると自動的にセカンダリインスタンスに切り替わります。
関連
 
すべてを見るには

返金は購入日から1日以内に申し出て下さい。詳細はこちらからご確認ください。
また、このコンテンツ以外の他の永久アクセス権は付与されない事はご注意下さい。

支払いはによって保護されています

購入コードをお持ちの場合は、こちらから購入コードを入力して購入してください。
(Stripeのクーポンコードではありません)

購入済の方はこちらからログインしてください