てくすた

ピクスタ株式会社のエンジニア・デザイナーがつづるよもやまテクニカルブログです

PIXTAにおけるAWS Lambdaの活用事例

新年明けましておめでとうございます。
開発部のもりと申します。
本年も”てくすた”をよろしくお願いします。

今回のテーマはPIXTAにおけるAWS Lambdaの活用事例として、CloudWatch EventsをトリガーにしたEBSボリュームのスナップショットの取得と、CodeCommitをトリガーにしたOpsWorksへのデプロイの方法についてご紹介させていただきます。

AWS Lambdaとは

ご存知の方も多いかと思いますが、「AWS Lambda」はクラウド上でアプリケーションを実行するプラットフォームです。AWS Lambdaを使えば何らかのイベントをトリガーにして処理を実行することが可能です。例えば、以前紹介させていただいた記事では、APIGatewayをトリガーにしてAWS Lambdaで何らかの処理してS3にデータを格納するといったことがサーバーレスで実現できます。
なお、トリガーに指定できるイベントのソースは、これだけあり、今回はCloudWatch EventsとCodeCommitをトリガーにした例を紹介します。

CloudWatch EventsからのEBSスナップショット取得

なぜやるか?

AWS Lambdaの特徴のひとつに「インフラの運用管理が不要」が挙げられると思います。
今までEC2インスタンスのEBSのバックアップ方法としてスナップショットの取得を毎日cronでシェルスクリプトを実行することで実現しておりました。
そのスクリプトを実行するためのEC2インスタンスを用意しておりましたが、それ以外での利用用途がなく管理の手間とコストの削減を目的に、AWS Lambdaに置き換えました。

どうやるか?

AWS Lambdaに移行するに当たって置き換えたのは、

  • シェルスクリプトをPythonに
  • cronをClowdWatch Eventsに
  • 当たり前ですが実行環境をEC2からAWS Lambdaに

としました。
AWS LambdaへのデプロイはServerlessFrameworkを使用しています。
(私はPythonを学ぶいい機会となりました。)

やってどうなったのか?

効果は小さいながら前述のEC2インスタンスを削減することができました。
また、実行コードをServerlessFrameworkに乗せたことで、コードレビューもできるようになり、デプロイも容易になりました。

CodeCommitからのOpsWorksでのアプリケーションデプロイ

なぜやるか?

もうひとつ特徴として「イベントドリブン」が挙げられると思います。
PIXTAのサービスは複数の機能から構成されており、その機能毎にステージングとプロダクションの環境をOpsWorksのstackに構築しており、デプロイの際は、各stackにおいてリリース担当者がリリースのオペレーションを実施しておりました。
リリース作業に1日の大部分を費やしていることや、単調なオペレーションが担当者の負担になっていることが課題としてあったため、リリース作業時間の短縮とオペレーションの自動化を目的に、AWS Lambdaに置き換えました。

どうやるか?

着想はHerokuでのアプリケーションデプロイでした。つまりgit pushのコマンドで対象のコードをデプロイすることができれば大幅にオペレーションコストは削減できるだろうと考えました。そこでgit pushの「イベント」に対応して、AWS Lambdaに登録したOpsWorksへのデプロイコードを実行させステージングとプロダクション両方の環境へデプロイするようにしました。

f:id:philip_moris:20180117130708p:plain

置き換えたのは、

  • git push時のミラーにCodeCommitを追加
  • 手動でのオペレーションをPythonに

としました。
リリースの対象となるブランチをリリース担当者の開発環境からgit pushすることで、CodeCommitに反映され、それをイベントとしてAWS Lambda関数が実行されOpsWorksへデプロイされます。

やってどうなったか?

手順が簡略化したこと、手動でのオペレーションがなくなったことで省力化とデプロイの時間の短縮が実現できました。また、コーポレートサイトのようなほぼ静的なページで構成されるアプリケーションについてもデザイナー等の非エンジニアでもリリースが可能となり属人化を避けれるようになりました。

まとめ

CloudWatch EventsとCodeCommitをトリガーとしたAWS Lambdaの利用事例を紹介させていただきました。
紹介した事例では、Lambda関数の実装にPythonを採用しておりますが、最近ではC#やGo言語もサポートしてきており、開発できる幅も広がってきていると思います。先に挙げたようなインフラの運用管理の手間を少しでも減らしたい、何かをイベントとして何らかの処理を実行させたいといった課題がもしあれば、一度AWS Lambdaを試してみてはいかがでしょうか。

* * * * *

ピクスタでは、能動的に改善に取り組んでサービスを良くしたいエンジニアを募集しています。

recruit.pixta.co.jp

