サーバーレスの入門に!自宅サーバーレス+自宅S3環境で作るサーバーレス・サムネイルサーバー!


サーバーレスのHelloWorld = サムネイルサーバー?

日本語で読めるサーバーレスの有名記事といえば,伊藤直也さんの一休の事例だと思います.
下記の記事では,Amazon Lambdaを使って,一休のサムネイルサーバーを構築した事例の紹介をしています.

伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(前編)。QCon Tokyo 2016
伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(中編)。QCon Tokyo 2016
伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(後編)。QCon Tokyo 2016

スケールしやすい.安価にサービスできる.ということでサーバーレス・アーキテクチャは注目が集まっているようです.

そこで,今回,一休での事例をまねて,

1時間で作れる!かんたん自宅Serverless環境! ~はじめてのServerless Application入門~

で作った,Serverless環境のiron-functionsと,

コマンド1つで作れる!かんたん自宅Amazon S3互換環境!

で作った,S3互換環境を合わせて,サーバーレス・サムネイルサーバーを構築したいと思います.

サーバーレス・サムネイルサーバーのシークエンス

ざっくりとした概要の図です.
1. リクエストから「ファイル名」と「サイズ」を受け取る
2. minioのthumbsのバケットに該当のサイズのサムネイルがあるか?ない場合は3.へ.ある場合は7.へ.
3. minioのimagesのバケットからファイルをダウンロード
4. ダウンロードした画像ファイルをリサイズ
5. リサイズした画像をminioのthumbsのバケットへアップロード
6. リサイズした画像をレスポンスし,終了
7. minioのthumbsのバケットからサムネイルをダウンロード
8. ダウンロードした画像をレスポンスし,終了

API仕様書

(仕様書というほどではないですが・・・)

概要

png画像のサムネイルを提供する.

  • 指定できるサイズは画像の横のサイズのみ.
  • 縦横比は保持する.
  • png以外の画像フォーマットには非対応.
  • テストのため,エラーハンドリングは考慮しない

エントリポイント

/iron_thumbs/thumbs

URLパラメーター

変数 説明
filename imagesにアップロードされたファイル名(key)
size サムネイルの画像の横のサイズ

レスポンス

png画像データ

minioのimagesバケットに入っているtest.pngを,画像の横のサイズが30pxになるように縮小し,バイナリデータとして出力する.

事前準備

今回,

  • iron-functions
  • minio

という2つのソフトウェアを使います.
手前みそですが,Seveless環境のiron-functionsは

1時間で作れる!かんたん自宅Serverless環境! ~はじめてのServerless Application入門~

の記事で紹介しています.
また,S3互換環境であるminioに関しては,

コマンド1つで作れる!かんたん自宅Amazon S3互換環境!

の記事で紹介しています.
今回,サムネイルサーバーを構築するにあたって,最低限必要なものとして,

  • docker
  • awscli
  • glide

の3種類です.glideは上記記事では,取り扱っていませんが,go言語でライブラリをインストールするためのパッケージとして今回必要です.
あとgolangの開発環境があるとよいですが,今回,動かすだけであれば必要ないと思います.(おそらく,glideのインストールと同時にgo言語の開発環境も入ると思います)

ソースコード

今回,コードが300行程度あったため,githubのほうに置きました.
https://github.com/kotauchisunsun/iron_thumbs.git

デプロイ

最初にMinio(S3互換サーバー)を前回と同じ設定で8080ポートでサービスします.

設定する変数
Access Key AKIAIOSFODNN7EXAMPLE
Secret Key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# minioを起動
$ sudo docker run --rm -p 8080:9000 -it \
    -e MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE\
    -e MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\
    minio/minio server /export

その次に,minioにバケットを作成します.

# minioへimagesバケットの作成
$ aws --endpoint-url http://192.168.0.6:8080 s3 mb s3://images
# minioへthumbsバケットの作成
$ aws --endpoint-url http://192.168.0.6:8080 s3 mb s3://thumbs

iron-functionsを8000ポートでサービスします.

# iron-functionsのサービス
$ sudo docker run --rm -it --name functions -v ${PWD}/data:/app/data \
                   -v /var/run/docker.sock:/var/run/docker.sock \
                   -p 8000:8080 iron/functions

サムネイルサーバーをサービスします.一応Dockerコンテナの流儀のようなものを意識して,各種サービスに必要なパラメーターは環境変数として渡しています.

