Eight事業部 Platform Unit / Engineering Manager の 藤井洋太郎(yotaro) です。
前回、以下のRuby on Lambdaを使った画像処理基盤の記事を書かせていただきましたが、
今回の記事では少し前に、Eightフィード機能の高速化の際に行った、「画像フォーマット変換、リサイズ、キャッシュサービスをサーバーレスで構築した話」をしたいと思います。
Eightフィード
Eightのメイン機能として、「ニュースフィード」があります。
この機能は、これまでに名刺交換をした「人」や「企業」に関連するニュースや更新情報を届けるものです。例えば、
- 役職変更、転勤などの人事異動情報
- 企業のプレスリリースなどの最新ニュース
- ユーザや企業による最新動向
など「ビジネスに必要な情報」が各ユーザにパーソナライズされた形で届きます。
サクサク動かない。原因はOGP画像!?
しかし、そんなフィード機能にある課題を抱えていました。それはフィード投稿に含まれる「OGP画像」の読み込みが遅く、フィードのスクロールがカクカクしたり、画像が読み込まれるまで動かないという課題でした。
調査をしたところ、OGP画像として設定されている画像のサイズがかなり大きいケースがあることが大きな原因だとわかりました。 (なんと、サイトによっては数MBの画像が指定されていることも!)
そこで、私達のチームは画像のリサイズやフォーマット変換、キャッシングを行うことでユーザ体験の向上を図りました。
サーバレスなサービスの構築
画像圧縮やキャッシュ処理を行う機能は、Eight本サービスから切り出した、サーバーレス環境で構築することにしました。 環境は以下です。
- 画像フォーマット:
JPEG, WebP
- AWS:
CloudFront, API, AWS Lambda, S3
- 言語:
Python3.6
- フレームワーク:
Serverless Framework
- 開発環境:
Docker
- CI/CD:
CircleCI
Eightの開発は Ruby on Rails がメインですが、当時RubyはLambda非対応だったこともあり言語は Python を利用しました。
また、フレームワークにはAWS Lambdaだけでなく周辺環境の構築が容易でプラグインも豊富なServerless Frameworkを採用しています。
WebPフォーマットの採用
また画像変換する際に最も重要視したのは WebP画像 に対応することでした。
WebPとはGoogleが開発した画像フォーマットで、同品質のJPEGと比べて約25-34%ほどファイルサイズを小さくできるとされています。
対応ブラウザはこちらから確認できます。Safariはまだ対応していませんが、その他のモダンブラウザでは扱うことができます。
今回は、WebPフォーマット変換とリサイズを行い最適化するので、かなりのサイズ削減が期待できます🚀
構成図
早速ですが、全体の構成図は以下のような形になっています。
画像のキャッシュはCloudFrontを利用し、キャッシュが存在していなければAPI Gateway -> Lambda と処理が渡されオリジナル画像を取得 & フォーマット変換を行う流れです。
構成自体は一般的な形と言えるかもしれませんが、開発するにあたり工夫した点をここから紹介したいと思います。
Python3.6 で WebP変換
PythonでWebPを利用するには主に2つの方法があるかと思われます。
- cwebp をOSコマンドから叩いて変換
- https://developers.google.com/speed/webp/docs/cwebp?hl=ja
- cwebp はgoogleから提供されているWebP変換ライブラリ
- Pillow(PIL)などの画像処理ライブラリを利用
- https://python-pillow.org/
- Pillowは開発中止となったPILからフォークされ開発されているpythonでは有名な画像処理ライブラリ
- 様々な画像操作が可能で、対応フォーマットも豊富
変換のみであればcwebpでも良かったのですが、今後画像ファイルを操作することも考慮し、後者のPillowを利用することにしました。
Pillowを使った画像のWebP変換は、まずPillowをインストールし、
$ pip install Pillow
以下のようにすることでWebPに変換できます。
from PIL import Image im= Image.open('jpeg_file_path') im.save('webp_file_path', 'webp')
変換時に品質の調整などを行いたい場合は、save
メソッドの引数にオプションを渡すことで調整できます。
渡せるオプションはフォーマットによって異なるので、Pillowの公式ドキュメントを参照すると良いでしょう。
と、ここまでは単純そうに見えますが、Pillowは所謂 pure Python
なライブラリではないため、お手元の環境次第ではWebP変換に必要なライブラリが見つからずエラーが出ることが多々あります。
つまり、手元で動作してもLambdaの動くAmazon Linux上でも確実に動作するとは言い切れません。
そこで、開発環境やビルド/パッケージ環境もLambdaと同等の環境で実行していくことがプラクティスとなってきます。
Docker環境の構築
Lambda実行環境を擬似的に再現できる lambci/docker-lambda
を使います。
lambdaでサポートされている言語、バージョンでタグが打たれています。
今回は python3.6
を利用するため、 lambci/lambda:build-python3.6
イメージを使っていきます。
$ docker run -it lambci/lambda:build-python3.6 /bin/sh $ pip install Pillow
Pillowがインストールできた状態で、WebPが扱えるか試してみましょう。 おそらく、問題なく動作するはずです。提供されているLambda環境では、ライブラリの追加や環境変数等の設定も不要そうです。
Lambdaにデプロイする
Lambdaにデプロイするためには、外部ライブラリも含めた形にしなければいけません。そこでライブラリをinstallする際は
# Dockerコンテナ上で作業 $ pip install Pillow -t ./ $ zip -r zip_file ./*
などとし、作業ディレクトリ配下に展開した上で、zip化 & アップロードを行うと良いでしょう。
Serverless Frameworkの設定
Docker上で操作する前提でここまで書きましたが、今回の構成は Lambda だけでなく、 API Gateway との接続等も行う必要があります。 これらの構成まで手動管理というのはさすがに再現性がないのでフレームワークを使って管理していきましょう。
今回は Serverless Framework を使って管理していきます。
API Gateway と Lambda の接続
npm install serverless
でインストールし、作業ディレクトリ直下でプロジェクト作成(serverless create
)をします。
その後 serverless.yml
を以下のように設定することで Lambda と API Gateway を管理できます。
provider: name: aws runtime: python3.6 region: ap-northeast-1 functions: get_ogp_image: # 関数名 handler: functions/ogp_images.get # 関数の場所 description: 渡された画像urlからオリジナル画像を取得し、フォーマット変換やリサイズを行う # 関数説明 events: - http: # API Gateway の設定 path: ogp_images # APIパス method: get # HTTPメソッド
Lambda と API Gateway の連携自体は上記のようにとても簡単ですが、実は今回の構成はこれではうまく動きません。
今回は画像バイナリを返すAPI(サービス)となるので、バイナリを扱うための特別な設定が必要となってきます。
CONVERT_TO_BINARY
IntegrationResponse リソースの contentHandling プロパティを CONVERT_TO_BINARY に設定して、レスポンスペイロードが Base64 でエンコードされた文字列からそのバイナリ BLOB に変換されるようにする
とあるため、それに従ってAWS Console上から設定してみます。
設定反映後 API Gateway 上でテスト実行してみると、以下のようにバイナリ形式でレスポンスが返ることがわかります。
ここで設定した、バイナリ変換オプションですが Serverless Framework ではデフォルトでは対応していません(たぶん)。
そこで、プラグインを使って実現していきましょう。
serverless-apigw-binary プラグイン
serverless-apigw-binary というAPI Gatewayのバイナリオプションを有効化するプラグインを使いました。
npmパッケージとして提供されているため、 npm install --save-dev serverless-apigw-binary
といった形で先にインストールしておきましょう。
インストール後は serverless.yml に
plugins: - serverless-apigwy-binary ... functions: get_ogp_image: handler: functions/ogp_images.get description: 渡された画像urlからオリジナル画像を取得し、フォーマット変換やリサイズを行う events: - http: path: ogp_images method: get contentHandling: CONVERT_TO_BINARY # <- 今回追加したい設定
とすれば良いです。この柔軟さやプラグインの豊富さが Serverless Framework の強みですね!
デプロイ
そして最後はデプロイです。
$ serverless deploy
基本のコマンドは上記ですが、stagingとproductionを切り替えたいといったケースもあると思います。
その場合は --stage
optionを使うことで設定を切り替えられます。
$ serverless --stage staging $ serverless --stage production
また、デフォルトでは作業ディレクトリ直下のファイル群をパッケージング化するようになっていますが、テストコードなどテスト・ローカル環境のみで利用するコードもあるでしょう。
このような場合は、Packagingでinclude/exclude
を設定することで、必要なコードだけデプロイすることが可能となります。
また、詳細の解説はここではしませんが、環境変数の設定やIAM Roleの設定など細かい設定も可能なので、環境に合わせて設定していただくと良いと思います。
その他: TIPS
Docker環境
今回は深く掘り下げませんでしたが、基本的にはすべてDocker環境上に構築しています。
lambci/lambda:build-python3.6
イメージをベースに、serverless framework やserverlessを実行するための node
などを含むDocker Imageを独自に作成しています。
そして、コンテナを立ち上げる際に、ローカルの作業ディレクトリをDockerコンテナにマウントさせています。
こうすることのメリットとしては以下があげられます。
- 実環境でのみ発生するトラブルを極力避けられる。
- 開発環境を容易に構築することが可能。
- コーディングなどの作業は、ホスト側で行えるので馴染みのエディタなどを使える。
以前書いたRuby on Lambdaの記事でも同様のことを行っているので参考にしてもらえればと思います。
serverless-python-requirements プラグイン
serverless-python-requirements というプラグインを使うと、requirements.txtに記載したmoduleをデプロイ時にいい感じにパッケージング化してくれます。
また、pure Pythonでないmoduleも以下のようにすることでLambda Docker環境上でクロスコンパイルしてくれます。(すごい!)
custom: pythonRequirements: dockerizePip: true
ですが、今回は上述したようにすでに開発環境自体をDockerで構築しているため、上記の指定をする必要はありません。
Python開発ではおなじみの requirements.txt
でモジュール管理できるのはとても便利です。使わない手は無いでしょう🙌🏻
Lambda Layers
Lambda Layersは当時はまだ無かった機能ですが、Pillowなどのシステム依存なライブラリはLayersに切り出すとより良いかもしれません。
リリース後の反応
そして最後にリリース後の反応です。
画像配信サービスをリリースした後、メインサービスから見ていた画像へのアクセスの向き先をこのサービスに向かせるようにしました。
オリジナル画像のリサイズやフォーマット変換を行ったため、ファイルサイズは大きいもので10〜100分の1になることはわかっていましたが、その効果は想像よりも大きいものでした。
これまでアプリやブラウザで閲覧する際に感じていた、フィード描画時のワンテンポの遅延が明らかに解消されました。
このように、リリース直後から反響は大きく、フィードバックはもちろん、フィード投稿のインプレッション数やネットワーク使用量も大きく改善されました。
おわりに
今回の記事では、 Eightのコア機能である「フィード」の裏側で動いている、画像処理サービスについて紹介しました。
この対応を機に、Eight内での他の画像処理機構においても WebP フォーマットがファーストチョイスとなり広く採用するきっかけになりました。 こういった細かなパフォーマンス改善がサービス品質を支えていくということを改めて体感する機会となりました。
また、Eightフィード機能は長い歴史がありますが、この半年の間にゼロベースでアーキテクチャを刷新した機構でもあります。 その話は、また次の記事で紹介できればと思います👋🏻