recruit.pixta.co.jp

開発環境のレスポンスを60秒から1.5秒に高速化した3つのポイント

ピクスタ開発部の星 直史 です。
メリークリスマス!!!これは現実です!クリスマスは今年も開催します!!!
本日、私はケンタッキー、パック寿司、コージーコーナーのショートケーキとシャンメリーを買い込み、ネットしながら一人クリスマス会をしようと思います!!

この記事はPIXTA Advent Calendar 2017 25日目の記事です。

さて、先日てくすたでfotowaの開発環境構築にDockerを導入した話を紹介しました。
PIXTAをメインに開発するチームの開発環境でもDockerを導入をしているのですが、PIXTAのTOPページを表示するのに60秒近くかかっていました(!)
この記事では、開発環境高速化の取り組みについて紹介をします。

開発環境の概要

ピクスタ開発部メンバーの多くはMacOSで開発をしています。*1 また、Docker for Macとdocker-syncをインストールし、Docker Composeでコンテナを立ち上げることで、コマンド一発で開発環境を立ち上げています。

高速化するまでのステップは基本に忠実に行いました。

  • 問題を再現する
  • 対策の検討と実施
  • 対策の検証
  • ボトルネックを探し1に戻る

問題解決を迅速に行うためには、事実をベースにして仮説検証をすることが重要です。
そのためには、問題となっている事象を再現し、何が原因で問題が引き起こされているのか的確に把握することが重要です。問題の原因特定が仮説検証の精度を左右するといっても過言ではないと思います。

問題を再現する

問題を再現するにはまず、問題がどこにあるかを確認するためRailsアプリケーションのlog/development.logを確認します。

...略
Started GET "/" for 172.21.0.1 at 2017-12-25 03:32:36 +0000
  ActiveRecord::SchemaMigration Load (23.0ms)  SELECT `schema_migrations`.* FROM `schema_migrations`
  ActiveRecord::SchemaMigration Load (22.6ms)  SELECT `schema_migrations`.* FROM `schema_migrations`
  Currency Load (10.1ms)  SELECT `currencies`.* FROM `samples`
  Currency Load (9.8ms)  SELECT `currencies`.* FROM `samples`
Processing by SampleController#index as HTML
#<ActionDispatch::Request::Session:0x0056081879bca8>
{:analysis=>{}, :_csrf_token=>"2mE6LEmIjbJ/GuU9SvsN5PUu7qiivU4fKX2/pifFOQk="}
Dalli::Server#connect cache.pixta.jp:11211
hogehoge.pixta.jp:11211 failed (count: 0) SocketError: getaddrinfo: Name or service not known
Session::DalliStore#get: No server available

...略

Started GET "/assets/i18n.self.js?body=1" for 172.21.0.1 at 2017-12-25 03:35:14 +0000
Started GET "/assets/i18n/shims.self.js?body=1" for 172.21.0.1 at 2017-12-25 03:35:15 +0000
Started GET "/assets/sample1.js?body=1" for 172.21.0.1 at 2017-12-25 03:35:16 +0000
Started GET "/assets/sample2.js?body=1" for 172.21.0.1 at 2017-12-25 03:36:21 +0000
Started GET "/assets/sample3.js?body=1" for 172.21.0.1 at 2017-12-25 03:37:53 +0000
Started GET "/assets/sample4.js?body=1" for 172.21.0.1 at 2017-12-25 03:38:24 +0000

...略

このログを見て2点問題があることに気づきました。

  • Assetsをライブコンパイルしている
  • キャッシュを使用していない

ログを確認すると、レンダリングが完了するまでに、Assetsを動的なSprocketsコンパイルしているため配信に時間がかかっていることと、キャッシュが効いていないため、都度DBアクセスする重い処理が走っていることがわかりました。

対策の検討と実施

Railsアプリケーションにおける問題は前述の通りなので、対策としては設定ファイルを変更が考えられます。
具体的には、config/enviroments/development.rbの修正です。

Assetsを事前にコンパイルする

config/enviroments/配下の設定の詳細はRailsガイドを参考にしました。
compile周りの設定は下記の設定で変更しました。

# コンパイル済みアセットを圧縮するかどうかを指定。圧縮することで、データ容量を減らし、高速化をする
config.assets.compress = true

# JavaScriptの圧縮に使用するプログラムを定義。:uglifierが最も圧縮率が高い
config.assets.js_compressor = :uglifier

# CSSの圧縮に使用するプログラムを定義
config.assets.css_compressor = :scss

# 動的なSprocketsコンパイルをするかどうかを指定
config.assets.compile = false

# デバッグ用にアセットの連結と圧縮をやめるかどうかを指定
config.assets.debug = false

