JFRをBigQuery/Metabaseのオレオレダッシュボードで可視化する

この記事は Java Advent Calendar 2019の9日目です。前日はSphereさんの「Visual Studio CodeでParaya MicroのWebアプリケーションを作る準備をする: すふぃあの記憶」でした。

はじめに

JDK Flight Recorderを皆さん使っていますか? JDK 9からAPIが整備されJDK 11からOpenJDKに寄贈されたので商用ライセンスを持っていなくても本番で自由に使えるようになりました。

常時プロファイルをonにして障害時にすぐに情報を取得できるブラックボックス分析はとても便利です。OpenJDK系ディス鳥の標準機能なので、まだ利用されてない方はぜひ使って見てください。

JFRはあまり詳しくないという方は、更新があまりできてないですが個人的にJFRの情報をまとめてるサイトも書き始めたので良ければご参考ください。

koduki.github.io

閑話休題。JFRはJDK Mission ControlないしはVisual VMで見るのが基本なのですが、対象のJFRファイル以外の情報と統合したくてデータを抽出したいことも良くあります。

事後の分析を複数環境に対してやりたいときやPVとの相関を見たいとき。そんな時は仕方ないのでExcelにコピペして集計したりしました。

これを解決しようとすると、以前は非公式APIを使うとかあまり便利な方法が無かったのですが、JDK9以降はAPIが標準化されて便利になったのでそのやり方を紹介したいと思います。なお、基本的にはCode Oneで喋ったのと同じ内容となります。

JFRを取得する

JFRを取得しないと始まらないのでJFRの取得をします。

WebアプリケーションだろうがバッチだろうがJFRの取得はJVMオプションを指定するだけで簡単に出来ます。

今回はQuarkusで作ったRESTベースのWebアプリです。

java -XX:StartFlightRecording=settings=profile,disk=true,filename=/var/log/myapp/myapp.jfr \
-XX:FlightRecorderOptions=repository=/var/log/myapp/reposistory / \
-jar myapp-runner.jar

-XXオプションを-jarの後に書いた場合は正常に動かないので注意してください。

ポイントはrepositoryオプションを使ってリポジトリを指定することです。filenameで指定するJFRファイルはプロセスの終了時かJFR.dumpコマンドの実行時にしか出力されません。

それだと、不便なことも多いので循環バッファの一部をディスクに出力するようにrepositoryオプションを指定すると特にWebアプリケーションでは便利です。

以前は必要だったUnlockCommercialFeaturesFlightRecorderオプションは不要です。また、JDK 8の頃とはJFRに必要なJVMオプションがJDK8から少し異なっています。例えば、以下のような差分があります。

JDK 8:

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:FlightRecorderOptions=defaultrecording=true,
dumponexit=true, dumponexitpath=/var/log/myapp/myapp.jfr

JDK 11:

-XX:StartFlightRecording=dumponexit=true,
filename=/var/log/myapp/myapp.jfr

JDK9のオプションがJava Flight Recorder Command Referenceで確認できるので見直しておくのが良いと思います。

起動したWebアプリケーションに負荷を掛けてみます。すると以下のようなディレクトリ構成でJFRファイルが出力されます。

$ tree /var/log/myapp/ 
/var/log/myapp/
├── myapp.jfr
└── reposistory
    └── 2019_12_01_23_43_51_89649
        ├── 2019_12_01_23_43_51.jfr
        ├── 2019_12_01_23_53_42.jfr
        └── 2019_12_01_23_53_42.part

myapp.jfrは基本的にはプロセスが終了するまで空なので、スローレスポンスの分析のために中間の状態を取得したい場合はrepository配下のファイルを取得することになります。障害時にはプロセスが終了するのでmyapp.jfrを取得することになるでしょう。

今回はプロセスを落とさずにJFRを取得したかったのでリポジトリから2019_12_01_23_43_51.jfrを取得しています。

とりあえずJMCで開いてみるとこんな感じです。

f:id:pascal256:20191202182654p:plain

JFRをJSONに展開する

続いて、JFRファイルを取り扱いやすい形式に変換しましょう。

JFRファイルはバイナリです。これはこれで便利なのですが、他のツールと連携する場合には不便です。なのでテキストベースの形式に変換します。

JFRはJDK JFR Consume APIを使うことでJavaを使ってJFRを解析出来ます。自分で解析PGを組んでも良いのですが、JFR Toolを使うことでXMLJSONに変換できるので今回はこちらを使います。

以下のコマンドでJFRファイルをJSONテキストに変換できます。

$ jfr print \
--json  \
--events jdk.GCPhaseParallel,jdk.ExecutionSample,jdk.CPULoad,jdk.GCHeapSummary \
2019_12_01_23_43_51.jfr > 2019_12_01_23_43_51.json

categorieseventsはJFRのイベントブラウザから確認できます。こちらの情報を使って内容をフィルタする事ができます。

JSONの中身は以下のようにJFRの値を単純に変換した形となります。