# ソースコードをクローン
$ git clone https://github.com/kotauchisunsun/iron_thumbs.git
$ cd iron_thumbs
# パッケージの依存関係を解消
$ glide update
$ sudo fn build
$ fn apps create iron_thumbs \
    --config IMAGE_BUCKET=images\
    --config THUMB_BUCKET=thumbs\
    --config S3_ENDPOINT=http://192.168.0.6:8080\
    --config S3_REGION=us-east-1\
    --config S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE\
    --config S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ fn routes create iron_thumbs /thumbs

今回,テストデータとして,いらすとやの画像を使います.それっぽい画像としてクラウドコンピューティングの画像を使います.

# 素材をダウンロード
$ wget "http://4.bp.blogspot.com/-dVbfTZcofUU/VGX8crT5GiI/AAAAAAAApG4/CB7GF5UmMqE/s400/computer_cloud_system.png"
# minioにファイルをアップロード
$ aws --endpoint-url http://192.168.0.6:8080 s3 cp computer_cloud_system.png s3://images/

実際にiron_thumbsに対してリクエストしてみます.

$ curl "http://localhost:8000/r/iron_thumbs/thumbs?filename=computer_cloud_system.png&size=180" > thumbs.png

これでサムネイルが出来ています!

ベンチマーク

「いらすとや」ベンチ

今回もベンチマークを取ってみました.

どのようなものを想定したかというと,「いらすとや」を自分で運用すると考えました.

「いらすとや」で画像を検索すると,検索にマッチした画像のサムネイル(赤枠の部分)が表示されます.この画像の横のサイズが約180pxになっています.(実際は少し違います)
このサムネイルをいかに速く作れるのか?をベンチマークしてみたいと思います.

使ったベンチマークソフトは以前も使ったApacheBench.設定は,

$ ab -n 1000 -c 10 "http://localhost:8000/r/iron_thumbs/thumbs?filename=computer_cloud_system.png&size=180"

こんな感じ.
今回,2種類のベンチマークをしました.

always_make_thumbs

これはリクエストごとに常にサムネイルを生成し,レスポンスします(ソースコードを改変してます.)

cached_thumbs

これはthumbsのバケットからサムネイルを取ってきて,レスポンスします.

サムネイルサーバーのシステム設計をする

色々なサムネイルサーバーの話がネットには転がっています.

pixivのサムネイル事情(pixiv)
料理を楽しくする画像配信システム(cookpad)

このように各社各社がしのぎを削ってるようです.
今回,このようなサムネイルサーバーの構成を考えてみました.

実運用されているサムネイルサーバーは,サムネイルの動的生成だけでなく,CDNが前に立つことがよくあるようです.
理由は主に2つあって,

  1. サムネイルサーバーの負荷軽減
  2. 高速なサムネイルの配布

です.サムネイル作成はCPUへの負荷が高く,サーバーが高負荷になりやすいです.では,サーバーを追加すればよいか?という話になるのですが,お金のないベンチャーはそういうことが出来ません.そのため,出来るだけCDNでサムネイルをキャッシュし,なおかつアクセスのある時は,サーバーレスで短時間動かすことで,安値でスケールしやすいシステムを作ることが出来ます.
実はこの辺の,サムネサーバーの要求要件が,かっちりサーバーレスのアーキテクチャにはまっているため,事例として,よく取りあげられてるのだろうなぁ.と思っています.

なのですが,

ぶっちゃけCDNとサムネサーバーとS3の面倒を見るのはめんどくさいです

キャッシュというのは,高速化には常套手段ではありますが,個人的には,バグを引き起こす原因の上位に来てます.(サーバーレスアーキテクチャだけど,すごくステートフルだし)
実際に,最近,メルカリではCDNの周りで個人流出事件を起こしてます(CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして)
そのような理由で,個人的にはキャッシュは出来るだけ,使いたくない気持ちではあります.
しかし,今回考えたサムネサーバーの構成は,先ほどの図にあるように2段階にキャッシュを持っています.

  1. CDN
  2. サムネイル

CDNにサムネイルがなければ,サムネサーバーに問い合わせ,サムネサーバーがサムネイルを今までに作ったことがなかった場合,画像ファイルをダウンロードしてきて,サムネイルを作ります.

そのため,今回は,出来るだけキャッシュを少なくするために,「どこかのサーバーを省略できないかな?」というのが,このベンチの目的です.
そのため,

  • CDNを使わないバージョン=cached_thumbs
  • サムネを動的に作り続けるバージョン=always_make_thumbs