precompileした場合の懸念点はいくつかありました。

  • JSやCSSに修正した場合に再度rake assets:precompileコマンドを実行しなくてはならない。
  • config.assets.debug = falseconfig.assets.compress = trueとしているため、エラー時のデバッグがしにくい

上記について、既にwebpackを導入していたため--progress --watchオプションで起動することで、修正反映直後に再ビルドが走り、かつ、エラーが発見できるため、問題ありませんでした。*2

キャッシュ使用する

キャッシュの設定についても、Railsガイドを参考に設定していきます。
memcachedをコンテナ化しているので、それを指定します。

# Railsでのキャッシュ処理に使用されるキャッシュストアを設定します
config.cache_store = :dalli_store, "memcached.pixta.jp", { compress: true }

# Sprocketsで使用するキャッシュストアを定義します
config.assets.cache_store = :dalli_store, "memcached.pixta.jp", { compress: true }

対策の検証

設定ファイルを変更したのちに下記コマンドを実行し、TOPページにアクセスすると、10秒前後に高速化されました。

  • rake assets:precompile
  • yarn webpack --config config/webpack.config.babel.js --progress --watch

高速化されたとはいえ、ソースコードを修正し、更新するごと10秒待たされるのは苦痛です。
ログを見るとRailsアプリケーションに到達してからは高速化されたのですが、一番最初の処理が開始されるまでに待ち時間がありました。

このことから、Webサーバー(Apache HTTP Server、Passenger)の処理に問題がありそうなので、Webサーバー側のログを見ます。*3
具体的にはaccess.logerror.logです。

172.21.0.5 - - [25/Dec/2017:03:39:33 +0000] "GET /sample HTTP/1.1" 200 315 "-" "Typhoeus - https://github.com/typhoeus/typhoeus"
172.21.0.1 - - [25/Dec/2017:03:39:32 +0000] "GET / HTTP/1.1" 200 17108 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
172.21.0.1 - - [25/Dec/2017:03:40:55 +0000] "GET /assets/frontend/vendor.js HTTP/1.1" 304 - "http://dev.pixta.jp/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"

確認しても、特に疑わしい箇所はなかったのですが、access.logに書き込まれるまでに数秒待たされていました。
これは何度アクセスしても同じでした。

問題を再現する(2サイクル目)

さすがにこれ以上ログから追うことはできなそうでしたので、Apache HTTP Serverの設定ファイルを確認することにしました。

...略
<IfModule mpm_prefork_module>
    StartServers          16  
    MinSpareServers       16  
    MaxSpareServers       32  
    ServerLimit           400 
    MaxClients            400 
    MaxRequestsPerChild   10000
</IfModule>

<IfModule mpm_worker_module>
    StartServers          4   
    MaxClients            1024
    MinSpareThreads       64  
    MaxSpareThreads       192 
    ThreadsPerChild       64  
    MaxRequestsPerChild   10000
</IfModule>

...略

LoadModule alias_module modules/mod_alias.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_default_module modules/mod_authz_default.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule dir_module modules/mod_dir.so
LoadModule env_module modules/mod_env.so
LoadModule expires_module modules/mod_expires.so
LoadModule headers_module modules/mod_headers.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule status_module modules/mod_status.so

...略

設定ファイルで挙動に影響がありそうな箇所は2箇所のみでした。

  • MPM(Multi Processing Module)の設定
  • 不要なLoadModuleのInclude

ここの2箇所以外はミニマムな設定だったので、ここに狙いをつけることにしました。

対策の検討と実施(2サイクル目)

MPMの設定

Apache HTTP ServerにはMPM(Multi Processing Module)といわれるリクエストを受けた際の処理の方法の違いがあるモジュールが用意されています。
現在の設定を確認するために、下記コマンドで確認します。

$ /usr/sbin/apachectl -V
Server version: Apache/2.2.15 (Unix)
Server built:   Mar 22 2017 06:52:55
Server's Module Magic Number: 20051115:25
Server loaded:  APR 1.3.9, APR-Util 1.3.9
Compiled using: APR 1.3.9, APR-Util 1.3.9
Architecture:   64-bit
Server MPM:     Prefork
  threaded:     no
    forked:     yes (variable process count)

コマンドを確認すると、設定がPreforkが設定されていることがわかります。
Apache HTTP ServerのMPMは数種類用意されています。代表的なものは下記の通りです。

  • prefork
  • worker
  • event

それぞれの違いについては、公式のドキュメントに詳細が載っていますので、解説はそちらに譲ります。
マルチプロセッシングモジュール (MPM)

