2016.12.21

Elixir & Phoenix は意外と広告向けサーバに向いてる?


Phoenix のロゴ

この記事は Elixir (その2)とPhoenix Advent Calendar 2016 の24日目です。

次世代システム研究室の DevOps ネタ担当の M.Y. です。いつもは Ops 寄りのことを書いてますが、今回は Dev 寄りの話題です。

最近、同僚に「Elixir は良いぞ」と熱弁されたのをきっかけに Elixir を触り始めました。プログラミング Elixir、Programming Phoenix、Phoenix のチュートリアルを読んでから、簡単な API サーバを書いて遊んでいます。

Elixir は関数型言語、かつコンパイラ言語なので、敷居が高く、ちょっとした Web アプリを作るだけなら Ruby on Rails とかを使いたくなってしまいます。ただ、ちょっとした JSON を配信するサーバを作るくらいであれば、コード量が少ないため、敷居が高いというデメリットよりもパフォーマンスが良いというメリットの方が効いてきます。

次世代システム研究室は、GMO アドパートナーズグループのアドテク商材に関わっており、私自身もその一部で開発に参加しています。その経験から考えて、Elixir & Phoenix は意外と広告向けのアプリケーションサーバに向いているのではないか、という気がしています。

そこで今回は、Phoenix を使って実際に簡単なアプリケーションサーバを実装し、その性能特性を調べてみました(コードもあるよ)。

広告向けのアプリケーションサーバとは?


次世代システム研究室は、過去に GMO MARS DMP や TAXEL といった広告向けのシステム開発に参画しています(参考:次世代システム研究室の参加プロジェクト)。

広告向けのシステムには、HTTP リクエストに含まれる URL パラメータに応じて JavaScript や JSON を返す、軽量なアプリケーションサーバが必要になります。例えば、HBase×Impalaで作るアドテク 「GMOプライベートDMP」 にて弊社エンジニアが講演した事例では、アプリケーションサーバを Play Framework で実装しています。この記事では、そのようなアプリケーションサーバを対象とします。

Elixir と Phoenix について


Elixir は、Erlang VM (BEAM) 上で動作するプログラミング言語です。Ruby に似た文法を持っているのですが、Ruby とは違ってコンパイラ言語で、高速に動作します。

Phoenix Framework(以下、Phoenix)は、Ruby における Ruby on Rails のような Web アプリケーションフレームワークです。元 Rails 開発者が開発したので使い勝手はとても Rails に似ているのですが、Elixir の提供する機能を使って改善されています。例えば、Phoenix は Plug というフレームワークをベースに作られており、暗黙的な継承関係が少ない、といった特長があります。このあたりの細かい話は Programming Phoenix に詳しく書いてありましたので、興味のある方には本書をお薦めします。

Phoenix に関しては、2014年7月に Chris McCord(Programming Phoenixの著者)が Rails との性能比較を行い、「Phoenix は Rails より10倍近く速い」という結果を発表しています(Elixir vs Ruby Showdown – Phoenix vs Rails)。この比較を他のフレームワークにも広げた mroth/phoenix-showdown: benchmark Sinatra-like web frameworks では、Phoenix は JVM で動作する Play Framework と比較しても同等に高速かつレイテンシのブレがないという結果が出ています。

ただ、性能比較の内容を確認すると、これらはデータベースアクセスやログ出力をオフにした状態での比較のようです。実サービスでは当然これらも必要になるので、今回は、広告向けのアプリケーションサーバに必要な機能を全部付けた状態で性能を調べてみます。

広告向けのアプリケーションサーバに必要な機能


広告向けのアプリケーションサーバには、あまりリッチな機能は必要ありません。最低限必要なのは、以下のような機能です。

  • データベースからの高速なデータ読み込み
  • 読み込んだデータのキャッシュ
  • アクセスログの記録
  • JSON, JSONP, JavaScript などの生成

プロジェクトごとに採用するソフトは多少違いますが、例えば以下のような組合せを採用しています。また、Web アプリケーションフレームワークは、Hadoop クラスタと接続することもあり、Play Framework を採用することが多いです。

  • データベースからの高速なデータ読み込み → HBase
  • 読み込んだデータのキャッシュ → Ehcache や Memcached
  • アクセスログの記録 → logback および Flume
  • JSON, JSONP, JavaScript などの生成 → Play Framework のテンプレートエンジン

これらの機能を、Elixir & Phoenix で実現する方法を考えてみます。

データベースからの高速なデータ読み込み


