Help us understand the problem. What is going on with this article?

サーバレスにサムネイル画像を配信する試み

More than 1 year has passed since last update.

AWS流行ってますね。

先月末のre:Invent 2016では既存のサービスの拡充や、意欲的な新サービスが発表されたようです。

ところで、最近API GatewayはBinary Support, Lambdaでは環境変数のサポートなど、サーバレスな仕組みをお手軽に実現するためのサポートも増えました。1
今回は、API GatewayのBinary SupportとLambdaを使って既存の構成をサーバレスな仕組みに置き換えられそうか、試してみました。
対象は、サムネイル画像の配信システムです。

注意: 資料を元にいくつかの既存システムを紹介していますが、私の解釈の間違いや現状のシステムとの差異がある可能性に注意してください。

要件

  • ユーザに投稿してもらった画像は、オリジナル画像としてS3に保存
    • オリジナル画像は配信しない (private)
  • サムネイル画像が要求されたらオリジナル画像から動的変換2 し、CDNで配信
    • サムネイル画像は保存しない
    • CDNにヒットしなかったらもう一度動的変換

動的変換は便利

サービスの要求によりますが、よくある事前にサムネイル画像生成は将来的にもアプリが事前に決められたサイズしか要求しない場合には、シンプルで都合が良い構成です。

しかし、既存の画像を丸々新しいサイズにリサイズしなければならないので、新たにレスポンシブルなデザインに対応するなど新しいデバイスの展開やプロトタイピング的な利用が難しくなります。

今回はよりチャレンジングそうな動的変換を行うことにします。

既存の構成

動的変換でよくある構成を次に示します。TOFU, pixivのサムネイル事情 などを参考にしています。

リクエスト数やCDNへのヒット率に応じて、EC2のインスタンスの増減をすればお手軽にスケールできそうです。

スクリーンショット 2016-12-17 15.33.25.png

サムネイル画像へのアクセス

  • http://example.com/images/[width]x[height]/[hash]/[image].jpg
    • hashは悪意のあるユーザが width, heightをちょっとづつずらしてリサイズ処理を多数走らせないように用意

サーバレスな構成

API GatewayとLambdaを使って次のように構成してみました。シンプルですね。
アクセス方法は既存の構成と同じにできます。

スクリーンショット 2016-12-17 16.47.37.png

スケールすることに関して

  • API GatewayはLambdaのプロキシとして十分にリクエストを処理できる
    • AWSがよしなにやってくれる
    • ボトルネックになるとしたらLambda
  • Lambdaもスケールすることに関して何も気にしなくて良い
    • AWSがよしなにやってくれる
  • Lambdaなのでスケールすることに関して気にする必要がある
    • リージョン毎に 同時実行数制限がある (デフォルト: 100)
      • サムネイル処理に1000msかかるとすると、100 rps程度まで処理できる
      • AWSに要求すれば引き上げられる

構築手順の概要だけ説明します。

Lambdaの設定

API Gatewayのバックエンドとして構築し、パス名を処理する必要があります (API Gatewayの設定でeventにセットできる)。
また、Lambdaから対象バケットのオブジェクトに対してGetObject を行える設定が必要です。

変換はLambdaの環境でサポートされているImageMagickが手軽ですね (ImageMagickがベストかどうかについては議論の余地があります)。

const aws = require('aws-sdk');
exports.handler = (event, context, callback) => {
  const s3Client = aws.S3();
  const size = parseSize(event.size);
  const hash = event.hash;
  const image = event.image;

  // hashを使った検査
  ...
  // サムネイル生成
  s3Client.getObject({ Bucket: "...", Key: image }).promise()
  .then(response => resize(response.Body, size))
  .then(thumbnail => callback(null, thumbnail))
  .catch(err => callback(err))
}

API Gatewayの設定

  1. /{size}/{hash}/{image} というリソースを作成
    • 全てパスパラメータ
  2. /{size}/{hash}/{image} にGETメソッドを用意
    • 先ほどのLambdaと統合
    • Integrationでマッピングパターンを次のように設定
      • この設定でLambdaのハンドラのeventにパスが渡せる
  3. Binary Supportを利用し、image/*のときバイナリを返すように設定
{
  "size": "$input.params('size')",
  "hash": "$input.params('hash')",
  "image": "$input.params('image')"
}

CloudFront

  1. Custom OriginをAPI Gatewayのエンドポイントに設定
  2. Origin (API Gateway)がサムネイル画像を返せない時は、no_img.png のような画像を短い時間キャッシュするように設定
    • 高い負荷の時は、一部この画像が表示されることになる

性能

siege を使って簡単に測定して得られた知見。

  • 800x800 (500KB) のサンプル画像で、100x100のサムネイル生成の平均変換時間は500ms程度
    • いわゆるLambdaのコンテナがHotなとき 参考
  • API Gateway + Lambdaは100rpsくらいならErrorなくさばけていた
  • Lambdaの同時実行数は100を超えても短い時間なら問題無い

まとめ

  • サーバ管理が不要に
    • AWSの仕様や変更をウォッチし続ける必要はありそうですが...
  • CloudFrontのヒット率が高ければ (90%以上)、1000rps程度のサムネイル画像要求ならさばける可能性が十分ある
  • 全ての仕組みが従量課金制なので、発展段階の小規模なサービスにはコストを低く抑えやすい
    • Route 53を使えば途中で配信方法を切り替えるなど柔軟な対応もできそう

参考文献

動的変換

事前に変換

クラウドサービス

その他


  1. Binary Support, Environment Variables 

  2. サムネイル画像が実際に要求された時に初めて変換することを動的変換と呼ぶことにする 

mmhiyoko
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away