preforkはリクエストを受けたらプロセスが処理します。workerはリクエストをスレッドが処理することになるので、プロセスを立ち上げる分のオーバーヘッドが少なくなるため、workerの方が高速になります。

MPMをpreforkからworkerへの切り替えは、etc/sysconfig/httpdHTTPD=/usr/sbin/httpd.workerを追加し、再起動するだけで変更できます。

# The default processing model (MPM) is the process-based
# 'prefork' model.  A thread-based model, 'worker', is also
# available, but does not work with some modules (such as PHP).
# The service must be stopped before changing this variable.
#
HTTPD=/usr/sbin/httpd.worker

変更結果は下記の通りです。

$ /usr/sbin/apachectl -V
Server version: Apache/2.2.15 (Unix)
Server built:   Mar 22 2017 06:53:18
Server's Module Magic Number: 20051115:25
Server loaded:  APR 1.3.9, APR-Util 1.3.9
Compiled using: APR 1.3.9, APR-Util 1.3.9
Architecture:   64-bit
Server MPM:     Worker
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)

不要なLoadModuleのInclude

httpd.confのLoadModulesを確認すると、開発環境においては不要なModuleが読み込まれていました。

こちらについては、あまり特別なことはしておらず、公式のドキュメントを参照し、読み込まれているModuleを一つずつ、どんな用途で使用するのかを確認しながら、消していき、必要最低限のModuleだけをIncludeすることにしました。

対策の検証(2サイクル目)

MPMをworkerに設定し、LoadModuleを必要最低限に絞りアクセスすると、10秒前後かかっていたのが3秒弱にまで縮まりました。

しかし、Railsアプリケーションの最初の処理が動き出すまでに、1秒前後かかっていました。
前述の通り、ピクスタの開発部は大多数がMacOSを使用しています。そのため、docker-syncでホストとコンテナ間でファイルの同期をする必要がありました。マウント時の同期オプションの変更PRなどで速度改善がなされましたが、そもそもファイル同期が遅いというissueがあることや、中でもMacOSとDockerの同期速度が遅いので、そこがボトルネックになっていると仮説を立てました。

問題を再現する(3サイクル目)

試しに、素のEC2インスタンスにDockerとDocker Composeをインストールし、開発環境を立ち上げた後にアクセスしたところ、3秒だった処理が1.5秒前後にまで縮まりました。
このことから、ホストOSの相性が悪かったことがわかりました。

対策の検討と実施(3サイクル目)

ピクスタ開発部はベトナムにも開発拠点があり、ベトナムメンバーが主に使用しているOSはUbuntuです。
そのため、Dockerにより開発環境構築の標準化を進めていましたが、Dockerをインストールする手順がUbuntuとMacで微妙に異なってしまうため、ドキュメントが2つ存在していました。

そこで、MacOSとDockerの同期速度の改善と、開発環境の標準化をもう一歩進めるために、Vagrantを導入することにしました。

元々、Ubuntu用のドキュメントや、docker-composeの資産があったので、単純にVagrantで仮想環境を立ち上げ、Vagrantfileのprovisioningブロックの中でドキュメントに書かれていた処理内容を転記するだけでしたので、楽に導入することができました。

対策の検証

前述の通り、仮装環境を立ち上げ、その上にDockerを起動させることで、3秒から1.5秒にすることができました。狙い通りですね。

また、仮装環境内でコンテナを立ち上げたことで、bundle installrake assets:precompileなど、ファイルI/Oが発生する処理も高速化されました。当初240秒かかっていた処理が60秒(!)に高速化されました

まとめ

Railsの設定ファイルを変更することで速くなることは予想していましたが、ミドルウェアの設定や、Dockerとそれを動作させるOSとの相性まで考え抜けたのは、計測を基にして対応したからだと思います。

60秒かかっていた処理を1.5秒にできたので、快適な開発を手に入れることができたのは直接的な成果ではありますが、開発部全体のパフォーマンスが向上することで、間接的にサービスへの貢献できたのではないかと思います。

また、記事上ではサクサク解決しているように見えますが、詳細な設定ファイルの調整や挙動をしっかり調べて変更を行ったので、スキルの向上も図れたと思います。派手なことはしていませんが、こういう泥臭い作業の蓄積がスキルの蓄積になるものだと、個人的には思います。

ピクスタでは常に改善を繰り返し、サービスに貢献していきたいエンジニアを募集しています!!

recruit.pixta.co.jp

*1:Mac以外のOSでも希望があれば使うことができます

*2:逆にここがクリアできなければ、今回の修正は入れることができなかったと思います。

*3:LogLevelをdebugにすると、詳細な情報が見ることができます