Dockerコンテナで動くJVMアプリケーションに対して async-profiler を使ってみる

Dockerコンテナ上でasync-profilerをJVMアプリケーションに掛けてみます。 async-profilerを使って flame graphを取得することで アプリケーションのボトルネックを調べてみましょう。
2019.03.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。齋藤です。 今日は負荷試験の合間にブログを書いてみます。

前回やった async-profiler の記事 の続きで 今回はDocker コンテナ上で 動くJVMアプリケーションに対してprofilerを動かしてみます。 コンテナのOSはdebianです。

はじめに

今回はperformanceのメトリクスを取るために --privileged のパラメータを渡す必要があります。 Linux の perf eventを読みとる必要があるからです。

そのため、以下の形で特権モードで動かします。 なお、特権モードを有効にして本番稼働させるのは危険です。 十分に危険性を理解した上で行ってください。

docker run --privileged openjdk:8-jdk

また、Linux の perf eventにはコンテナホストの情報が含まれているため デフォルトでは無効化/制限されており、アプリケーションを起動する前にパラメータを弄る必要があります。 そのため、準備をしてから profilerを掛けてみます。

前提

前提を示しておきます。

  • macOS Mojave version 10.14.3
  • docker 18.09.2
  • Docker Desktop 2.0.0.3 (31259)

その他の前提は以下の通り

$ docker version
Client: Docker Engine - Community
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        6247962
 Built:             Sun Feb 10 04:12:39 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.6
  Git commit:       6247962
  Built:            Sun Feb 10 04:13:06 2019
  OS/Arch:          linux/amd64
  Experimental:     false

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
openjdk             8-jdk               5f8e49b0a018        4 weeks ago         624MB

$ docker run openjdk:8-jdk cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

$ docker run openjdk:8-jdk java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

準備

今回はコンテナイメージに手を入れずに実行したいため 以下のような entrypoint.sh を初期化処理のために、コンテナホストに用意しておきます。 コンテナホストからgithubにアクセス出来る必要があります。

初期化処理の中で、perf eventが取れるようにしておきます。

#!bin/bash
# linux perf eventが取れるようにしておく
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 0 > /proc/sys/kernel/kptr_restrict
# 必要に応じてデバッグシンボルを入れておく。
# デバッグシンボルを入れておくと
# JVMのメソッドのトレースも出来るようになります。
# 必要があれば入れておきましょう。
# apt update && apt install openjdk-8-dbg
curl -s -L -o /tmp/async-profiler-1.5-linux-x64.tar.gz https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.5/async-profiler-1.5-linux-x64.tar.gz
mkdir -p /tmp/profiler && tar xf /tmp/async-profiler-1.5-linux-x64.tar.gz -C /tmp/profiler
exec $@

また、profileで使うJavaのアプリを用意しておきます。 今回は簡単なデモ用のアプリです。

$ cat Test.java
import java.util.*;

public class Test {
  public static void main(String[] args) throws Exception {
    while(true) {
       Thread.sleep(1000);
       Random r = new Random();
       for(int i = 0; i < 1000; i++) {
         System.out.println(r.nextInt());
       }
    }
  }
}

dockerコンテナ上でprofileを取ってみる

今回はbashでインタラクティブに操作しながらprofileを取ります。 また、先程示した、entrypoint.shを使って起動させます。

$ docker run -it --privileged -v `pwd`/entrypoint.sh:/tmp/entrypoint.sh -v `pwd`/Test.java:/Test.java --entrypoint "/tmp/entrypoint.sh" openjdk:8-jdk bash

ここから先はdockerコンテナ内での操作です。

# デモ用のプログラムをコンパイル
> javac Test.java
> java Test > /dev/null &
# JVMのPIDを調べる
> jps
7 Jps
20 Test
# 30秒間、profileを取るサンプル
> /tmp/profiler/profiler.sh -d 30 -f `pwd`/profile.svg 60
> exit

svgの取得が終わったらコンテナを終了して コンテナからsvgを拾ってきてブラウザで表示してみましょう。

# コンテナからsvgを拾ってくる
$ docker container cp <container-id>:/profile.svg .

ブラウザで拾ってきたSVGを表示すると、以下のようなFlame Graphが見れます。

以下の画像は、デバッグシンボルを入れた後に取ったメトリクスです。 黄色いバーが見えますが、JVMのメソッドが呼ばれている事がわかります。

まとめ

前回 はローカルのMacの端末でprofilerを取りましたが 今回の記事では、Dockerコンテナ上で動かしているアプリケーションに対して コンテナの中からprofilerを掛けることが出来ました。 JVMアプリケーションでボトルネックを調べたい時に試してみてください。

現在、自分達が作っているアプリケーションはECSなどで動いているので、他にも手順が必要なのですが 今回はここまでにしておきます。