Amethyst を試してみる

Ruby の文法をベースにした静的型付コンパイル型言語 [crystal](http://crystal-lang.org/) とその web アプリケーションフレームワークである [Amethyst](https://github.com/Codcore/amethyst) を使ってみます。なおエディタには Atom と [language-crystal-actual](https://atom.io/packages/language-crystal-actual), [linter-crystal](https://atom.io/packages/linter-crystal) パッケージを使いました。

### crystal インストール
crenv を使って 0.9.1 をインストールします。

### amethyst + active_record インストール
crystal では shards という bundler 風パッケージ管理ツールがあります。
shards.yml というファイルに依存関係を定義します。なぜかファイル形式が crystal ではなく yaml です。

```yaml
dependencies:
  amethyst:
    github: Codcore/amethyst
    branch: master
  activerecord:
    github: waterlink/active_record.cr
    branch: master
```
github のレポジトリを直接指定する形です。インストールコマンドは言語組み込みです。

```bash
$ crystal deps
Updating https://github.com/Codcore/amethyst.git
Updating https://github.com/spalger/crystal-mime.git
Updating https://github.com/waterlink/active_record.cr.git
Installing amethyst (master)
Installing mime (master)
Installing activerecord (master)
```
依存関係を辿って crystal-mime というパッケージもインストールされました。

### Hello World
では最小のアプリを作ってみましょう。以下のようなコードを src/hello.cr として保存します。
Model, View は使わずに Controller だけです。Amethystではディレクトリ構造は厳密に定義されてはいないようです。

```rb
require "amethyst"

class HelloController < Base::Controller
  actions :world

  def world
    html "hello world!"
  end
end

class HelloApp < Base::App
  routes.draw do
    get "/world", "hello#world"
    register HelloController
  end
end

app = HelloApp.new
app.serve
```
Appクラスというのがあります。これはrailsでいう config/application.rb やroutesに相当する箇所だと思います。アプリケーションの設定やルーティングを定義します。

なお Atom と linter-crystal を使った場合、linter が最初の`require`の時点で amethyst ファイルが見つからないというエラーを返してきます。この問題はlinterが`crystal build`を実行するときにプロジェクトのディレクトリではなく異なるディレクトリから実行するためです。crystal は pwd にある shards しか読み込まないため、インストールされた amethyst のソースコードが読み込まれません。とりあえず回避するために ~/.atom/packages/linter-crystal/lib/linter-crystal.js を編集して、exec の直前で `process.chdir("/path/to/project")` することで回避しましたが他のプロジェクトに使えなくなるので一時的な対策です。

#### 実行
```
$ crystal run src/hello.cr
[Amethyst 0.1.7] serving application "hello" at http://127.0.0.1:8080

   [Amethyst 0.1.7] serving application "hello" at http://127.0.0.1:8080
```
HTTPサーバが起動しました。ここでブラウザで http://127.0.0.1:8080/hello/world にアクセスすると無事 "hello world!" と表示されました。コンソールには何も表示されません。

私のように ruby から crystal に移行を検討している人は速度に関心があると思うので、速度を計測してみます。
Amethyst では rack middleware のように簡単に middleware を追加することができます。`routes.draw`のブロックの末尾に以下の行を追加します。

```rb
use Middleware::TimeLogger
```
なぜ routes の中に書くのかはよくわかりません。再度`crystal run` してブラウザでアクセスすると今度はコンソールに以下のように表示されます。

```bash
$ crystal run hello.cr 
[Amethyst 0.1.7] serving application "hello" at http://127.0.0.1:8080

   [Amethyst 0.1.7] serving application "hello" at http://127.0.0.1:8080

____/ #<Amethyst::Middleware::TimeLogger:0x2231b40> \__________________________________________________________________

   Time elapsed    :  0.0530 ms
```
私の戦闘力は 53μs です。wrk での計測も行いましょう。

```
$ ./wrk -t12 -c12 -d30s http://127.0.0.1:8080/hello/world
Running 30s test @ http://127.0.0.1:8080/hello/world
  12 threads and 12 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.84ms   13.47ms 174.71ms   93.96%
    Req/Sec     1.33k   330.76    12.28k    87.28%
  475947 requests in 30.10s, 131.18MB read
Requests/sec:  15813.05
Transfer/sec:      4.36MB
```
15,813req/sec です。興奮する数字を叩きだしてくれましたね。

#### ビルド
上記は crystal コマンドから直接実行しましたが、コンパイルしてバイナリを生成することもできます。
ビルドも`crystal`コマンドを使います。

```bash
$ time crystal build src/hello.cr 

real	0m1.460s
user	0m1.916s
sys	0m1.792s
$ 
```
Core i7-2600 使用で1秒以上かかりました。これは大きなアプリの開発ではかなり問題になりそうな予感がしますね。crystalではjavaのようにソースコード一つひとつがクラスファイルに変換されるような形式ではなく、コンパイル時に指定したファイルを起点としてバイナリを一つ作成する形式になっています。そのため差分コンパイルがおそらくできないのではと思います。そうなるとこのコンパイル速度は重くのしかかってきます。

今度はバイナリが生成されているので、これを直接実行できます。

```bash
$ ./hello
```
速度は特に変わりませんでした。

#### release ビルド
実は上記のバイナリは、ただの開発用のバイナリです。ビルド時に`--release`オプションをつけることで、デバッグ用の情報などを省いたより高速なバイナリを生成できるのです。お待たせしましたね。。。さぁて、第２回戦と行きましょうか。。。

```
$ time crystal build --release src/hello.cr 

real	0m3.351s
user	0m3.428s
sys	0m0.072s
```
コンパイル速度はさらに遅くなりました。しかたないですね。では起動してみましょう。

```
Running 30s test @ http://127.0.0.1:8080/hello/world
  12 threads and 12 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.85ms   21.87ms 270.76ms   91.85%
    Req/Sec     1.88k   555.39     2.47k    82.79%
  667408 requests in 30.03s, 183.95MB read
Requests/sec:  22221.38
Transfer/sec:      6.12MB
```
今度は22,221req/sです。40%ほど向上しました！

#### production モード
Amethyst には production モードというのがあり、いくつかのmiddlewareが省略されるようですが、これを有効にしても速度には特に変化は無しでした。


（追記）
これは該当箇所が以下の様な条件判定になっていたため、environmentに何も指定しなかったら productionと同じ挙動になっていた、ということでした。これは良くないですね。

```
if self.class.settings.environment == "development"
```
（追記終わり）

#### マルチコア
マルチコアをどの程度活用できるのかも気になるところですが、wrk を並列度１でテストしても結果が 17248, 並列度 2 で 19443。上記の並列度１２の結果とそこまで変わりません。並列度を上げてもCPUの利用率は 200-300% の間を漂う感じで４コア全てを使い切ることはありませんでした。CPUを完全に使いきれるようになれば 30,000rps くらいまでは上がるのかな？と期待できます。一方で並列度１の時点で CPU を 200% (コア２つ分）使っており、これも理由はよくわかりません。

#### メモリ
ベンチマークで負荷をかけるとメモリの使用量がぐんぐん伸びていきます。確認した限り２Gまでは増えました。hello world でこの状態になると production で使うのは躊躇してしまいますね。ここまでメモリがぐんぐん増えてくるとGCにどれだけCPUを使ってるのかも気になりますね。今後の改善に期待です。もしかしたら上記の並列度1の時でCPU200%のうちの半分はGCなのかも？

(追記）
メモリが増え続ける原因はセッションでした。現在のバージョンではセッションを全てオンメモリに記録しているので wrk からアクセスするとリクエストごとに新しくセッションが作成されてメモリが膨れ上がるようです。libs/amethyst/amethyst/base/app.crを編集して session middleware を使わないようにするとメモリの増加が解消されました。ただ、現時点ではまだセッションの削除が実装されていないのでこの問題は遅かれ早かれ発生します。

また、やはり CPU200%のうちの半分はGCだったようで、wrk並列度1でのアクセス時にはCPU100%程度しか使わないようにもなりました。一方で、並列度をあげてもCPU100%を超えることはなくなりました。現在はまだシングルスレッドでの挙動なんですね。wrkの並列度を上げると性能が多少あがるのは node と同じように非同期IOを使っているということかな。

加えて、新規セッションを作成するときは SecureRandom で session id を生成しているのですが、この程度の処理がベンチマークには大きく影響するようで、固定文字列を返すだけにしてみると性能が 1.5倍ほどになります。
（追記終わり）

### まとめ
組み込みの http サーバ単体で 200,000 rps くらいでるという話を聞いていたのですが、フレームワークを挟むとやはり桁が落ちるようですね。それでも rails よりはひと桁速いので良しとしましょう。