Phoenix には Ecto というモデル層があり、RDBMS をサポートしています。しかし、広告向けのアプリケーションサーバでは、レイテンシを小さくするために、HBase や KVS を採用します。

Elixir から HBase を使うためのライブラリとしては、以下の2つが見つかりました。HBase を使うニーズはあまりないのか、開発は活発ではないようです。


実装方法はそれぞれ異なっており、Diver は Java Server を hidden Erlang node として起動し、その Java Server を通して HBase cluster と通信します。HBasex は自分自身で HBase の REST および Thrift インターフェイスと通信します。awesome-elixir というリストには前者(Diver)が載っていること、および HBasex は少し試した程度では動かせなかったことから、今回は Diver を使いました。

読み込んだデータのキャッシュ


Play Framework で採用されている Ehcache に似たローカルキャッシュとして、Cachex というライブラリがありました。継続的に開発されており、かなり多機能なようです。


Memcached や Redis のライブラリも探してみました。コミット数が同程度のライブラリがいくつか見つかり、いずれが決定版なのかはまだわかっていません。


今回は初めての評価なので、Cachex を使ってみました。Memcached や Redis との接続もいずれ試してみたいです。

アクセスログの記録


私達のシステムでは Hadoop を使うという都合から、logback で数分単位でローテートしたファイルを Flume で HDFS に転送しています。同じようなことができないか調べてみたのですが、Elixir ではローテート可能なログ出力機能が見当たりませんでした。

Elixir のロガーは “backend” を追加することで拡張可能です(参考:Elixir のマニュアル)。ただ、Elixir の標準ライブラリには、標準出力にログを出力する console backend しか含まれていません。

色々探してみたところ、ファイルにログを出力する backend としては、唯一 LoggerFileBackend が見つかりました。ただ、これにはログローテーション機能は無いようです。


LoggerFileBackend のページに以下の記載があったため、今回は Elixir 外の機能(logrotate など)を使ってログローテーションを行うことを想定し、1個のファイルにすべてのアクセスログを出力します。

A simple Logger backend which writes logs to a file. It does not handle log rotation for you, but it does tolerate log file renames, so it can be used in conjunction with external log rotation.


JSON, JSONP, JavaScript などの生成


Play Framework の場合は、フレームワークに含まれるテンプレートエンジンを利用しています。

Phoenix の場合は、Poison というライブラリで JSON を生成して、テンプレートエンジンで出力します。Programming Phoenix によると、Phoenix のテンプレートエンジンはテンプレートの内容を連結リストとして保持しており、コストの高い文字列結合をしていないので高速、とのことです。

今回は、現実にありそうなケースに近づけるために、JSONP 形式でデータを返すことを考えます(参考:JSONP – Wikipedia)。

性能評価のためのサンプルプログラム


サンプルプログラムの機能


広告向けのアプリケーションサーバを実装する場合、どれくらいの性能が出て、どこがボトルネックになるのかを調べるために、簡単なサンプルプログラムを実装しました。ソースコードは GitHub にて公開しています。実装の詳細に興味のある方は、こちらをご覧ください。


このプログラムは、

http://server:4000/api/sample1.js?id=1&callback=Example.process

のようなURLにアクセスすると、id に対応する設定(複数の URL)を HBase から取得し、callback の名前を持つ関数でラップした JSON を返します。
Example.process({"urls":["http://ad1.example.com/tag.js","http://ad2.example.com/tag.js"]});
このレスポンスを受け取った側で、JSON に含まれる URL から script タグを生成する、といったユースケースを考えてください。

また、性能評価のために、URL ごとに内部動作を変えています。現実的にありそうな組合せは sample4.js の組合せです。sample1.js ~ sample3.js は機能を減らしたもの、sample5.js は敢えて HBase にログを書き込むようにしたものです。

URLConfigsLocal CacheAccess Log
sample1.jsFixed valueNoNo
sample2.jsRead from HBaseNoNo
sample3.jsRead from HBaseYes, AlwaysNo
sample4.jsRead from HBaseYes, AlwaysYes (File)
sample5.jsRead from HBaseYes, AlwaysYes (HBase)


以下のように、MIX_ENV=prod を指定してコンパイルおよび起動します。この指定をしたほうが、性能が大幅に良くなりました。
# MIX_ENV=prod mix compile
# MIX_ENV=prod PORT=4000 iex --name "myserver@127.0.0.1" --cookie "mycookie" -S mix phoenix.server

環境


