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 にログを書き込むようにしたものです。

観点対応
・攻撃者が有効なユーザー名とパスワードのリストを持っているような場合に、クレデンシャルスタッフィング(パスワードリスト攻撃)のような自動攻撃を許してしまう

・総当たりやその他の自動攻撃を許してしまう。

・多要素認証がない、または効果がない。
・可能な場合多要素認証を実装して、自動化されたクレデンシャルスタッフィング(パスワードリスト攻撃)、総当たり攻撃、そして盗まれたクレデンシャルを再利用する攻撃を防ぐ。
・パスワードとして、デフォルトや、弱いもの、"Password1" や "admin/admin”といったよく知られたものを許可してしまう。

・平文や、弱く暗号化またはハッシュ化されたパスワードを保存している(参照: A02:2021-暗号化の不備)。
・特にアドミンユーザーについて、デフォルトのクレデンシャルのまま出荷やデプロイしない

・弱いパスワードのチェックを実装する。上位10000件の弱いパスワードのリストと照らし合わせる。

・パスワードの長さ、複雑さ、およびローテーションのポリシーを、米国標準技術局 (NIST) の800-63 bガイドライン (5.1.1 記録された秘密) またはその他の最新のエビデンスベースのパスワードポリシーに合わせる。
・弱い、もしくは効果のないクレデンシャルのリカバリーとパスワード再設定を行ってしまう。例えば知識ベースの回答は、安全でない。・登録、クレデンシャルのリカバリー、APIの経路が、全ての結果に対して同じメッセージを返すことで、アカウント列挙攻撃に対して対策されている。
・失敗したログイン試行に対して、制限をかけるか増加的な遅延時間をつける。しかしDoSのシナリオを作成しないよう注意する。クレデンシャルスタッフィング、総当たり、その他の攻撃を検知した場合は全ての失敗を記録し、管理者に通知する。
・URLにセッション識別子が公開されている。
・ログイン成功後にセッション識別子が再利用されている。
・セッション識別子を正しく無効化していない。ユーザーセッションや認証トークン(主にシングルサインオンのトークン)がログアウト後や一定時間操作がない場合に正しく無効化されていない。
・サーバーサイドの、安全な組み込みのセッション管理を利用して、ログイン後に高いエンロトピーで新たにランダムなセッションIDを生成する。セッション識別子はURLに含めず、安全に格納し、ログアウトや無操作時、タイムアウト時に無効化する。


以下のように、MIX_ENV=prod を指定してコンパイルおよび起動します。この指定をしたほうが、性能が大幅に良くなりました。
# MIX_ENV=prod mix compile
# MIX_ENV=prod PORT=4000 iex --name "[email protected]" --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” 列に表示される値
観点対応
・ソフトウェアとデータの完全性の不備は、完全性違反に対する保護のないコードとインフラストラクチャによる。この例として、アプリケーションが信用できないソース、レポジトリ、CDNから取得したプラグイン、ライブラリ、またはモジュールに依存している場合がある。
・安全でないCI/CDパイプラインは、不正アクセス、悪意あるコード、システム侵害の可能性をもたらす。現在の多くのアプリケーションは自動更新機能を備えており、更新情報が十分な完全性の検証なくダウンロードされ、前の信頼できるアプリケーションに適用されてしまう。攻撃者は自らの更新情報をアップロードし、それらが配布され全てのインストールで実行されてしまう可能性がある。・デジタル署名やその他のメカニズムを使うことで、ソフトウェアやデータが期待通りのソースから得られており改変もされていないことを検証する。

・npmやMavenといったライブラリと依存関係が信頼できるレポジトリを利用していることを確認する。リスクが高い場合、内部の検証済みでよく知られたリポジトリを使うことを検討する。

・OWASP Dependency Check やOWASP CycloneDXといったソフトウェアサプライチェーンセキュリティツールを、コンポーネントが既知の脆弱性を含まないことを確認するために使う
・他の例として、オブジェクトやデータが攻撃者が閲覧・改変可能な構造にエンコードまたはシリアライズされると、安全でないデシリアライゼーションに対して脆弱である。・なんらかの形の完全性検証や改ざん・リプレイを検出するためのデジタル署名なしで、署名なしまたは暗号化されていないシリアライズされたデータが、信用できないクライアントに送られていないことを確認する。
・悪意あるコードや構成がソフトウェアパイプラインに入り込む機会を最小化するため、コードや構成の変更のレビュープロセスがある。
・CI/CDパイプラインに正しい分離、構成、アクセスコントロールがあり、ビルドやデプロイのプロセス中に流れるコードの完全性を保つ。


また、sample4.js の場合に、ログファイル上の欠損していないことを確認するために、sample4.js を5回実行しました。その結果は以下の通りです。
  • Requests by wrk
    • wrk コマンドの出力
  • Logs in access.log
    • access.logに記録されたログの行数
観点対応
・ログイン、失敗したログイン、高価値なトランザクション等の監査可能なイベントのログがない。・全てのログイン、アクセス制御やサーバーサイドの入力値バリデーションの失敗を、怪しい・悪意あるアカウントを特定できるようなユーザーコンテキストとともに記録し、後にフォレンジック解析できるよう十分な期間保持する。

・高価値なトランザクションに対して、改ざんや削除を防ぐための完全性制御を行うとともに、監査証跡を取る。例えば追加専用のデータベースや類似のものを使う。
・ログを、ログ管理ソリューションが簡単に処理できる形式で生成する。

・ログデータが、ロギングまたはモニタリングシステムへのインジェクションや攻撃を防ぐよう正しくエンコードされている。
・警告やエラーがない、または不十分であったり不明瞭なログメッセージを生成する。
・アプリケーションやAPIで怪しい行動のログがモニタリングされていない。

・OWASP ZAPのような動的アプリケーションセキュリティテスト (DAST) ツールによるペネトレーションテストやスキャンがアラートを引き起こさない。

・リアルタイムまたはそれに近いアクティブな攻撃に対して、アプケーションが検知、エスカレーション、アラートできない。
・DevSecOpsチームは、怪しい行動を検知し即座に対応するために、効果的なモニタリングとアラートを確立すべき。
・ログがローカルのみに保存されている。
・適切な閾値の変更や、対応のエスカーレションプロセスがない、または効果的でない。・米国標準技術局 (NIST) 800-61r2 以降のような、インシデント対応と復旧のプランを採用または策定する。

考察

現実的にありそうな組合せ(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 などの特徴的な機能もあり、サーバサイドで使える場面がありそうです。今後も色々試してみたいと思います。

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

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

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事