{
  "recording": {
    "events": [{
      "type": "jdk.GCConfiguration", 
      "values": {
        "startTime": "2019-12-01T23:43:51.141988376-08:00", 
        "youngCollector": "G1New", 
        "oldCollector": "G1Old", 
        "parallelGCThreads": 4, 
        "concurrentGCThreads": 1, 
        "usesDynamicGCThreads": true, 
        "isExplicitGCConcurrent": false, 
        "isExplicitGCDisabled": false, 
        "pauseTarget": "PT-2562047788015215H-30M-8S", 
        "gcTimeRatio": 12
      }
    }, {
      "type": "jdk.GCSurvivorConfiguration", 
      "values": {
        "startTime": "2019-12-01T23:43:51.141992948-08:00", 

容量を確認してみましょう。

$ ls -lh 2019_12_01_23_43_51.jfr 2019_12_01_23_43_51.json
-rw-r--r--  1 koduki  staff    18M Dec  1 23:56 2019_12_01_23_43_51.jfr
-rw-r--r--  1 koduki  wheel   25M Dec  2 20:36 2019_12_01_23_43_51.json

元が18MBだった展開すると25MBになっているのが分かります。あくまでフィルタをした状態のサイズで仮にフィルタをしない場合は3.2GBくらいのサイズになり、JFRの保存形式の効率の良さが伺えます。

また、events配列の中身だけあれば良いのとJSONはNDJSON(Newline delimited JSON)形式にする必要があるのでjqコマンドで変換します。

$ cat 2019_12_01_23_43_51.json | jq -c '.recording.events[]' > 2019_12_01_23_43_51_f.json

これでひとまずのJSONへの展開は完了です。

BigQueryに取り込む

では、BigQueryに先ほどのJSONを取り込みましょう。

まず先ほどのJSONをイベント毎のファイルに分割します。

$ echo "jdk.GCPhaseParallel jdk.ExecutionSample jdk.CPULoad jdk.GCHeapSummary" \
|xargs -P4 -n1 sh -c 'grep \"type\":\"$0\" 2019_12_01_23_43_51_f.json \
> logs/${0}.json'

イベント毎フォーマットが異なるので単純にBigQueryにアップロードしようとするとエラーになります。

INSERT文を使うか、別の形式に変換して可変部分をJSON文字列にしてSQLで解析するというELTっぽい手もあるのですが、それはそれで面倒なので今回はシンプルにイベント毎にテーブルを分けてロードすることにします。

続いてGCSにアップロードします。

# Create Bucket
$ gsutil mb gs://jfr-storage/  

# Upload
$ gsutil cp logs/*.json gs://jfr-storage/

# Check
$ gsutil ls -lh gs://jfr-storage/ 
 88.88 KiB  2019-12-03T08:42:43Z  gs://jfr-storage/jdk.CPULoad.json
  7.31 MiB  2019-12-03T08:42:56Z  gs://jfr-storage/jdk.ExecutionSample.json
 104.4 KiB  2019-12-03T08:42:56Z  gs://jfr-storage/jdk.GCHeapSummary.json
  5.37 MiB  2019-12-03T08:43:05Z  gs://jfr-storage/jdk.GCPhaseParallel.json
TOTAL: 4 objects, 13492203 bytes (12.87 MiB)

BigQueryにロードしていきます。

bq load --autodetect --source_format=NEWLINE_DELIMITED_JSON jfr_logs.jdk_CPULoad gs://jfr-storage/jdk.CPULoad.json
bq load --autodetect --source_format=NEWLINE_DELIMITED_JSON jfr_logs.jdk_ExecutionSample gs://jfr-storage/jdk.ExecutionSample.json
bq load --autodetect --source_format=NEWLINE_DELIMITED_JSON jfr_logs.jdk_GCHeapSummary gs://jfr-storage/jdk.GCHeapSummary.json
bq load --autodetect --source_format=NEWLINE_DELIMITED_JSON jfr_logs.jdk_GCPhaseParallel gs://jfr-storage/jdk.GCPhaseParallel.json

テーブルが作成されたのが確認できました。

$ bq ls jfr_logs
        tableId           Type     Labels   Time Partitioning   Clustered Fields  
 --------------------- ---------- -------- ------------------- ------------------ 
  jdk_CPULoad           TABLE                                                     
  jdk_ExecutionSample   TABLE                                                     
  jdk_GCHeapSummary     TABLE                                                     
  jdk_GCPhaseParallel   TABLE

Metabaseで可視化する

さて、ではBigQueryに取り込んだJFRのデータをビジュライズしていきましょう。 今回はMetabaseを使って可視化を行いました。GCPにMetabaseをインストールしてBigQueryと繋ぐ方法は下記の記事に別途まとめてるのでご参考ください。

qiita.com

JFRの解析して、とりあえずこんな感じの簡単なレポートを作ってみました。 f:id:pascal256:20191205171856p:plain

SQLは以下の感じで組んでいます。

今回は面倒だったので複数のJFRがテーブルに入ることは無い想定でSQLを組み立てています。実際はその辺を考慮してWHERE句等を書く必要があります。

CPU Load Summary

SELECT 
  values.startTime,  
  values.machineTotal, 
  values.jvmUser 
FROM `{project_id}.jfr_logs.jdk_CPULoad`

GC Summary

SELECT 
  values.name,
  SUM(CAST(REGEXP_REPLACE(values.duration, r"[PTS]", "") AS NUMERIC)) as total_duration, 
  count(1) as count
FROM `{project_id}.jfr_logs.jdk_GCPhaseParallel`
GROUP BY name

Heap Summary

SELECT 
  values.startTime as timestamp, 
  values.heapUsed 
FROM `{project_id}.jfr_logs.jdk_GCHeapSummary`

Methods Call

SELECT method_name, count(1) as count FROM
(
  SELECT 
    CONCAT(
      REGEXP_REPLACE(REGEXP_REPLACE(values.stackTrace.frames[OFFSET(0)].method.descriptor, r";.*|\(L|[\(\)]", ""), "/", "."), 
      ".", 
      values.stackTrace.frames[OFFSET(0)].method.name
    ) as method_name
  FROM `{project_id}..jfr_logs.jdk_ExecutionSample`
  UNION ALL
  SELECT 
    CONCAT(
      REGEXP_REPLACE(REGEXP_REPLACE(values.stackTrace.frames[OFFSET(1)].method.descriptor, r";.*|\(L|[\(\)]", ""), "/", "."), 
      ".", 
      values.stackTrace.frames[OFFSET(1)].method.name
    ) as method_name
  FROM `{project_id}.jfr_logs.jdk_ExecutionSample`
  UNION ALL
  SELECT 
    CONCAT(
      REGEXP_REPLACE(REGEXP_REPLACE(values.stackTrace.frames[OFFSET(2)].method.descriptor, r";.*|\(L|[\(\)]", ""), "/", "."), 
      ".", 
      values.stackTrace.frames[OFFSET(2)].method.name
    ) as method_name
  FROM `{project_id}.jfr_logs.jdk_ExecutionSample`
  UNION ALL
  SELECT 
    CONCAT(
      REGEXP_REPLACE(REGEXP_REPLACE(values.stackTrace.frames[OFFSET(3)].method.descriptor, r";.*|\(L|[\(\)]", ""), "/", "."), 
      ".", 
      values.stackTrace.frames[OFFSET(3)].method.name
    ) as method_name
  FROM `{project_id}.jfr_logs.jdk_ExecutionSample`
)
GROUP BY method_name
ORDER BY count desc
LIMIT 10

簡単なSQLのわりにはそれっぽいレポートが出来てるのは無いでしょうか?

以前はJFRをES + Kibanaでビジュアライズしましたが、BigQuery + Metabaseの方が圧倒的に楽です。

カスタムイベントを追加する

WeblogicならWLDFの情報がJFRに入っていて分析も捗るのですが、それ以外のシステムで値を採ろうと思ったらカスタムイベントを書く必要があります。

以前は、非公式APIを使う必要がありましたがJDK9からはカスタムイベントの仕様も刷新され正式に利用できるようになりました。

ちなみにライブラリがJDK8でも利用される場合は「空実装の互換API」があるようなのでこちらを使えばコンパイルエラーにならないはずです。(#未検証)

とりあえずはWebアプリケーションの基本という事でResponse Timeを取ってみます。

まずはJFRイベントを作成します。

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

    @Label("Method")
    String method;

    @Label("URL")
    String url;
}

基本的にはEventクラスを継承して、格納したいプロパティをフィールドとして記述するだけです。@Label@CategoryでJMC上からの見え方を制御できます。

アノテーションにはその他にも閾値を設定する@Thresholdなどがあります。スロークエリなど特定の条件のみとれば良いときは設定すると負荷やデータサイズも下がって良いと思います。

代表的なアノテーションは以下です。

アノテーション 概要 デフォルト値
@Category 複数のイベントを束ねるカテゴリの作成。複数指定する事で階層化もできる N/A
@Name イベントの名前。 イベントクラスのパッケージ名を含むフルクラス名
@Label プロパティ名 N/A
@Description イベントの簡易な説明 N/A
@Threshold JFRに記録するための閾値 0 ns
@StackTrace イベントにスタックトレースを含むかのフラグ。デフォルトでは含まれる true
@Enable イベント記録を有効にするかのフラグ。デフォルトでは有効 true

では、続いてJFRイベントを記録します。

HTTPのレスポンスタイムなのでJAX-RSのメソッドをフックすることになります。今回の対象システムはQuarkusを使ってるのでCDIのインターセプタを使ってAOPを行います。

まずはトリガーとなるアノテーションを作成します。

@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface WebTrace {
}

続いて、アノテーションをエンドポイントになるJAX-RSのコードに付与します。

@Path("/account")
@WebTrace
public class AccountResource {
.
.
.

最後にCDIのインターセプタを作成します。JFRイベントの記録もここです。

@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@WebTrace
public class WebTraceIntersepter {
    @Inject
    HttpServletRequest req;

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {
        var event = new HttpRequestEvent(); // JFRイベントの初期化

        event.begin();  // JFRイベントの記録開始
        var result = ic.proceed();
        event.end();    // JFRイベントの記録完了

        // JFRイベントに任意のパラメータを追加
        event.url = req.getRequestURI();
        event.method = req.getMethod();
        // JFRイベントをコミット
        event.commit();

        return result;
    }
}

JFRイベントは単にnewするだけで良いです。DIとかFactorytとかが必要なのいで簡単ですね。今回の場合はic.proceed()が実際の処理が実行されるタイミングです。その前後でbegin/endする事で実行時間の記録を行います。単位はナノ秒です。

必要なパラメータを詰めたらcommitします。今回はURLとGETなのかPOSTなのかと言う情報も欲しかったので、HttpServletRequestをInjectionして取得しています。JFR関係ないですが1点注意なのはquarkus-undertowを依存に含めないとQuarkusではHttpServletRequestをInjectionできないので注意してください。

JMCのイベントブラウザで以下のように格納されたのが確認できるかと思います。

f:id:pascal256:20191208033755p:plain

格納したURLMethodはもちろんJFR側の機能としてDurationスタックトレース、あるいは開始と終了のタイムスタンプも取れているので調査が捗りそうですよね?

JFRのカスタムイベントに関しては徳益さんの「使ってみよう!JDK Flight Recorder」か「Java Flight Recorder in JDK 9 – Marcus Hirt」が詳しいです。

カスタムイベントをMetabaseでビジュアライズ

JMCの標準機能だけではグラフにして表示とかはできませんが、JMC Pluginを作れば可能なはずです。ただし、Eclipse力が必要になります。

と言うわけでBigQuery及びMetabaseを使ってビジュアライズしましょう。ちなみに、話の流れ的にMetabaseを使ってますが手元で探索的に作業するだけならgnuplot使うとかJupyter Notebookを使うのも良いと思います。

先ほどと同じ手順でJSONに変換してBigQueryに取り込みます。

$ jfr print --json --events minica.util.profile.HttpRequestEvent myapp.jfr > myapp.json
$ cat myapp.json | jq -c '.recording.events[]' >  myapp_f.json
$ grep '"type":"minica.util.profile.HttpRequestEvent"' myapp_f.json > HttpRequestEvent.json
$ gsutil cp HttpRequestEvent.json gs://jfr-storage/
$ bq load --autodetect \
                 --source_format=NEWLINE_DELIMITED_JSON \
                 jfr_logs.HttpRequestEvent \
                 gs://jfr-storage/HttpRequestEvent.json

Metabaseでグラフにしてみます。

f:id:pascal256:20191208060250p:plain

利用したSQLはこちら。

レスポンスサマリ:

SELECT 
  values.startTime as timpestamp,
  CAST(REGEXP_REPLACE(values.duration, r"[PTS]", "") AS NUMERIC) * 1000 duration
FROM `{project_id}.jfr_logs.HttpRequestEvent` 

URL分布:

SELECT 
  REGEXP_REPLACE(values.url, r"[0-9]+", "{key}") as url,
  count(1) as count
FROM `{project_id}.jfr_logs.HttpRequestEvent`
GROUP BY url

TPS:

SELECT PERCENTILE_CONT(count, 0.5) OVER() as tps FROM(
  SELECT sec, count(1) as count  FROM(
    SELECT 
      UNIX_SECONDS(values.startTime) as sec,
      CAST(REGEXP_REPLACE(values.duration, r"[PTS]", "") AS NUMERIC) * 1000 duration
    FROM `{project_id}.jfr_logs.HttpRequestEvent` 
  ) GROUP BY sec
)
LIMIT 1

JMCでカスタムイベントをビジュアライズしようとするとプラグインを書く必要がありGUI力が足らない私としては結構難しかったのですが、Metabase + BigQueryであれば手慣れたSQLでサクッとグラフに出来るのが良いですね。

リアルタイム連携をする

せっかくなのでリアルタイム連携も作っていきましょう。

と言っても、JFR Stream APIはまだ未実装なので下記のような流れでやります。

  1. JFRのオプションを変更してリポジトリに小刻みにファイルを吐く
  2. JFRをGCPにアップロードする
  3. GCSにアップロードされたことCloud Functionsで検知してCloud Runをキックする
  4. Cloud RunでCloud SDKコマンドを使ってGCSからBigQueryに連携する

Cloud RunはCloud SDKを使うBashとすることで概ね今までの通りの方法で動くのがポイントです。

JFRのチャンクサイズの変更

まずは、JFRのチャンクサイズを変更します。デフォルトでは12MBとかなり大きいのでアップロード負荷が高いです。なので、1MBまで下げておきます。

java -XX:StartFlightRecording=settings=profile,disk=true,filename=/var/log/myapp/myapp.jfr -XX:FlightRecorderOptions=repository=/var/log/myapp/jfr_logs/,maxchunksize=1M -jar myapp.jar

maxchunksizeを変更する事で比較的小さなファイルがリポジトリにたくさん出来るようになります。

JFRをGCPにアップロードする

続いてJFRのアップロードです。 前回実行時とのファイルリストの差分をチェックして新規があればgs://jfr-storage/にJFRをアップロードします。

REPO=/var/log/myapp/jfr_logs
WORKDIR=/tmp

CURRENT_REPO="$REPO/"$(ls -1 $REPO|sort|head -1)
ls -l $CURRENT_REPO|awk '/jfr/{print $9, $5}'|sort > $WORKDIR/jfr.list
diff ${WORKDIR}/jfr.list ${WORKDIR}/jfr.list.prev |grep jfr|awk '$NF > 0{print $2}' > ${WORKDIR}/jfr.diff
mv $WORKDIR/jfr.list $WORKDIR/jfr.list.prev

cat ${WORKDIR}/jfr.diff|xargs -n1 -P1 -I{} gsutil cp "${CURRENT_REPO}/"{} gs://jfr-storage/

watchコマンドやcronで定期実行すると良いでしょう。

GCSにアップロードされたことCloud Functionsで検知してCloud Runをキックする

Cloud RunはまだHTTP以外のイベントソースを受け取れないのでイベントのフックにはCloud Functionsを使います。スクリプトは下記の通り。

package.json:

{
  "name": "check_gcs4jfr",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/storage": "^1.6.0",
    "request": "^2.88.0"
  }
}

index.js:

var request = require("request");

exports.checkGCS4JFR = (event, context) => {
  const gcsEvent = event;
  const url = "https://{Cloud RunのURL}?args=" + gcsEvent.name;

  console.log(`Recive Event: ${gcsEvent.name}`);
  request.get(url, function(err, res, body) {
    if (err) {
      console.log("Error: " + err.message);
      return;
    }
    console.log(body);
  });
};

パススルーしてるだけなのでシンプルですね。以下のコマンドでデプロイします。

$  gcloud functions deploy checkGCS4JFR --runtime nodejs8 --trigger-resource jfr-storage --trigger-event google.storage.object.finalize

Cloud RunでCloud SDKコマンドを使ってGCSからBigQueryに連携する

最後にGCSからBigQueryへのアップロードです。と言っても基本的には先ほど記載したコマンドをそのまま実行するだけです。

HTTPからのリクエストでスクリプトが動かせるようにhttp-wrapperを利用します。 Dockerfileは以下の通り。中で使ってるスクリプトこちらです。

FROM google/cloud-sdk

RUN apt-get update && apt-get install -y openjdk-13-jdk-headless jq && apt-get clean && rm -rf /var/lib/apt/lists/*
ENV PATH /usr/lib/jvm/java-13-openjdk-amd64/bin/:$PATH

RUN mkdir -p /app
WORKDIR /app

RUN curl https://storage.googleapis.com/shared-artifact/hwrap -o hwrap && chmod a+x ./hwrap
ADD parse.sh ./
ADD transfer_gcs.sh ./
ADD load_bq.sh ./
ADD run.sh ./

CMD ["./hwrap", "-p8080", "./run.sh"]

ではCloud Runにデプロイします。

$ GCP_PRJ_ID=$(gcloud config get-value project)
$ gcloud builds submit --tag gcr.io/${GCP_PRJ_ID}/jfr4bq
$ gcloud beta run deploy --image gcr.io/${GCP_PRJ_ID}/jfr4bq

これで準備はOKです。試してみましょう。

$ watch --interval 60 ./transfer2gcs.sh /var/log/myapp/jfr_logs

BigQueryにJFRの情報が準リアルタイムで連携されるのがわかります。

f:id:pascal256:20191209165741p:plain

まとめ

半分くらいJavaではない話を書いた気もしますがいかがだったでしょうか?

リアルタイム分析のところはさておきカスタムイベントとBigQuery/Metabaseでの分析はかなり使いやすいかと思います。 JDK9での仕様の標準化とJDK11でのOpenJDKへの取り込みでこう言った事がやりやすくなったのは非常に良いことかな、と思っています。

MicroprofileのMetricsやOpenTracingなど情報をとる仕組みも標準化してきたので、上手くこの辺りをJFRにも書き込んでWeblogic並みの障害分析環境を標準化出来ないかも今後チャレンジしてみたいです。

それだは、来年もみなさんとJavaにとって良い年でありますように。Happy Hacking!

参考

そもそもJWTに関する私の理解は完全に間違っていた!

TL;DR

  • ステートレスなJWTはそもそもセッションの代替では無い
  • アクセストークンとしての利用が基本で数分レベルの短寿命な有効期限で利用
  • 従来のセッションに近い概念はリフレッシュトークン。てか、リフレッシュトークンはセッションでも(たぶん)良い
  • ユースケース的にモノリスには不要。SPAでMSAな時にメリットが出て来る。

はじめに

Webで認証システムといえばセッション! と言う感じのレガシーおじさんなのですが、最近はJWTとかも出てきてとても気になっていました。新しいものは使ってみたくなりますよね?

なので以前軽く調べてみたたのですが「JWTは危ない」とか「JWTをセッションに使うな」的な記事が大量に出て来ます。

co3k.org

qiita.com

一方で反論記事もあり色んな議論が渦巻いています。そもそも利用を推奨する記事も多い。

auth0.hatenablog.com

この辺りで「JWT怖い!」となり、状態を結局サーバで管理しないとセキュアに使えないならメリットは一体? とも思って一旦放置してたのですが「リフレッシュトークンとアクセストークンに分けて考えるのが前提」と言うのを下記の記事で理解してスッキリしました。

auth0.com

といわけで、私ので理解があってるかの確認がてらまとめてみました。

たぶん私と同じくユースケースが理解出来てなくてイメージが出来なかったり過った利用方法をする人も居ると思うので、そもそも何で必要なの? ってところを重点的に書いてみました。

実際にJWTをまだ使っては無いので理解が過ってるところがあれば指摘いただければと思います。

そもそもセッションにJWTを使って良いの?

「ダメです」

特に、JWTのメリットとして語られるサーバサイドで状態管理しなくて良いから云々でセッションの代替として使うのはアンチパターンと思われます。

そもそもセッションとは状態管理です。その時点でステートレスなインフラを作るのは難しくクライアントサイドでの管理はリスクがあります。幻想は捨てましょう。

ただし、JWTをアクセストークンとして使う事で今まではセッションで実現していた事の一部が代替できます。それは「認証」です。

そもそもセッションがなぜ必要?

さて、そもそもセッションはなぜ必要でしょうか? Webで言う「セッション」とはステートレスなプロトコルであるHTTPにサーバサイドで状態を持たせる仕組みの総称です。

元々は論文などのドキュメント公開の仕組みとして作られたHTMLとHTTPは状態が必要に無いのでその仕組みは存在しません。しかし、ひょんな事からWebアプリケーションとして様々な事に利用されるようになっため状態を持たないと都合が悪いケースが増えて来ました。

その流れでクライアントサイドに情報を保存するCookieなども登場し、クライアントとサーバで共通のキーを持ちそのキーにサーバサイドで情報を紐付ける「セッション」が実現できるようになりました。

f:id:pascal256:20191103131008p:plain

「セッション」を使う事でユーザがログインしているかを判別してユーザ毎の情報を表示するなどの状態をベースにしたWebアプリケーションが作れるようになりました。

セッションのストレージには色んなものが試されていて「NFS + ファイル」「RDB」「インメモリ」「KVS」とかありました。現在の主流はKVSでしょう。JavaEE系はインメモリも現役ですね。

歴史があるだけあってこのCookieとサーバサイドのストレージを使ったセッションはセキュリティ的にも運用的にもかなりの積み重ねがあります。そのため安心して使えるのですが問題ももちろんあります。

ステートレスアプリケーションとスケールアウト

サーバが3台とか5台くらいなら割となんでも良いのですが、サーバをスケールアウトして30台とか100台でWebアプリケーションを実現しようとすると、セッション用ストレージのインフラを作るのが結構骨です。

その流れで普及しだしたのがステートレスアプリケーション。RESTとかROAとセットで語られることが多かったと思います。

ざっくりと言えば「セッションを使ったアプリケーションはスケールさせるのが大変だから情報をクライアントに持たせて全部リクエスト毎に付与しよう」と言う考え方です。

これならサーバに状態を持たなくて良いからストレージがSPOFやボトルネックになる事も無いわけです。なので基本的な考え方になって行くわけですが、この考え方がだと送られてくるデータを全部信用するしか無いわけです。

そもそも渡されるu_idを自由にクライアントが指定できるのでなりすまし天国です。なので、バックエンドAPIとしての利用が限界でそれも「信頼されたアプリケーションから送られて来た情報は信頼する」と言うのが前提となっているので少し弱い。

マイクロサービスとSPA

色々あってマイクロサービスが流行ります。これをサーバサイドでアグリゲーションして一つのWebページとして返してる分にはあまり問題が無いのですが、SPAとかでクライアントサイドでアグリゲーションする場合は問題です。

流石にクライアントサイドからの情報を無条件に信用するステートレス実装はできません。なので、セッションなどを使ったステートフルな実装が基本になるかと思います。その場合はセッションのチェックが1ページ見るのに5個も10個も走る事になります。

今時はインメモリ実装かKVSなのでそこまで一つ一つは大きなコストでは無いですが無視できるものでも無いはずです。特にマイクロサービスだとインメモリは不可能なので必然KVSになります。負荷の集中もそうですが、マイクロサービスなのに共通のストレージにアクセスするのは正直筋の悪い設計となってしまいます。

f:id:pascal256:20191103133958p:plain

この辺はエアプなので想像ですが、たぶんこう言う問題は起こるはずです。

ソーシャル・ログインとAuthorizationプロトコル

少し違う文脈としてソーシャル・ログインの台等とそれを支える技術であるOpenIDから始まりOAuthを経てOIDCに辿り着いたAuthorizationプロトコル群があります。

以下のブログでも書きましたが、もはや自前で認証機能を作るのはリスクの観点で可能な限り避けるべき事なので、GoogleTwitterといった信頼できる認証システムをOAuthやOIDCで使うのが一般的です。 koduki.hatenablog.com

これらの認可プロトコルでは必然的に認可の基盤と実際に利用されるアプリケーションが異なります。その為ものすごく素朴に実装すると毎回認可サーバに問い合わせます。

これはインターネット越しだとそれなりのレスポンスになるはずなのでなんとかしたい。。。と言うわけでこの手のプロトコルの仕様を決める際に寿命の長いリフレッシュトークンと短いアクセストークンに分ける考え方やステートレスに認証できるJWTも作られた見たいです。

リフレッシュトークンとアクセストークンを利用した認証

さて、前置きがとても長くなったけどようやく本題のリフレッシュトークンとアクセストークンのお話。

マイクロサービスとSPAのところでした説明した図を思い出して欲しいのですが各サービスではクライアントの情報は信用できないのでサーバサイドで管理する必要があリマス。逆転の発想で「クライアントからのリクエストが信用できればサーバでの状態管理は不要」となります。

「ユーザID」や「どの機能アクセスできるか等の権限」といった情報を信頼できるサーバで秘密鍵により電子署名を付与する。APIサイドでは公開鍵を使って改ざん検知をするだけで渡された情報を信用できるのでストレージにセッション情報を保存する必要はない。これがアクセストークン。

では、信頼できるサーバとは何か? そもそもクライアントで秘密鍵を使って署名を付けてもオレオレ証明書なので信頼性はない。なので、第三者の認可サーバを使う。ここでユーザIDとパスワードだとかMFAだとか良い感じにユーザを認証してその証明となるキーを発行する。これがリフレッシュトークン。

リフレッシュトークンを持ってるクライアントが認可サーバに必要な情報を渡して署名付きのアクセストークンを作るという流れです。

f:id:pascal256:20191103154050p:plain

JWTは署名付きで任意のペイロードを持てるのでステートレスなアクセストークンの実装に最適という話。そしてリフレッシュトークンはステートフルが要求されるのでJWTを使う意味はあまりないはずです。

自分で実装するならリフレッシュトークンはトラディショナルなCookie + サーバサイドなセッションがWebアプリは一番楽かもと思っています。

JWTとセキュリティ

さてステートレスなJWTは実際セキュアだろうか? これは実装次第だけど基本的には問題ないと思います。

当たり前ですがJWTであるアクセストークンは改ざんチェックしかしてないので本人認証としては弱い。なんらかの問題で流出したらアウト。秘密鍵を変えるか有効期限が過ぎるのを待つしか無いです。

たとえリフレッシュトークンを使って再発行しても古いアクセストークンをステートレスに無効にすることは出来ない。やるならブラックリストデータを状態として持つしかない。

なので理想的にはアクセストークンはワンタイムトークンとして振る舞うのがセキュリティ的には良いと思います。仕様上それは出来ないから1分以下とか超短期の有効期限にするのがベスト。

これならアクセストークンが流出してもあまり問題はないはず。リフレッシュトークンはサーバサイドで状態を持ってるので万一流出したら破棄することが出来るので通常のセッションと同程度の長さで問題はないし明示的なログアウトも作れます。

ただし、アクセストークンの有効期限が短いと認可サーバへの問い合わせ回数が増えるのでここはトレードオフですね。認可サーバの負荷的には従来のセッションによると大差無い気もするけどクライアント側の通信コストが問題。ユーザが多いと署名コストもボトルネックになるかもです。

この辺のさじ加減は作ってみないと分からないのでバランスはサービス次第になるかと思います。

JWTはセッションの代わりになるのか?

なりません。

セッションが担っていた一部である認証に関しては限定条件で効果的に果たせそうです。ただ、そもそも状態の保存ができません。「ペイロードに格納すれば良いじゃん?」って話ですがそれは都度都度アクセストークン発行すれば出来ますが、たとえばAmazonの買い物カゴみたいなのをJWTで作るのはかなり面倒では。

その場合は大人しくCookieやLocal Storageに格納してAPIのBodyに入れるのが良いかと思います。暗号化もサポートしてるけど多少センシティブなものも突っ込めそうですが、ストレージとしての用途を考えて作ってるとは想定しづらいのでセッションの単純置き換えは無理でしょう。この点に関してはそう思ってる人も多分居ないと思いますが。

まとめ

さて、思ったより長かったけどJWTを使った認証の流れとユースケースを私なりにまとめてみました。

何というか従来的なモノリスなアプリケーションだと何ら意味が無いという事が分かった。

セキュアに作るならステートフルなリフレッシュトークンとステートレスなアクセストークンにする必要があり、単独アプリならステートフルなトークンすなわちセッションだけで十分だし。ソーシャルログインとか別の仕組みの一部として使う分には別だけど。

ここを理解しないとJWTをセッションに使おうという発想になってしまうのだろうなぁ。とは言え私の今の理解が正しいとも限らないけど。認証周りは本当にややこしい。。。

それではHappy Hacking!

参考

Graphvizで画像を読み込むとエラーになる

Graphvizで画像を下記の構文で読み込もうとするもビルド時にエラーが発生。

d [shape=none, label="", image="sample.png"];
$ dot -Tpng -o sample.png sample.dot             Wed Oct 30 23:03:17 2019
2019-10-30 23:03:18.988 dot[97555:3328824] +[__NSCFConstantString length]: unrecognized selector sent to class 0x7fff9f6133a0
2019-10-30 23:03:18.988 dot[97555:3328824] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[__NSCFConstantString length]: unrecognized selector sent to class 0x7fff9f6133a0'

どうもbrewでインストールしたGraphvizにlinbpngとかが適切にリンクされてないのが原因っぽい。と言うわけで以下のコマンドで再インストール。

$ brew uninstall graphviz
$ brew install pango librsvg
$ brew install graphviz --build-from-source

これで無事画像が表示されるようになる。

個人的に気になってるNewSQLな一覧

OceanBaseとかNEDOに採択されたProject Tsurugiとか気になるニューフェイスが登場したので、トラッキング用にメモしておく。 NewSQLの定義が謎だけど、たぶん脱レガシーを標榜したSQLは皆NewSQLに分類されてるんじゃ無いかなぁ?

OceanBase

  • Alibabaによって開発/実用されているRDB。暫定世界最強。
  • SSDなどの最新のHWを前提にした構造。書き込みに強い
  • MySQL互換
  • 詳しくは下記 koduki.hatenablog.com

Project Tsurugi

PG-Strom

  • GPUを活用してスケールアップを目指したPostgreSQLベースのDWH。OLAP向け。
  • JOINやSCANをGPUにオフロードすることで大幅な性能を向上
  • SSDからRDMAで直接GPUに転送してるので圧倒的なI/Oを誇る。
  • 以下参照

VoltDB

Peloton

CockroachDB

 まとめ

自分で書いたことはもちろん利用者としてもDBに詳しいとは言えないけど、DB系の説明は読んでてワクワクしちゃうのでつい追っちゃうんですよね。

もちろん、HANAとかAWS AuroraとかCloud SpannerとかExadataやNonStop SQLとか商用にも興味深いのはいっぱいあるのでだけど、とりあえずマイナーそうなのを忘れないように上げてみた。気になったのが増えたら追加するかも。

しかし、SpannerのクローンはあるのにAuroraのクローンは無いのかな?S3にログ書くデザインとかCloud前提であれも面白そうなんだが。

それではHappy Hacking!

世界1位になったアリババの独自開発DB OceanBaseとは何者か?

さて、1週間くらい前のニュースですがAlibabaがOracleのデータベースを抜いて世界1位の座を獲得しました。 itnews.org

世界一位って何よ? って所なのですが TPC-C - All Results - Sorted by Performance の結果みたいです。 f:id:pascal256:20191021055634p:plain

まあ「Oracleのデータが10年前の11gなので今のExadata X8Mとは比べ物にならない程性能が低いであろうこと」「HPEとかも虎の子のNonStop SQL出してない」「レギュレーションの問題なのかやる気の問題なのかAWS AuroraもGCP Spannerも居ない」という事もあって、最速のRDBランキングとしての妥当性に疑問はありますが、これはランキングが悪いのであってAlibabaを貶めるものでは無いでしょう。誇大広告感は狙ったと思いますがw

ベンチマーク結果としては以下のように、2位のOracleと比較して性能がザックリと2倍。ただしコスパは10%ダウンです。とはいえ当時円高だった事もあって同等といっても差し支えは無いでしょう。

DB System Performance (tpmC) Price/tpmC
OceanBaseAlibaba Cloud Elastic Compute Service Cluster60,880,80095.6 円
OracleSPARC SuperCluster with T3-4 Servers30,249,68884.2 円(当時)

どんなデータベース?

Database of Databases - OceanBaseによると2013年から開発されていて以外と実績がある事がわかります。GithubソースコードGPLで公開されていますがほとんど更新されていません。現状の最新版の2.0とはかなり変わってるかもです。

我々が普通に使おうとするとAlibaba Cloudを利用するしか無いっぽいですね。

公式ページや中国語の解説サイトをGoogle翻訳しながら調べた感じだと以下のような特徴があります。

  • 伝統的なRDBのACIDやSQLとNoSQLの可用性やスケーラビリティを備えたデーターベース
  • SSDなどの最新のハードウェアを前提として設計
  • Shared Nothingな分散DB
  • マルチリージョンなどクラウド環境を想定したアーキテクチャ
  • MySQL互換
  • LSM treeのような階層的なストレージ構造により高速なDMLを実現

この点としては割と現代的な分散DBあるあるな特徴ですね。Auroraとか。

ストレージエンジンなんかは結構面白そうな特徴を持っていました。 f:id:pascal256:20191021064254p:plain ref: OceanBaseの概要 - ストレージエンジン

データをまずIncremental Data(増分データ)とHistorical data(ベースライン)に分けます。その上で、増分データはインメモリ(MemTable)、ベースラインはSSD(SSTable)に分けて保存します。

RDBで良く使われるB-Tree Indexは原理的にReadには強いけどWriteには弱いという特徴を持っています。これはリアルタイムにインデックス全体を再構築するためです。増分データとベースラインを分けることでWriteが発生した時の作業範囲を小さく出来ます。で、実際にデータを見るときにはSSTableにMemTableの差分を適用して見ることで対応してるかと思います。

また、SSTableは常にRead Onlyなのでロックは不要。増分が書き込まれるMemTableもRead時はロックフリーとの事なのでイベントログ自体を書き込んでると思われます。

LSM treeそのものを使ってるかはちょっと分からなかったですが、マージコストをLevelDBやRocksDBと比較してるので似たような考え方などだと予想。

結果として、DMLは全てメモリ操作になるのでインメモリDB並みに高速なようです。

Ali Payとかにも使ってるなら書き込み性能が参照性能以上に重要になって来るので、その辺りを重視した設計になってるのでしょうね。

まとめ

分散トランザクションとか色々書いてあって面白そうなのですが、良く分からないのでデーターベースに詳しい人が誰が解説してくれるのを期待!

あと、日本語はもちろん英語のドキュメントすらほぼ無いのが最大の欠点ですね。その気があれば上手く英語化したりグローバルにもオープンにしていく事で、ひとかどのポジションを気づくかもです。

しかしRDB自作できる会社は強いですよねぇ。日本だと今や日立くらい? KVSは結構各種Web企業も一時作ってましたが。

TPC-Cに関しては恐らくAmazonAWSに全面移行された記事とか合わせてイラっとしてるOracleさんが、大人気なくベンチマークを更新し他社もそれに乗ってきてそれなりに意味のあるランキングになるとちょっと面白いなぁ、と個人的には思います。

参考

JDK Flight Recorderのアーキテクチャ概要

はじめに

JDK Flight Recorder(JFR)はJavaで利用できる常時本番適用可能な超低オーバーヘッドのプロファイラです。性能分析とか障害対応の強い味方ですね!

今まで商用ライセンスのみ使用可能だったのですが、JDK11よりOpenJDKに取り込まれ自由に使えるようになりました。オープンになったので色々自分でも使いたいですし、色んな人にも使って欲しいなと思っています。

ただ、Oracleの公式ドキュメントが余りにもサラリとし過ぎていて良くわかんないので、いくつかのOracleや有志の発表資料や自分の知識を元にアーキテクチャ概要を解説して見ました。

この辺をしっかり分かりやすく書いた公式ドキュメントが無い(=あればそれが欲しい!)のと、ソースコードまで読んだ訳では無いので普通に間違ってる箇所がある可能性があります。詳しい方いましたらぜひ指摘して頂けると嬉しいです。

JFRにおける「イベント」

JFRはプロファイリング/診断のためにイベントを集めるメカニズムです。

イベントとはJVMまたはJava アプリケーションから取得するデータで、「名前」「タイムスタンプ」「カスタムペイロード」を持ちます。こちらのペイロードが実際の性能情報等が書き込まれる部分で、CPUの情報とかヒープだとかスレッドだとか具体的な値が格納されています。

JFR Eventsの概要

主として下記のような情報が取得できます。

取得対象 主な取得内容
オペレーティングシステム メモリ、CPU Load、CPU情報、ネイティブライブラリ、プロセス情報
JVM メモリの割当て、クラスのロード, JIT, GC, メソッドプロファイルイベント
Java API ソケット I/O, ファイル I/O, 例外、スレッド、モジュール情報
カスタムイベント アプリケーションコンテナなどのミドルウェア、その他ユーザ定義イベント

アプリケーション、JVM、OSと広範囲に情報が記録される事がわかるかと思います。また、カスタムに関しては下記のように作成/利用が可能です。

public class HelloJFR {
    @Label("Hello World!")
    static class HelloWorldEvent extends Event {
        @Label("Message")
        String message;
    }
    public static void main(String[] args) {
        HelloWorldEvent event = new HelloWorldEvent();
        event.message = "Hello World!";
        event.commit();
    }
}

Weblogicのようにすでにカスタムイベントに対応したミドルウェアを使えばサーブレットJDBCの情報も取得できますし、自分で拡張することも可能です。 こちらに関しては4章と6章で詳しく説明します。

イベントタイプとサンプリング

JFRでは以下の3つのタイプのイベント分類して情報を収集します。

  1. 期間イベント(duration event)

    • 開始時間と終了時間を持つイベントです。GCイベントやカスタムイベントで性能を取りたい時に使います。
    • 閾値を儲ける事が出来るので、例えば10ms以下のSQL実行イベントは記録しない、などの設定も可能です
  2. インスタントイベント(instant event)

    • 期間を持たない発生すると直ちに記録されるイベントです。Exceptionなどがこれに当たります。
  3. サンプリングイベント(sample event/requestable event)

    • 定期的に記録されるイベントです。CPU使用率などがこれに当たります。こちらの頻度は変更可能です。

JFRは大量のメトリクスを格納しつつも低遅延を保つために閾値を指定したりサンプリング頻度を過度に上げない事が重要です。

多くの場合、極端に実行時間が短いイベントは問題の焦点ではない事が多いので閾値を付けるのは妥当なトレードオフだと思います。もちろん、これはアプリケーションの性質で大きく異なり、例えばバッチとリアルタイムシステムでは「遅い」という時間の基準が違うので、必要に応じてカスタマイズをします。

データフォーマット

イベントはJSONやMessagePackのような自己記述的なデータ構造です。IDLやヘッダーなどが不要になるのでイベント単位で色々加工したいログには向いたデータ構造と言えます。データもバイナリフォーマットなため非常に効率的です。

例えば、クラスロードイベントの場合、タイムスタンプ、タイムスパン、スレッドID、スタックトレースペイロードを含み、下記のように格納されます。

<memory address>: 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00

  • Event size [98 80 80 00]
  • Event ID [87 02]
  • Timestamp [95 ae e4 b2 92 03]
  • Duration [a2 f7 ae 9a 94 02]
  • Thread ID [02]
  • Stack trace ID [01]
  • Payload [fields]
    • Loaded Class: [0x8d11]
    • Defining ClassLoader: [0]
    • Initiating ClassLoader: [0]

ref: https://openjdk.java.net/jeps/328

循環バッファとデータフロー

JFRではイベントは循環バッファに記録されます。これは、メモリとディスクを組み合わせた概念で階層を持たせる事で低遅延かつ大量のデータ格納を実現しています。

循環バッファの各領域 説明
Thread-local Buffers 各スレッドが持つローカルバッファ領域。5KB/Thread
Global in-memory Buffer Javaアプリケーションで持つインメモリ領域。452KB (default)
ディスク領域(リポジトリ) オプション指定をして置けば溢れたデータをChunk単位に分割してダンプする。 Max Chunk Sizeは12MB (default)

まず、JFRがJVMおよびJavaアプリケーションから集めたデータを 「Thread-local Buffers」 に格納します。ここは比較的小さめのサイズとなっています。

次に、 「Thread-local Buffer」 のデータが溢れたら、「Global in-memory Buffer」 にデータがコピーされ古いデータは破棄されます。

最後に、 「Global in-memory Buffer」 から溢れたデータは リポジトリ と呼ばれるディスク領域に 「Chunk」 の単位で格納されます。こちらはJVMオプションでdisk=true,repository={一時ファイル保存先}を指定した場合のみに格納されます。また、ファイルに格納されるイベントは複数スレッドから集められているので必ずしも時系列順を保証はしません。

JFRのデータフローを図示すると以下のようになります。また、MaxSizeもしくはMaxAgeで循環バッファ全体の閾値を決めます。この閾値を越すとリポジトリのデータも古いものから順に消去されることになります。 JFR Eventsのデータフロー 図. JFR イベントのデータフロー

また、異常/正常問わずJavaのプロセスが終了すればJFRファイルとして結果を格納する事が可能です。

リポジトリとJFRファイル名

JFRではfilenameというJVMオプションでJFRファイルの格納先のファイル名を指定できます。

ただし、注意が必要なのはこれは循環バッファの内容が 順次書き込まれる場所ではない という事です。

ここは少しややこしくて、この関係性を理解していないとJFRファイルの取り扱いで混乱してしまいます。

前述の通り、循環バッファの内容はrepositoryオプションで指定されたディレクトリにファイルダンプとして出力されます。もし「実行中のJavaアプリケーションのJFRファイルを取得したい」と思うなら、リポジトリのファイルを取得してください。

filenameに出力されるのは最終アウトプットになるJFRファイルです。このファイルはプロセスの完了時かJMXまたはJMCやJCMDでJFRのダンプ命令を出した時に出力されます。そして 「プロセス完了時点またはダンプ命令時点の循環バッファ」 が格納されます。そのためプロセスが動いている間は基本的にファイルは書き込まれません。

なので「Webアプリケーションのような長期に動くJavaアプリケーションのJFRファイルをバックアップしたい」等といった用途にはリポジトリからファイルを取得してやる必要があります。おそらくこちらのリポジトリに格納される一時ファイルの方が 「ログ」としてみた時の運用には近いと思います。

また、maxsizemaxageを指定した場合は循環バッファ全体の閾値となるので結果としてfilenameに含まれる内容になります。裏を返せばリポジトリに格納される一つ一つのJFRファイルのサイズはmaxsizeで指定できません。こちらに関してはmaxchunksizeを変更してやる必要があります。

両方指定することも可能ですが、個人的にはリポジトリだけ指定してれば実運用上は十分な気がします。この辺りに関しては5章で解説します。

JFRの開始とJFRファイルダンプ

JFRはMBean(FlightRecorderMXBean)またはJDK JFR Managemnet APIを使うことで操作(開始/終了)が可能です。また、JDK Mission ControlやJCMDおよびJVMオプションでも可能です。

JFR Eventsのデータフロー

デモなどではJMCやJCMDを使ったものを良く見ますが、実運用上はJVMオプションで指定して常に記録を行いリポジトリに格納される一時ファイルかプロセス終了時に作成されるJFRファイルを確認する事がほとんどだと思います。

まとめ

さて、ちょっと長くなりましたがこんな感じでしょうか。特に、循環バッファやリポジトリとJFRファイル名の事は把握しておいた方が実運用でも便利です。

コメントまたは指摘等を頂けるととても嬉しいです。

それではHappy Hacking!

参考

Oracle Code One 2019に参加してきました

ブログに書くの忘れてましたが、去年に引き続き今年もOracle Code One/Oracle Open Worldに参加してきました。

f:id:pascal256:20191001135557p:plain

すでにキーノートとかの詳細は素晴らしいレポート出てるのでそちらをみてください。

あと、去年とは違いありがたいことに今年は採択されたので発表することができました!

Performance Monitoring with Java Flight Recorder on OpenJDK [DEV2406]

話したいところはある程度気持ちを乗せてちゃんと話せたかなと思う反面、質疑応答はもう少しスムーズに出来たらとも思ったのでやはり英語の練習をもっとしないとですね。

で、ここからはかなり主観というか個人的な想像を入れた気になるトピックのまとめです。

JDKの新機能と進捗

ここに関しては良い意味で新しい情報は無かったです。OpenJDKがOSSとして運用されJSRやJEPでオープンに仕様が管理されているので、Oracle隠し球で持ってた新機能の発表とかは無かったです。

カンファレンスに行って目新しい情報が無いというのもちょっと寂しいですが、AppleオープンソースのはずのSwiftでそれをやらかして炎上しましたし、OSSとしては健全なのでこのほうが良いですね。

もちろん、各機能のJEPの現状や詳細機能の説明はとても参考になりますし、クォータリリースなので1年間を振り返ったサマリーは役に立ちます。

GraalVM

反面、ロードマップを含めて色々発表があり注目の的だったのがGraalVMです。

GraalVM自体は去年も発表ありましたし、結構前から存在していますが1.0になったということもあって今年はかなりアピールしていました。

native-image(AOT)を中心にコミュニティからの発表も多かったですが「OpenJDKと互換性を保ったまま高速なJVM(JIT)」としてOracleがかなり強く推してたので、おそらくGraalVM EEを結構推していきそうな感じです。OCIなら無料というのも売りにできるでしょうし。

ちなみに価格表を見るとOracle Java SE($25/processor/month)よりGraalVM EE($18/processor/month)のが安いので、OracleJDK使うならGraalVM使えよ、というオーラが出まくっていますw

あと、ロードマップ的には11月にはちゃんとOpenJDK11に対応するので、少なくともLTS版には随時追従していく形になるのだと思われます。 思ったよりOpenJDK11対応が早くて安心。

AOTやマルチ言語対応もかなり進んでてLLVM組み込みはJNI代用として面白そう。あと、Rubyもほぼレディな状態になっていて後はバージョンを追いつかせるだけ、とのことなのでここも個人的にはかなり気になります。爆速だし、JFR使えるかもだし。

なおAOTとJITは現時点では以下のような性能のトレードオフがあるけど、将来的には全部AOTで上回りたいという野望があるそうですw f:id:pascal256:20191001140618p:plain

Jakarata EEとEclipse MicroProfile

大変悲しいことですが今回のタイミングでJakarata EEにMicroProfileは統合されませんでした。

名前空間の差分とかの話もあるのですが、そもそもマイクロサービスの早い進化に対応させたいMicroProfileと標準化を主眼としてるJakarata EEではライフサイクルが異なるという問題も大きいようです。

個人的にはLTS版的な感じて定期的にJakarataEEにMPの仕様が取り込まれれば良いと思うのですが、今のままだとSpring的なポジションになる可能性もあってちょっと心配。 たぶんJakarataEEで行われるであろう名前空間の変更と合わせてMPの取り込みも来年か再来年くらいにされるんでは? 的な雰囲気なので当面はMPが実質的な仕様を牽引してく形になりそうなので、こちらを追う感じですね。

MPに関してはOracleWeblogicではなくHelidonで対応していくとJakarataEEのセッション中に言ってましたし、IBM RedhatのQuarkusと合わせてエンタープライズのサポートも問題なさそうです。

MPとしてはやはり話題の中心はnative-image + QuarkusでWebSphere LibertyやPayara Microはあまりトピックにはなってなかった印象。もちろん、ユーザ事例として出てくるときはそれら既存の奴を使ってたでしょうが、あまりその手のセッションは参加しなかったのでちょっとバイアスあり。

ただ、native-imageとの連携もあるので既存のアプリケーションコンテナとしての実装ではなく、Helidon/QuarkusのようなMP向けライブラリとしての開発が主流になってく気は改めてしました。

ExadataとPersistence Memory

以下の記事でも書いたのですが、Exadataについに不揮発性メモリが乗ってきました。

koduki.hatenablog.com

Intelの3D XPointが発表されてここまでくるのに長かったなぁ。まあ、今回はインメモリDBに使えるというよりは超高速なストレージとしての利用になりますが、こうした事例が増えてDBがアーキテクチャ的に進化したり、HPEのThe Machineのような近未来的なアーキテクチャが実際に出てくるようになると興味深いですよね。

これで量産化されて自宅マシンでも使えるようにならないかなー。まあ、Itaniumが自宅で使える価格/流通ではなかったので、期待しすぎると悲しくなるかもですが><

f:id:pascal256:20191001151956p:plain

ref: Introducing Exadata X8M: In-Memory Performance with All the Benefits of Shared Storage for both OLTP and Analytics | Oracle Exadata Database Machine Blog

お得意のInfiniBandを捨てて100GbE + RoCE (RDMA over Converged Ethernet)を採用しています。40Gbpsという帯域から100Gbpsに上げつつ、RDMAをRoCEで処理することでレイテンシも確保しています。

「最速のコモディティサーバ」として設計されるExadataとしてパフォーマンスを上げつつフィットさせてきた感じですね。

今回のバージョンアップで帯域的/レイテンシー的なボトルネックにさらに改善ができたと思うので、やはり「僕の考えた最強のDBサーバ」としてのExadataは面白いですね。スペック厨的にワクワクするw

Oracle Cloudの戦略

さて、最後はOCIの話です。ここ最近Oraclegが力を入れてる気がするけどちょっとパッとしない、後発だからものは良さそうだけどなぁってポジションだったOCIですが今回は結構戦略を明確にしてきたと思います。

毎度のごとくAWSをディスりつつOracle DBをageてましたが、やはりOracleはデータベースの会社です。AzureにはWindowsやOffice365/ADとの親和性、GCPにはBigQueryとAIというキラーコンテンツがあるようにOCIのキラーコンテンツはDBだということを明確に出してきています。

なので、k8sやその他マネージドサービスなど他者との差分は追いつかせるけど、推しはやはりDB。単にRDBというだけじゃなくてAWSに比べて単一のアーキテクチャでDWHもトランザクションもグラフDBもドキュメントDBもやるよ、という点を強く出してました。

むしろ、Microsoftとの提携でイントラ系も基幹/業務系もエンプラ系は取り込みたいって感じですね。Web系に関してはスモールスタートができる料金体系になるかが鍵かなぁ。

特にShared環境Autonomous DatabaseはAWS Aurora Serverlessと同様に利用した分だけ課金するモデルのようなのでコストしだいですが最良のサーバレスRDBになり得るんじゃ無いかと。コストシミュレータで計算しようとするとバグってて常にゼロ円になる(そういうところだぞ、Oracle!)のですが、購入しようとすると最低1000ドルって書いてあるので多分そういう価格感ですね。ちょっと個人だと厳しそうかなぁ。

バックエンドが何しろExadataなので前述の通りのインフラスペックは言うに及ばずOracle Enterprise Editionをベースにしてるので暗号化やパラレルクエリやパーティションなどなど盛りだくさんでサポートしてるはず。この多機能性はMySQLPostgreSQLベースでは実現できないところなので、どこまで使えるかは触ってみないとですが、私とても気になります!

そして、開発者フレンドリーなAlways Free!

www.oracle.com

検証目的という形ではありますが、個人でも企業でも2データベース、 1 CPU、 20 GBストレージが利用できます。無料で! とりあえず私も作ってみましたが、数分ポチポチすれば作れました。

今後、本格運用する前のシステムなら個人でも取りあえずOCIでAutonomous DBで作るってのはありかもしれないです。 当然、WordpressとかOracle DBに現状対応してないのですが今後そういうOSSも対応してくるかもですね。その布石なのか先日ついにOracle JDBCMaven リポジトリに登録されました!

mvnrepository.com

個人向けにはちょっぴり高そうな匂いがしますが、企業ユースだと十分良い選択肢かもですね。低スペックで良いので最低料金を下げるか、無料枠の規定に個人ユースも可とか明記してくれるとさらに最高ですがw

まとめ

キーノートがちょっとジェネリックすぎたり、ランチが微妙になってたりと少し肩透かし感もあったOCO2019ですが、発表できたしそれなりにたくさんの情報もキャッチできたと思うので良かったです。 来年も行けると良いな。

それでは、Happy Hacking!