GMO アプリクラウドの SS High-CPU ver2.1(4vCPU (2.40GHz), 8GB RAM)3台を使って実験しました。3台の役割は以下の通りです。サンプルアプリを実行するサーバと、それ以外のサーバは分けました。

  • サンプルアプリ(Phoenix)を実行するサーバ
  • ベンチマークツール(wrk)を実行するサーバ
  • HBase を実行するサーバ

使用したソフトウェアのバージョンは以下の通りです。

  • Erlang/OTP 19
  • Elixir 1.3.4
  • Phoenix 1.2.1
  • Diver 0.2.0
  • Cachex 2.0.1
  • LoggerFileBackend 0.0.9

測定方法


phoenix-showdown と同様に、ベンチマークツール “wrk” を使って性能測定を行いました。各条件で3回ずつ実行し、スループットが2番目に良い結果を採用しました。

# wrk -t20 -c100 -d30S --timeout 1000 --latency "http://myoshiz-ex-1:4000/api/sample1.js?id=1&callback=EXAMPLE.process"

パラメータは phoenix-showdown とほぼ揃えました。ただし、以下の点で修正を加えています。

  • timeout オプションの値を 2000 から 1000 に変更した。–timeout 2000 とすると、RAM 8GB のマシンではメモリ不足で起動できなかったため。timeout オプションの単位は秒なので、30秒しか測定しない(-d30S)なら問題にならない。
  • レイテンシの情報を詳しく出力するために –latency オプションを追加した。

測定結果


wrk による測定結果は以下の通りです。Throughput などは phoenix-showdown と同じ基準で表示しています。測定結果の詳細は Benchmark Results に置いておきました。

  • Throughput (req/s)
    • wrk の実行結果で、”Requests/sec” に表示される値
  • Latency (ms)
    • wrk の実行結果で、”Latency” 行の “Avg” 列に表示される値
  • Consistency (σ ms)
    • wrk の実行結果で、”Latency” 行の “Stdev” 列に表示される値

URLThroughput (req/s)Latency (ms)Consistency (σ ms)
sample1.js12636.188.365.75
sample2.js8063.6112.423.10
sample3.js11556.059.075.91
sample4.js2883.6134.704.96
sample5.js93.141070.00129.72


また、sample4.js の場合に、ログファイル上の欠損していないことを確認するために、sample4.js を5回実行しました。その結果は以下の通りです。

  • Requests by wrk
    • wrk コマンドの出力
  • Logs in access.log
    • access.logに記録されたログの行数

Requests by wrkLogs in access.log
8671786817
9298093076
9074890847
8672986828
9114391242


考察


現実的にありそうな組合せ(sample4.js)で、十分な性能が出ることを確認できました。また、他の組合せの結果と比較すると、以下のような傾向があることがわかりました。

HBase からの get を追加すると、レイテンシが 4ms ほど増加


sample1.js と sample2.js の結果を比較すると、レイテンシが 4ms ほど増加することがわかりました。一方で、sample2.js と sample3.js の結果から、ローカルキャッシュへのヒット率が高ければその影響はかなり緩和されることもわかりました。

実際のサービスに導入する場合、レイテンシは以下の点にも左右されそうです。

  • 実際の環境では HBase へのアクセスが2回以上必要になる可能性がある。この回数をうまく減らす必要がある
  • 今回はスペックの低いサーバで HBase を動かしているので、実際の環境では更にレイテンシが低くなるはず

ファイルへのログ出力を追加すると、レイテンシが 26ms ほど増加


sample3.js と sample4.js の結果を比較すると、ファイルへのログ出力はレイテンシを大幅に増加させることがわかりました。

実際のサービスに導入するなら、ログ出力についてはパフォーマンスチューニングが必要そうです。あるいは、ログ出力先をファイル以外にしたほうが良いかもしれません。sample5.js ではログ出力先として HBase を試してみたのですが、比較にならないくらい遅くなってしまいました。理由については未調査です。

sample4.js に対する測定を5回実行した結果を見る限り、ログの欠損はなさそうです。wrk の出力するリクエスト数よりも、ログの行数のほうが多くなっていますが、これは wrk が動作を終了したあとも、Phoenix 側はレスポンスを返しているためではないかと考えています。

まとめ


今回は、Elixir および Phoenix を使って実際に簡単なアプリケーションサーバを実装し、その性能特性を調べてみました。

公開されているライブラリを単純な組み合わせて実装するだけでも、それなりのレイテンシで動作することがわかりました。ただ、実際のサービスに導入するなら、アクセスログの記録についてはパフォーマンスチューニングが必要そうです。

Elixir については、今回触れなかった Channel などの特徴的な機能もあり、サーバサイドで使える場面がありそうです。今後も色々試してみたいと思います。


次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。