この記事は Akatsuki Advent Calendar 2018 の23日目の記事です。 前回は id:yunon_phys さんの、エンジニア組織の責任範囲の透明性をRACI図で高めてみた でした。
はじめに
アカツキではElixirを使ってゲームのAPIサーバを開発・運用しています。 ゲームのAPIサーバは、大量のリクエストを低いレイテンシで捌くことが要求されるため、Erlang VMの高いスケーラビリティが利用できるのは効果的です。加えてRubyなどを書き慣れている人にとっつきやすいElixirの文法も魅力です。
とはいえ、その性能を引き出すためには、やはりアプリケーションのパフォーマンスチューニングは不可欠です。 その際、"Don't guess, measure" という言葉の通り、どこを改善すれば良いかを知るための良い計測ツールが必要になります。
これまでRuby on Railsのアプリケーションでは、計測・監視のためにNew RelicをAPM (Application Performance Monitoring) ツールとして使っていたのですが、現在New Relicでは公式にはElixirはサポートされていません。*1
そこで今回は、インフラの監視に利用していたDatadogにAPMも統合できることを期待して、Datadog APMを利用してみることにしました。
Datadog APMでは単なる集計値のみならず、タイミングの記録にも対応しており、下記のように個々のAPI内の処理内容をフレームグラフとして見ることも可能なため、性能改善には有効そうです。
対象アプリケーションの構成
今回、計測対象となるアプリケーションは以下のライブラリを利用しています。
ElixirのWebフレームワークとしてはPhoenixがメジャーですが、今回はPhoenixは利用せず、Cowboyの上にAPIエンドポイントごとのハンドラを書いていくというシンプルな実装になっています。
APMへの対応
ElixirからDatadog APMを利用するのには、コミュニティで開発されている Spandex というライブラリを使用しました。 Spandexは様々なAPMに対応することを想定して開発されていますが、現在のところDatadogのみに対応しています。
導入
まずは、mix.exs
に依存ライブラリとしてspandex、および関連ライブラリを加えます。
def deps do [{:spandex, "~> 2.1.0"}, {:spandex_datadog, "~> 0.1.0"}, # Datadogアダプタ {:spandex_ecto, "~> 0.1.0"}, # Ectoを計測 {:decorator, "~> 1.2"}, ...
そして、必要な設定を config/config.exs
に追記します。 DATADOG_HOST
環境変数は、別途インストールしたDatadog Agentの接続先を指すようにしておきます。通常はlocalhostで良いのですが、コンテナ等で導入した場合はそのコンテナ名とします。加えて、Agentにも DD_APM_ENABLED=true
を渡してTracingのメッセージ(MessagePack形式)を受け取れるようにしておきます。
config :spandex, service: :sample, adapter: SpandexDatadog.Adapter, disabled?: {:system, "DD_APM_DISABLED", false}, env: Mix.env() config :spandex_datadog, host: {:system, "DATADOG_HOST", "localhost"}, port: 8126, batch_size: 10, sync_threshold: 100, http: HTTPoison config :spandex_ecto, SpandexEcto.EctoLogger, service: :ecto, tracer: SampleApp.Tracer, otp_app: :sample
spandex_ecto は性能計測値をログから取り出しているため、Ectoのログ出力先を追加しておきます。
config :sample_app, MyApp.Repo, adapter: Ecto.Adapters.MySQL, loggers: [{Ecto.LogEntry, :log, [:debug]}, {SpandexEcto.EctoLogger, :trace, ["db_name"]}], ...
最後に、自分のアプリケーションにトレース用のモジュールを作り、アプリケーション起動時に初期化するようにします。
defmodule SampleApp.Tracer do use Spandex.Tracer, otp_app: :sample_app defmodule Error do defexception [:message] end def start() do Config.fetch!(:spandex) |> configure() end # Mixタスク等でAPI外からアプリケーションを利用する場合、これを最初に呼んでトレースを無効化しておく def disable() do Config.fetch!(:spandex) |> Keyword.merge(disabled?: true) |> configure() end # APIでエラーが発生した際は、これを呼び出すことでエラーだったことやそのメッセージを記録する def report_error(payload) do update_top_span(error: [exception: %Error{message: inspect(payload)}]) end end
defmodule SampleApp.Application do use Application def start(_type, _args) do ... SampleApp.Tracer.start() ...
以上で SampleApp.Tracer
モジュールを通してAPMの計測が行えるようになりました。
各APIの計測の開始と終了
APIの処理の開始時と終了時に、それぞれ SampleApp.Tracer.start_trace()
, SampleApp.Tracer.finish_trace()
を呼び出します。
type > service > resourceという階層を持つキーワードをつけることで、Datadog APMのWeb UI上でトレースを分類して統計を取ることが可能になります。
加えて、これらの間に SampleApp.Tracer.start_span()
, SampleApp.Tracer.finish_span()
を呼び出すことで、その中で行われた個々の処理タイミングを記録することができます。この記録をspanと呼びます。
各spanはトレース画面でタイミングや所要時間を見られるほか、個々の集計も行われます。例えばEctoの発行しているクエリのうち、累計で最も時間を消費しているのはどれか?それはどのAPIから呼ばれているか?といった情報を知ることができます。他にも下記の http
など、メタデータを記録させることもできます。*2
ここではCowboyのミドルウェアとしてこれらの関数を呼び出すことにします。 また、他のミドルウェアでユーザ認証などを行なっているため、その所要時間がわかるよう、 "middleware" というspanを作っておくことにしました。
defmodule SampleApp.Middleware.Tracer do alias SampleApp.Tracer @behaviour :cowboy_middleware def execute(req, env) do # APIのトレースを開始するとともに、 middleware の span を作る meta = [url: req.path, method: req.method] Tracer.start_trace("request", service: :sample, type: :web, resource: req.path, http: meta) Tracer.start_span("middleware", service: :middleware, type: :middleware) {:ok, req, env} end end defmodule SampleApp.Middleware.Tracer.Start do alias SampleApp.Tracer @behaviour :cowboy_middleware def execute(req, env) do # middleware の span を閉じる Tracer.finish_span() {:ok, req, env} end end defmodule SampleApp.Middleware.Tracer.Finish do alias SampleApp.Tracer @behaviour :cowboy_middleware def execute(req, env) do # APIのトレースを終了する Tracer.finish_trace() {:ok, req, env} end end
これらがをミドルウェアとして呼び出されるようにCowboyに登録します。
:cowboy.start_clear( :sample, [...], %{middlewares: [ SampleApp.Middleware.Tracer, :cowboy_router, SampleApp.Middleware.Authorize, # 各種ミドルウェアが間に入る ... SampleApp.Middleware.Tracer.Start, :cowboy_handler, # ここでAPIのハンドラが呼び出される SampleApp.Middleware.Tracer.Finish ] } )
以上でアプリケーションの各APIがトレースできるようになりました!
計測対象の追加
Ectoの計測は追加済みですが、他にmemcachedや外部サービスの呼び出しなどをトレースしたい場合、それらの箇所にもspanを追加していきます。
トレース対象の前後で start_span()
, finish_span()
を呼び出してもいいのですが、
SampleApp.Tracer.span("name", service: :sample, ...) do ... end
のようにdoブロックで計測対象の処理を書くこともできます。spandex 2.4.0以降であれば、関数を丸ごとトレースする際はデコレータを使って
@decorate trace(service: :external_service, type: :custom, resource: ...) def trace_me() do ... end
のように、さらに簡単にトレース対象に加えることができます。
トレースの無効化
計測によるオーバーヘッドは、やはり多少なりともあります。そこで、必要ない時には簡単にトレースを無効化できるよう、環境変数 DD_APM_DISABLED=true
を渡せるようにしてあります。これを使って一部のホストだけでトレースを取るといった運用も可能です。
余談ですが、Spandex 2.0.0でトレースを無効化しようとすると例外が発生するというバグがありました。これを修正するPull Requestを送ったところ、すぐにマージしてもらえ、2.1.0では修正されました。些細ではありますがコントリビューションができて良かったです。
結果
集計結果はDatadog APMのWebページ上で閲覧します。サービス全体や個々のAPIについて、呼び出し頻度やレイテンシの分布、消費時間に占めるDBや外部サービスの割合などがひと目でわかります。
さらに下部にある個々のAPI呼び出しをクリックすれば、フレームグラフ形式でspanを見られ、さらにそこからspanにつけたメタデータの閲覧へと、細かくドリルダウンしていくことができます。
加えて、そのホストのメトリクスも同じ画面内で参照できるため、例えば特定のトレースが遅い時、CPU負荷は高かったのか?といった情報も簡単に得られるようになりました。これはインフラ監視とAPMを統合することのメリットと言えます。
終わりに
この記事では、ElixirのWebアプリケーションのAPIの性能をDatadog APMで計測する方法として、Spandexライブラリの使用方法を紹介しました。
実際に、APMを仕掛けた状態で開発中のアプリケーションのストレステストを行ったところ、N+1クエリなどの非効率な処理がいろいろと見つかり、中でも頻度が高いところを集中的に改善することができました。
それだけに留まらず、DatadogのメトリクスとしてAPMの集計値を利用できるため、個々のAPIの性能に対してアラームを設定したり、Watchdog機能を使って急激に呼び出し頻度が増えたAPIやクエリを自動検出することも可能になり、サービスの運用中にも便利に活用できそうです。