無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Phoenix Framework に関する有名なベンチマーク同士の関係

f:id:muziyoshiz:20161224204900p:plain

Phoenix Framework(以下、Phoenix)は、Elixir のための Web アプリケーションフレームワークです。

Phoenix の開発者 José Valim 氏は Ruby on Rails のコミッタだったため(Rails Contributors - #5 José Valim を見る限り、2014年まで?)、使い勝手はとても Rails に似ています。そのため、Phoenix は Rails とよく比較され、「Rails よりも10倍近く速い」という評判を時々目にします。

ただ、その評判の出処になったベンチマークについて、僕は具体的な内容を知りませんでした。また、記事によってベンチマークのリンク先がまちまちで、それぞれの関係がよくわかりませんでした。自分で Phoenix を使ってみるにあたり、このあたりを少し調べてみたので、その結果をメモしておきます。

最初のベンチマーク

Phoenix と Rails に関する最初のベンチマークは、Chris McCord(Programming Phoenix: Productive, Reliable, Fast の著者)による2014年7月のブログ記事のようです。

littlelines.com

この記事は「Phoenix は Rails より10.63倍速い」というベンチマーク結果を示しています。ちなみに、これは Elixir vs Ruby Showdown というシリーズの2番目のポストで、最初のポスト(Elixir vs Ruby Showdown - Part One)では「Ruby の i18n gem と比較して Elixir 版は73倍速い」という結果を出してます。

このベンチマークの内容は以下のようなものです。この特徴は、後述する他のベンチマークにも引き継がれています。

  • URL ‘/:title’ の :title 部分をコントローラに渡す
  • コントローラは、モデルの返り値を想定したマップ(固定値)の配列をビューに渡す
  • ビューは、受け取った配列を回して HTML を生成し、クライアントに返す
  • データベースへのアクセスは行わない
  • ログ出力は行わない
  • ENV = production で動作させる
  • ベンチマークツールには wrk を使って、30秒間テストする

具体的なコードの説明も載っているので、後述するベンチマークを読むにしても、まずこのブログ記事は読んでおいた方が良いです。

ベンチマーク対象の拡大

その後、このベンチマークに触発されて、ベンチマーク対象を他のフレームワークに広げたものが Matthew Rosenberg 氏により公開されました。

github.com

Phoenix は、Rails よりむしろ Sinatra に似た軽量フレームワークではないか、との理由から、比較対象は主に Sinatra にインスパイヤされたフレームワークから選ばれています。また、Phoenix の実装の基盤である Plug というライブラリを素で使った場合も追加されています。

  • Phoenix (Elixir)
  • Plug (Elixir)
  • Rails (Ruby)
  • Sinatra (Ruby)
  • Express, Express Cluster (JavaScript)
  • Martini (Go)
  • Gin (Go)
  • Play Framework (Java)

このテストは、Phoenix のバージョンアップに合わせて、何度か実施されています。

Round 1 と 2-3 はスペックが違うので単純に比較できません。

  • Round 1: 3.4GHZ Core i7 (quad core), 12GB RAM
  • Round 2-3: 4.0GHZ Core i7 (quad core), 32GB RAM

Round 1 はスペックが低いので、単純比較できるのは Round 2 と 3 のみです。Phoenix の結果だけ比較すると、Round 3 は 2 よりも若干遅くなっています。Plug の結果はほとんど変わっていないので、Phoenix に機能が増えたからでしょうか?

Round 3 の結果を見ると、Phoenix は Play より Consistency は優れているものの、Throughput は Play の約 0.47 倍となっています。Elixir は stop-the-world GC がないので Java より高速、と思っていたので、最初にこの結果を見たときはかなりがっかりしました。

高スペックなベアメタルサーバ上での結果

2015年7月に、上記の phoenix-showdown を Rackspace のベアメタルサーバ(CPU Dual 2.8 Ghz, 10 core, RAM 128 GB)で実行した結果が公開されました。

Comparative Benchmark Numbers @ Rackspace

スペックが高いほど Elixir の真価が発揮されるのか、このベンチマークでは、Phoenix の throughput は Play とほぼ同じか若干上で、Consistency では引き続き上回っています。Rails との差は更に開いて、Phoenix の throughput は Rails の15倍になっています。

感想(と職場ブログの宣伝)

素の状態では、Phoenix は Rails よりも10倍以上高速なのは確かなようです。ただ、他のコンパイル言語(Java や Go)と比較すると、throughput については決定的な差は無さそうです。

また、これらのベンチマークはデータベースアクセスやログ出力といった、普通のアプリには絶対にある機能を省いているので、周辺ライブラリの充実度によっては、開発者の多い Java & Play Framework で実装した方が、総合的には速くなるのかもしれません。Rails 開発者が多い環境なら Phoenix に飛びついてもよさそうですが、Java 開発者が多い環境では Play のほうが速い、ということもあるのかもしれません。

そのあたりが気になって、Elixir (その2)とPhoenix Advent Calendar 2016 の24日目として、Phoenix で書いた簡単なアプリケーションサーバに、HBase アクセスや、ファイルへのログ出力を足したら、性能はどう変わるか、という話を書きました。時間の都合で Play との比較まではできていませんが、今後気が向いたらそこまで試してみたいと思っています。

recruit.gmo.jp

記事の公開後に教えてもらったページ

togetter.com

Exrm を使った Phoenix アプリケーションのデプロイ方法を ansible-elixir-stack から学ぶ

f:id:muziyoshiz:20161122004038p:plain

これまでのあらすじ

Elixir の世界には、Ruby での Ruby on Rails に相当する "Phoenix" という Web アプリケーションフレームワークがあります。しかし、Capistrano に相当するものは無くて、デプロイの考え方は Rails とはだいぶ違いそうです。

前回は Elixir の世界のビルドツール Elixir Release Manager (Exrm) で作った tarball をデプロイする方法について紹介しました。ただし、upgrade コマンドで無停止アップグレードしたいなら、ビルド環境には最新のソースコードだけでなく、アップグレードする前のバージョンのビルド結果も置いておかなければいけない(!)という話をしました。

muziyoshiz.hatenablog.com

今回は、この Exrm を使ったビルド方法の話の続きです。

Phoenix アプリケーションの情報に関するネット上の情報

Phoenix アプリケーションのデプロイについてネット上の情報を探したところ、(検索ヒット数はかなり少なかったのですが)ansible-elixir-stack という Ansible role と、その紹介記事が見つかりました。この Ansible role のコードを読んでみたところ、デプロイ方法がやっと理解できたので今回ご紹介します。

ansible-elixir-stack の使い方

github.com

ansible-elixir-stack は ansible-galaxy からインストールできます。

$ ansible-galaxy install HashNuke.elixir-stack

Phoenix アプリケーションの mix.exs に exrm を追加してから、

$ curl -L http://git.io/ansible-elixir-stack.sh | bash

を実行すると、その Phoenix アプリケーションのディレクトリ内に、Ansible の実行に必要なファイル(playbook や inventory など)が自動生成されます。自動生成された playbook は ansible-elixir-stack を呼び出して、inventory ファイルに記載されたサーバに Phoenix アプリケーションをデプロイします。

この ansible-elixir-stack という role は基本的にオールインワンなので、Nginx サーバなども自動的にインストールしてしまいます。開発環境の構築に使うならこのままでいいかもしれませんが、本番環境を構築する場合、これを参考に独自の playbook を書く必要がありそうです。

ansible-elixir-stack を実行するための手順については、以下のブログ記事で詳しく紹介されています。そのため、この記事では特に触れません。

https://blog.johanwarlander.com/2015/07/30/deploying-a-phoenix-application-using-ansible-elixir-stackblog.johanwarlander.com

ansible-elixir-stack は自動アップグレードをどうやって実現しているのか?

deploy_type 変数での動作の切り替え

ansible-elixir-stack を普通に使うと、前回のブログ記事に書いた「方法1. ソースコードをサーバに置いて、mix phoenix.server で起動」と同じように、サーバを1回停止して、再起動します。

ただし、deploy_type という変数に "upgrade" という値をセットしておくことで、無停止アップグレードの動作に切り替わります。この動作の切り替えについては Hot code-reloading のページ に記載されていました。今回はこちらの動作を解説します。

role 内部で実行されるコマンド

ansible-elixir-stack では、初回デプロイ時の playbook(setup.yml)と、2回目以降のデプロイ時の playbook(deploy.yml)が分かれています。ただ、いずれの場合も project.yml の以下の部分で git clone を実行し、サーバ上に Phoenix アプリケーションのソースコード一式をダウンロードします。

- name: "clone project"
  git:
    repo: "{{ repo_url }}"
    version: "{{ git_ref }}"
    dest: "{{ project_path }}"
    accept_hostkey: True
    force: True
    remote_user: "{{ deployer }}"

デフォルトでは project_path は /home/deployer/projects/{{ app_name }} です。

そして、release.yml の以下の部分で、Phoenix アプリケーションをビルドします。ちなみに、mix は ~/.mix 以下にインストールされたファイルを使うため、bash -lc の指定は必須です。

- name: "compile and release"
  command: bash -lc 'SERVER=1 mix do compile, release' chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  environment:
    MIX_ENV: "{{ mix_env }}"
    PORT: "{{ app_port }}"

そして、最後の部分で、「git clone した最新バージョンのバージョン番号取得」、および「upgrade コマンドの実行」を行います。

- when: deploy_type == "upgrade"
  name: get app version
  command: bash -lc "mix run -e 'IO.puts Mix.Project.config[:version]'" chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  register: app_version

- when: deploy_type == "upgrade"
  name: set upgrade command
  set_fact: upgrade_command='rel/{{ app_name }}/bin/{{ app_name }} upgrade "{{ app_version.stdout }}"'

- when: deploy_type == "upgrade"
  name: upgrade app
  command: bash -lc "{{ upgrade_command }}" chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  environment:
    MIX_ENV: "{{ mix_env }}"
    PORT: "{{ app_port }}"

上記の upgrade コマンドの実行は、仮にアプリケーション名を sample_app、バージョン番号を 0.0.2 とすると、以下と同じ意味になります*1

$ cd /home/deployer/projects/sample_app
$ SERVER=1 mix do compile, release
$ rel/sample_app/bin/sample_app upgrade 0.0.2

デプロイ作業を常にこの playbook で実行しているなら、デプロイ先サーバの以下のディレクトリには、前回のバージョンのビルド結果が残っているはずです。

/home/deployer/projects/sample_app/rel/sample_app/release/0.0.1

そのため、前回のブログ記事で問題に挙げた「ビルド環境に、アップグレードする前のバージョンのビルド結果も置いておかなければいけない」という条件はクリアされます。

しかし、そう考えると新しく追加したサーバにいきなりバージョン 0.0.2 をデプロイする、という場合にはどうなるのか?が気になります。そういうケースでは、git clone しても上記の 0.0.1 ディレクトリは作られません。ただ、その場合はアプリはまだ動いていないため、単にバージョン 0.0.2 のアプリが新たに起動されるだけで、特に問題は起こらないようです。

この方法の懸念点

サーバごとのコードの状態の違い

Capistrano の場合は、通常はデプロイするたびに新しいディレクトリで git clone が実行されます。そのため、すべてのサーバで、余計なファイルがない環境で最新のコードが実行されるという安心感があります。

一方、上記の方法では、force=Yes で git clone が実行されるとはいえ、すべてのサーバ上のファイルが同じにはならない可能性があるのが気になります。まあ、そんなことを気にするなら、無停止アップグレードは諦めて Docker でも使え、という話かもしれません。

バージョン番号を上げるのを忘れそう

この方法に限らず、Exrm の upgrade コマンドを使う場合に共通した問題ですが、デプロイのたびに毎回必ず mix.exs 内のバージョン番号を上げて、git push する必要があります。ちょっとした更新のたびにこれを実行するのはかなり面倒です。

def project do
  [app: :hello_phoenix,
   version: "1.4.1",
   elixir: "~> 1.0",
   ...

この問題への対応として、ansible-elixir-stack の作者は Hot code-reloading のページ にて「バージョン番号の末尾に Git のコミットハッシュ値を自動的につける」という方法を提案しています。

def project do
  {result, _exit_code} = System.cmd("git", ["rev-parse", "HEAD"])

  # We'll truncate the commit SHA to 7 chars. Feel free to change
  git_sha = String.slice(result, 0, 7)

  [app: :hello_phoenix,
   version: "1.4.1-#{git_sha}",
   elixir: "~> 1.0",
   ...

もしこの方法を採用するなら、この対策は絶対に入れておいた方がよさそうです。

*1:SERVER=1 というのは ansible-elixir-stack が勝手に作ったフラグなので気にしないで OK です。

Exrm(Elixir Release Manager)を使った Phoenix アプリケーションのデプロイ

f:id:muziyoshiz:20161122004038p:plain

Elixir の勉強中

最近、職場の飲み会で同僚に「Erlang VM はいいぞ」と熱弁されたのをきっかけに、「プログラミング Elixir」を買って Elixir を触りはじめました。Elixir でサンプルコードを動かすだけだと身に付かなそうなので、「Phoenix で API サーバを書く」というのを当面の目標にしています。

Phoenix というのは Web フレームワークの名前で、Elixir 版の Rails みたいなものです。ディレクトリ構造なども Rails に近く、Rails 経験者にはとっつきやすい代物です。実際、MySQL から取得したデータをそのまま JSON で返すだけの単純な API サーバはすぐ書けました。

そして、この API サーバをレンタルサーバにデプロイしようとしたんですが、Phoenix には Capistrano に相当するものがないんですね。でもまあ、同じような処理を Ansible で書けば済むだろう……と最初は思ってたんですが、Erlang VM の機能をフルに使おうと思ったら Capistrano のような流儀ではうまくいかないことがわかってきました。今回はそんな話をします。

Phoenix アプリケーションのデプロイ方法

Phoenix のサイトにあるガイドでは、Phoenix アプリケーションを(Heroku とかではない)普通のサーバにデプロイする方法が2通り紹介されています。

方法1. ソースコードをサーバに置いて、mix phoenix.server で起動

1つ目の方法は、Deployment / Introduction にある方法です。

Phoenix アプリケーションをローカルで開発するときは mix phoenix server というコマンドで起動するのですが、本番環境でも同じように起動する、という方法です。以下のようにすると、デーモンとしても起動できます。

MIX_ENV=prod PORT=4001 elixir --detached -S mix phoenix.server

この方法は単純でわかりやすいのですが、アプリケーションの起動・停止や、決まったディレクトリへのログ出力、といった Web アプリで普通必要になるものを、自前で用意する必要がでてきます。

例えば、Phoenix アプリをデーモンとして起動した場合、PID を記録しておかないと停止できませんが、そういう処理を自分で書く必要があります。私の探した範囲では、How to reload the server in production. · Issue #1288 · phoenixframework/phoenix にある方法で、起動と同時に PID を記録できるようです。

elixir --detached -e "File.write! 'pid', :os.getpid" -S mix phoenix.server

また、ログ出力についても、onkel-dirtus/logger_file_backend などを使って、出力先ファイルを指定しておく必要があります。

方法2. Exrm でビルドした結果をサーバに置いて、Exrm が自動生成したスクリプトで起動

そしてもう1つは、Deployment / Exrm Releases にある方法です。

Elixir Release Manager (Exrm) というのは、Elixir で書かれたアプリケーションを、配布可能な tarball にまとめるためのビルドツールです。開発マシンやビルドサーバ上で mix release コマンドを実行すると、以下のようなディレクトリ構成でファイルが生成されます。ここでは、ビルドしたアプリケーションの名前を、仮に admiral_stats_api とします。

rel
└── admiral_stats_api
    ├── bin  (アプリケーション管理用のスクリプト群)
    │   ├── admiral_stats_api
    │   ├── admiral_stats_api.bat
    │   ├── install_upgrade.escript
    │   ├── nodetool
    │   └── start_clean.boot
    ├── erts-8.1  (Erlang ランタイム)
    │   ├── bin
    │   ├── doc
    │   ├── include
    │   ├── lib
    │   └── man
    ├── lib  (アプリケーションが依存するすべてのモジュール)
    └── releases
        ├── 0.0.1
        │   ├── admiral_stats_api.bat
        │   ├── admiral_stats_api.boot
        │   ├── admiral_stats_api.rel
        │   ├── admiral_stats_api.script
        │   ├── admiral_stats_api.sh
        │   ├── admiral_stats_api.tar.gz (※)
        │   ├── start.boot
        │   ├── start_clean.boot
        │   ├── sys.config
        │   └── vm.args
        ├── RELEASES
        └── start_erl.data

上記のツリーで (※) を付けた admiral_stats_api.tar.gz には、このファイル自身を除く配布物すべてが入っています。この tarball をデプロイ先(仮に /var/www/admiral_stats_api とする)で解凍して、

$ bin/admiral_stats_api start

を実行するとサーバが起動し、

$ bin/admiral_stats_api stop

を実行するとサーバが停止します。ログファイルは、自動生成される log ディレクトリ以下に出力されます。

また、Exrm の凄い点として、アプリケーションの無停止アップグレードが可能です。これは、Capistrano がやるような「一瞬止めて、シンボリックリンクの向き先を変えて、再起動」という無停止っぽいアップグレードではなくて、本当に無停止で、内部状態も含めてアップグレードする、という機能です。

例えば、新しいバージョン 0.0.2 をリリースしたいときは、さっきと同じように mix release で tarball を作り、その tarball をデプロイ先の /var/www/admiral_stats_api/releases/0.0.2/admiral_stats_api.tar.gz に置いてから、

$ bin/admiral_stats_api upgrade 0.0.2

を実行すると、以下のようなメッセージが表示されて、バージョン 0.0.2 が動作し始めます。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
Installed Release: 0.0.2
Made release permanent: "0.0.2"

単純な API サーバで使うにはオーバースペックな機能な気もしますが、せっかく Erlang VM を使うんだし、今回はこちらの方法でデプロイすることにしました。

しかし、自動化しようとするとうまくいかない(なんで??)

上記の手順をコマンドで手で打ちながら確認して、「なるほど完全に理解した。この手順を単に Ansible で自動化すればデプロイ自動化できるよな!」と思って、こういう Ansible playbook を書きました。

  • git clone で最新版をダウンロード
  • Git リポジトリ上に置いてない、パスワードなどを含むファイル(prod.secret.exs)を自動生成
  • コンパイル(mix do deps.get, deps.compile, compile
  • リリース用の tarball を作成(mix release
  • tarball をデプロイ先にコピー
  • tarball を /var/www/admiral_stats_api/releases/バージョン番号 に配置(解凍はしない)
  • upgrade コマンドを実行(bin/admiral_stats_api upgrade バージョン番号

で、この playbook を実行したところ、最後の upgrade コマンド実行のところでエラーメッセージが出て失敗しました。playbook と同じコマンドを手で打ったところ、こんなエラーメッセージでした。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
escript: exception error: no case clause matching
                 {error,{enoent,"/var/www/admiral_stats_api/releases/0.0.1/relup"}}

一体何なのこれ……。

Phoenix のサイトや Exrm のサイトを読んでも理由が分からず、「プログラミング Elixir」にもそれらしい説明はなく、エラーメッセージで検索した結果をひたすら探し回ってわかったのですが、どうやら

「0.0.1 から 0.0.2 にアップグレードするための tarball を作るためには、 rel/アプリケーション名/releases/0.0.1 ディレクトリ以下が残っている状態で mix release コマンドを実行しなければならない」

らしいです。

改めて確認したところ、0.0.1 ディレクトリがない状態で 0.0.2 の mix release を実行したところ、コンソール出力は以下のようになっていました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

その一方、0.0.1 ディレクトリがある状態で mix release を実行したところ、以下のように 0.0.1 から 0.0.2 へのアップグレードのためのファイルが生成されたことを示すメッセージが増えました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
This is an upgrade, verifying appups exist for updated dependencies..
==> All dependencies have appups ready for release!
==> Generated .appup for admiral_stats_api 0.0.1 -> 0.0.2
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

ここまでのまとめ:結局どうしたらいいの?

要するに、Exrm を使って Phoenix をリリースするためには、ビルド時に、少なくとも1つ前のバージョンのビルド結果をローカルに置いておく必要があります。upgrade コマンドを使ってアップグレードする限り、これは必須のようです。

世間の人はどうやって Exrm を使っているのか調べてみたところ、例えば andrewvy/ansible-elixir という role では、ビルド結果をすべて git push していました。ただ、Phoenix アプリのビルド結果には秘密情報(prod.secret.exs)も含まれるので、Phoenix アプリを OSS にするならこの手は使えません。秘密情報をすべて環境変数から読み込むように書き換えるという手はありますが、それでも、本来 Git に登録する必要がないファイルを Git に登録しなければならない、というデメリットは残ります。

Phoenix アプリケーションを開発してる人って、普通はどうやってデプロイしてるんでしょうか? Exrm なんて使わずに、mix phoenix.server で起動する方法を採用して、起動スクリプトは自分で書いてるんでしょうか。経験者の方にぜひ教えてほしいです……。

この記事の続き

続きを書きました。

muziyoshiz.hatenablog.com

参考ページ

参考書籍

プログラミングElixir

プログラミングElixir

第18章「OTP:アプリケーション」の18.6節「EXRM − Elixir のリリースマネージャ」に、Exrm の解説が8ページほど書かれています。内部状態をマイグレートするために必要なコードについても若干説明あり。

Programming Phoenix: Productive |> Reliable |> Fast

Programming Phoenix: Productive |> Reliable |> Fast

まだざっと読んだだけですが、デプロイに関する話題は(少なくとも独立した節は)なさそうです。

艦これアーケードのプレイデータ管理ツール Admiral Stats のソースコード公開のお知らせ

f:id:muziyoshiz:20161011225236p:plain:w600

ソースコード公開しました

艦これアーケードのプレイデータ管理ツール "Admiral Stats" のソースコードを、GitHub で公開しました。https://www.admiral-stats.com/ で動いているのと全く同じものです。

github.com

先日正式リリースされた Ruby on Rails 5 で開発しています。Admiral Stats の開発に興味がある方や、動作の詳細に興味がある方はぜひご覧ください。

gitter のチャットルームも作りました

僕は本当につい最近まで知らなかったんですが、艦これブラウザ版には MyFleetGirls ってツールがあるんですね(完全に Admiral Stats の先人だ……)。ここの開発スタイルを参考にさせてもらって、gitter のチャットルームを作りました。

gitter.im

GitHub アカウントがあれば誰でも入れますので、ご意見・ご要望のある方はこちらにお寄せください。もちろん、お知らせ用の Twitter アカウント @admiral_stats や、GitHub の issue ページにお寄せいただいても OK です。

Admiral Stats の関連記事

Rails 5 での実装の解説と、Admiral Stats の利用状況の分析を過去に書きました。

muziyoshiz.hatenablog.com

muziyoshiz.hatenablog.com