と考えて,ベンチを行ってみました.

結果・考察

項目 平均レスポンス時間[ms]
always_make_thumbs 8417.722
cached_thumbs 7542.938

一度サムネをS3(minio)にキャッシュする方が,1,000[ms]程度速く,7,500[ms]でサムネイルをサービスすることが出来ました

ただ50歩100歩で,普通にWebページを見ていて,7.5秒もサムネの表示に時間がかかってたら使い物にならない感じです.前に検証した,

Serverless環境は600倍以上遅い? ~GoとNode.jsとPythonでベンチマークとってみた!~

の通り,そもそもiron-functionsは遅いです.簡単なGoのサーバーレスのサービスでも6,500[ms]程度かかってました.そこから考えると,サムネ生成からレスポンスまで,8,500[ms]-6,500[ms]なので,2,000[ms],約2秒と考えられます.

あなたのサイトは2秒以内?表示速度がフォームコンバージョンに与える影響

によると,

47%のEコマースユーザーは2秒以内にページが読み込まれることを期待している

だそうです.そのため,サムネの表示に2秒程度かかっていては,やはり問題があります.そのため,前段のCDNのようなキャッシュ機構はまだまだ必要なようです.

ただ一方で

今の世の中だと2秒ぐらいでサムネイルを作れる

という見方もあります.
確かに,2秒というレスポンス時間は遅いです.これに実際にサーバーレスの起動の時間が入ると,もっと時間がかかる.Amazon Lambdaが早いといっても,レスポンスに3秒ぐらいかかるかもしれない.ただ

1人サムネイル生成を待てば,あとはCDNがキャッシュしてくれる.

という世界観だとどうでしょう.割と個人的にはありなんじゃないかなーと.1人が3秒ぐらいサムネイルの生成を待つけども,それ以外の人はCDNが高速にレスポンスしてくれる.
 今回,サムネイルサーバーを作ってみて,「結構めんどくさいなぁ.」とか「S3のバケット2つに依存するなぁ」とか,「これサムネイル側のキーの衝突とかちゃんとハンドリングするの面倒だなぁ」とか,「効率化するためにサムネをS3にキャッシュするのだるいなぁ」とか思いました.そういうことを考えると,コードをクリーンに保ちつつ,運用しやすいインフラを考えると,サーバーレス+CDNの構成で十分で,サムネイル自身をバケットに保持してキャッシュする必要はそうそうないんだなーと思いました.
確かに,

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

という記事の中で,

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

サムネイル画像は保存しない.という方向性があって,へー.と思ってたんですが,これなら割と落としどころとしてはありなラインなんだなぁ.と検証してみて思いました.

感想

最近,後輩を指導してて,

私「この問題は,Aの可能性があります.ただ一方でBの問題も考えられ,Cの問題も考えられます.別の視点だとDの問題も考えられ・・・」
後輩「その原因のうち,どれを選んで,どれから調べればいいんですか?」
私「

という話をしました.
実際のところ,「問題のある箇所を特定するためにテストを行い」「問題を切り分け」「解決する」というのが一般的だと思います.しかし,全てが全てそういう風に出来るわけではないです.
そんな中で「えいや」と問題を割り切って,テストをしたときに,問題の原因が一発で分かる.みたいなところが「勘」と言ってる所以です.

今回,サムネイルサーバーを作ってみて,この「勘」と呼ばれる部分が働かずに苦労しました.私はGo言語に慣れていないのですが,慣れていると「普通は,はまらないところには,はまらない.」ということが出来ます.しかし,慣れていないと,「普通はこういう実装しないよな・・・」というやり方で実装してしまい,「普通は,はまらないところに,はまる」をしてしまい,問題解決にすごく時間を取られて,頭を悩ませたりしました.

あとiron-functions周りもそうでした.これは純粋にナレッジが少ない面もありますが,公式のドキュメントもなくて,厳しかったです.私はあまりDockerやDocker周りのお作法をあまり知らない面もあるので,その面で「勘」が働かず,毎度毎度厳しい感じになってました.

特に,glideによるライブラリのインストールとiron-functionsの連携部分が厳しく,GoとDockerとiron-functionsの3つの領域の重なった領域だったので,かなり時間がかかってしまいました.

そんなわけで日々組んでおかないといざ作るときになると厳しいなぁ.と思うことはよくあります.やはり日々精進が必要なのだなぁと感じた次第でした.