RubyをFlightRecoderを使ってブラックボックス分析する

## はじめに

性能分析や障害分析で詳細なログ情報が取得たいときは良くあります。

私は普段はRuby製のバッチやWebアプリを本番で動かさないのであまり気にした事が無かったのですが、パッと思い付かなかったのでJFR(JDK Flight Recorder)をRubyに適用出来るかを試してみました。

[JFRというブラックボックス分析向けのJVMの仕組み](https://koduki.github.io/docs/book-introduction-of-jfr/site/01/01-what_is_JFR.html)で低遅延のプロファイラを常時本番環境のアプリに適用してCPUやメモリ、GCログはもちろんスレッドダンプやI/O、カスタムイベントを１ファイルに集約保存する仕組みです。
これによって、障害時の原因の特定をスピーディーに実施できます。

今回はTruffleRubyを使う事でJFRを有効にしてみました。

## TruffleRubyのインストール

まずはインストールです。rbenvとかで入れたくなるのですが、その場合JVM版が入らないのでGraalVMからインストールを行います。

[こちら](https://www.graalvm.org/downloads/)からCommunity版をインストールするか、[SDKMAN](https://sdkman.io/)を使ってインストールします。下記はSDKMANのケースです。

```bash
# GraalVMのインストール
$ sdk install java 19.3.0.r11-grl  
$ java -version 
openjdk version "13.0.1-BellSoft" 2019-10-15
OpenJDK Runtime Environment (build 13.0.1-BellSoft+9)
OpenJDK 64-Bit Server VM (build 13.0.1-BellSoft+9, mixed mode, sharing)
```

続いてTruffle Rubyのインストールです。ここからの手順はGraalVMのインストール方法に依存せず共通です。

```bash
$ gu install ruby  
$ ruby --jvm --version
truffleruby 19.3.0, like ruby 2.6.2, GraalVM CE JVM [x86_64-darwin]
```

インストールはこれで完了です。

## 検証アプリを作成

動作テスト用のアプリをSinatraで作成します。

```bash
$ bundler init
$ echo 'gem "sinatra"' >> Gemfile 
$ bundler install 
```

下記のように`app.rb`を書きます。

```ruby
require 'sinatra'

get '/' do
  'Hello world!'
end⏎
```

## JFRの取得

アプリを起動させてJFRを取得します。以下のようなJVMオプションをRubyに渡します。`--vm.XX:...`のようにする事でJVMオプションを指定できます。

```bash
$ ruby --jvm --vm.XX:StartFlightRecording=settings=profile,filename=myapp.jfr app.rb
```

下記のメッセージが出ればJFRが有効になっています。

```
Started recording 1. No limit specified, using maxsize=250MB as default.
Use jcmd 53009 JFR.dump name=1 to copy recording data to file.
```

プロセス終了時にmyapp.jfrという名前で作成されます。

## VisualVMで見てみる

JFRはバイナリなので[JDK Mission Control](https://adoptopenjdk.net/jmc.html)をインストールして解析したり、`jfrコマンド`でJSONに変換したりして分析します。
GraalVMにはjvisualvmが同梱されているのでこちらを利用することも可能です。VisualVMでJFRを開くと以下のような情報が取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/feddb4fc-6afd-5924-1217-450a2c7e9c9c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/1fbc6259-a6e3-0bfe-198d-7d5175c7d661.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/cdbd112b-b35e-efff-3ae9-8073deafe14d.png)

他にもGCログとかヒストグラムとかスレッドロックとか色々見れますが割愛。VisualVMのJFRサポートはまだ絶賛開発中なので、本格的に見るならJMCの方がUIもこなれていてオススメです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/53f924d0-5482-218f-b817-faef979771c6.png)

## カスタムイベントの追加

Flight Recorderにはカスタムタグと言って任意のイベントを追加する仕組みがあります。これを使うことでログを吐くのと同じ感覚でJFRファイルの中に情報を組み込めます。

以下のように`jdk.jfr.Event`クラスを継承したコードをJavaで書きます。

```java/src/profile/HttpRequestEvent.java
package profile;

import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;

/**
 *
 * @author koduki
 */
@Category({ "Application Profile" })
@Label("HTTP Request")
public class HttpRequestEvent extends Event {

    @Label("Method")
    public String method;

    @Label("URL")
    public String url;
}
```

続いてRubyでJavaコードを呼び出します。

```ruby
require 'sinatra'
Java.import 'profile.HttpRequestEvent'

get '/' do
  event = HttpRequestEvent.new
  event.begin
  r = 'Hello world!'
  event.end

  event.setUrl("/")
  event.setMethod("get")
  event.commit

  r
end
```

`Java.import`でJavaのコードをrequireします。JRubyとTruffleRubyで構文が少し違うのでご注意ください。
`begin`/`end`で囲んだ範囲の処理を`duration`として自動で記録します。その後、先ほどJavaのクラスで指定した任意のプロパティに値を入れる事ができます。

続いて以下のコマンドで実行します。

```bash
$ javac java/src/profile/HttpRequestEvent.java -d java/classes
$ ruby --jvm --vm.classpath=./java/classes/ --vm.XX:StartFlightRecording=settings=profile,disk=true,filename=myapp.jfr app.rb
```

新しいオプションとして`--vm.classpath`を足しています。こちらで作ったJavaのカスタムイベントをクラスパスに通します。

準備はできたのでabコマンドで適当にリクエストを投げてみます。

```bash
$ ab -n 5000 -c 10 "http://localhost:4567/" 
```

こちらrubyのプロセスを終了してmyapp.jfrを見てみると以下のようにカスタムイベントが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/8a8e32f0-e3eb-cd1c-9e7c-291c388c33f3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/834f873b-86dd-835e-3322-215ee2f50c7a.png)

今回は単純にリクエストを入れましたが障害時に便利な情報を入れておくと良いでしょう。ちなみに閾値の設定とかもできます。

ちなみにJMC以外で例えばMetabaseとかで分析したい場合は[2019-12-09
JFRをBigQuery/Metabaseのオレオレダッシュボードで可視化する](https://koduki.hatenablog.com/entry/2019/12/09/170528#Metabase%E3%81%A7%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%99%E3%82%8B)をご参照ください。


## まとめ

Rubyの本番時の障害分析はおそらくNewRelicなどのAPMを中心に行われてるのでは無いかと思われます。

ただスレッドロックとか細かい情報も見たいことは本番運用してたらありそうなので、JFRはAPMよりも障害分析に特化した機能が多くRubyでも使おうと思えば使えるのは助かります。

今回はビジネスロジックの中に直書きしましたが、Rubyなのでメソッド呼び出しをフックして自動で書き込むとかFW側にJFRのロジックを入れれば、グッと使いやすくなると思います。

TruffleRubyはパフォーマンスが注目されがちですが、こうしたJavaのエコシステムと連携できるのも魅力の一つかと考えています。

ただ、スタックトレースがRuby側まで追いきれないようなので、この辺はやはりRubyネイティブのものがあると良いですね。

それではHappy Hacking!

## 参考

- [Truffle Ruby - Polyglot Programming](https://github.com/oracle/truffleruby/blob/master/doc/user/polyglot.md)
- [Migration from JRuby to TruffleRuby](https://github.com/oracle/truffleruby/blob/master/doc/user/jruby-migration.md)