mackerel-agentのDockerイメージでECS上で動作するjavaアプリケーションのjvmを監視する

# 概要
これまでリモート経由でのメトリクスに対応していなかった[mackerel-plugin-jvm](https://github.com/mackerelio/mackerel-agent-plugins/tree/master/mackerel-plugin-jvm)がリモート経由でも対応されたと教えていただいたのでやってみました。
これが可能になれば、軽量さも重視されるコンテナにおいてmackerel-agentを入れることを回避できるようになりますね。

ECS上に構築したjavaアプリケーションと同じサービス内で起動した [docker mackerel-agent ](https://hub.docker.com/r/mackerel/mackerel-agent/) から [mackerel-plugin-jvm](https://github.com/mackerelio/mackerel-agent-plugins/tree/master/mackerel-plugin-jvm)を使用してjavaアプリケーションのjvm関連のメトリクスの作成までを行います。

# 前提
以下のものは本記事では割愛します。

* ECSの使い方
* dockerの使い方
* docker-composeの使い方
* ecs-cliの使い方
* mackerelエージェントのインストール
* javaアプリケーションの詳細な中身

# docker-composeの定義ファイルを作成する
## jvm関連の値を取得するために必要な設定
javaアプリケーションを実行しているサーバー上にmackerel-agentをインストールする場合には意識することはなかったのですが、
他コンテナから取得させるには以下のものが必要となります。

* appコンテナ
  * 1099ポート(デフォルト)のEXPOSE
  * rmiregistry の起動
  * jstatd の起動
* mackerel-agentコンテナ
    * jdkのインストール(jps、jstat、jinfo)
    * plugin-jvmの設定
* コンテナ間のlink

## appコンテナ
### 1099ポートのEXPOSE
これはjava-appで定義しているDockerfileのEXPOSEに1099を追加するだけです。

```docker:Dockerfile
FROM java:8-alpine
　：
　：
EXPOSE 9000 1099
```
※ アプリケーションの使用ポートを9000としています。
### rmiregistry の起動
こちらはECS起動時に必要な内容となります。
以下のような内容をdocker-composeファイルの`command`ブロックで定義してあげれば起動可能です。

```
eval 'rmiregistry -J-Djava.rmi.server.codebase=file://$${JAVA_HOME}/lib/tools.jar &'
```
### jstatd の起動
まずはポリシーファイルを作成します。
このあたりは各社のポリシーに従って調整してください。

```policy:tools.policy
grant {
    permission java.security.AllPermission;
};
```
※ こちらのpolicyファイルはDockerfile内でCOPYするか、docker-compose.ymlの `command` ブロック内で出力させてあげてください。


`jstatd` の起動も dokcer-composeファイルの `command` ブロックに定義してやります。

```
eval 'jstatd -J-Djava.security.policy=/path/to/tools.policy -J-Djava.rmi.server.hostname=$${HOSTNAME} &'
```
１コンテナ１プロセスの考え方はそのうち…

## mackerel-agentコンテナ
### jdkのインストール
Dockerfileを作って管理するのが嫌だったので、こちらも　docker-composeファイルの `command` ブロックに記述してやります。

```
yum install -y java-1.8.0-openjdk-devel
```

### plugin-jvmの設定
今回はDockerfileは作成しないので、 `command` ブロックに以下のように追記します。

```
mkdir /etc/mackerel-agent/conf.d \
&& echo \"[plugin.metrics.jvm]\" > /etc/mackerel-agent/conf.d/jvm.conf \
&& echo 'command = \"mackerel-plugin-jvm -javaname=NettyServer -host=java-app\"' >>  /etc/mackerel-agent/conf.d/jvm.conf \
&& echo 'display_name = \"java-app\"' >> /etc/mackerel-agent/mackerel-agent.conf
```
最終行でMackerelの管理画面上で見分けの付けやすいように名称を付与しています。

### コンテナ間link
mackerel-agentのコンテナから名前解決でjava-appを探すために必要になります。
docker-composeファイルの `links` ブロックに追記してやるだけです。

```
links:
  - java-app
```

## docker-composeの定義を書く
これらを踏まえてdocker-composeファイルを書いていきます。

```docker:docker-compose.yml
version: '2'
services:
  java-app:
    mem_limit: 268435456
    image: java-app:latest
    user: quartette
    environment:
      - APP_ENV=dev
      - JAVA_OPTS=-Xms128m -Xmx256m -Xmn64m -XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32m -Dfile.encoding=UTF-8 -Duser.country=JP -Duser.language=ja -Duser.timezone=Asia/Tokyo -Djava.awt.headless=true -XX:-OmitStackTraceInFastThrow
    command:
      /bin/sh -c
        "eval 'rmiregistry -J-Djava.rmi.server.codebase=file://$${JAVA_HOME}/lib/tools.jar &' \
        && eval 'jstatd -J-Djava.security.policy=/path/to/tools.policy -J-Djava.rmi.server.hostname=$${HOSTNAME} &' \
        && /path/to/app.jar"
    labels:
      - Env=dev
    logging:
      driver: awslogs
      options:
        awslogs-group: "java-app"
        awslogs-region: "ap-northeast-1"
        awslogs-stream-prefix: "quartette"
  mackerel-agent:
    image: mackerel/mackerel-agent:latest
    mem_limit: 134217728
    command:
      /bin/sh -c
        "mkdir /etc/mackerel-agent/conf.d \
        && yum install -y java-1.8.0-openjdk-devel \
        && echo \"[plugin.metrics.jvm]\" > /etc/mackerel-agent/conf.d/jvm.conf \
        && echo 'command = \"mackerel-plugin-jvm -javaname=NettyServer -host=java-app\"' >>  /etc/mackerel-agent/conf.d/jvm.conf \
        && echo 'display_name = \"java-app\"' >> /etc/mackerel-agent/mackerel-agent.conf \
        && /startup.sh
        "
    environment:
      - apikey=[mackerel api key]
      - auto_retirement=true
      - enable_docker_plugin=true
      - opts=-v
      - include=/etc/mackerel-agent/conf.d/*.conf
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/mackerel-agent/:/var/lib/mackerel-agent/
    links:
      - java-app
    logging:
      driver: awslogs
      options:
        awslogs-group: "mackerel-container-agent"
        awslogs-region: "ap-northeast-1"
        awslogs-stream-prefix: "quartette"
```

# デプロイをして確認
## ECSのサービスを更新する
私の環境では、 `ecs-cli` を使ってデプロイをしているので以下のようにします。

```
ecs-cli compose -p java-app -f docker-compose.yml service up --container-name java-app --container-port 80 --roke ecsServiceRole --target-group-arn [targetGroupArn] --deployment-min-healthy-percent 50
```

## Mackerel上で確認
上手く値が取得できていればこんな感じのグラフができているはずです。

![スクリーンショット 2017-12-01 15.24.47.png](https://qiita-image-store.s3.amazonaws.com/0/63155/a45e78aa-c7b6-5032-df18-1af28a72e661.png)

## グラフが表示されない場合
多くの場合、mackerel-agentからアプリケーションとの通信に失敗しているだけなのでコンテナにログインして確認をしましょう

### mackerel-agent側からの確認
* plugin-jvm内で実行されているコマンドを直接実行する
mackerel-jvm-pluginではjps、jstat、jinfoあたりのコマンドが実行されています。
jpsとjstatの値が取れていればひとまずは問題ないと思います。

```
# jps java-app
5 RegistryImpl
6 Jstatd
7 NettyServer

# jstat -gc 7@java-app
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
30720.0 29184.0 1880.2  0.0   71168.0  44096.2   393216.0   283934.1  60032.0 56869.0 8320.0 7870.9    150    2.792   3      3.371    6.163
```
正常に設定できていればこんなような値が取得できるはずです。

* mackerel-jvm-pluginを直接実行する  
jpsとjstatが取得できれば問題なくメトリクスが作成されるはずですが、plugin内でエラーになっている可能性もあるのでjvm-plugin も実行してみます。  
正常に取得できるようであれば以下のような値が取得できます。

```
# mackerel-plugin-jvm -javaname=NettyServer -host=java-app
jvm.nettyserver.gc_events.YGC	0.000000	1512110013
jvm.nettyserver.gc_events.FGC	0.000000	1512110013
jvm.nettyserver.gc_time.YGCT	0.000000	1512110013
jvm.nettyserver.gc_time.FGCT	0.000000	1512110013
jvm.nettyserver.memorySpace.oldSpaceRate	72.208176	1512110013
jvm.nettyserver.memorySpace.newSpaceRate	11.839294	1512110013
jvm.nettyserver.gc_time_percentage.YGCT	0.000000	1512110013
jvm.nettyserver.gc_time_percentage.FGCT	0.000000	1512110013
jvm.nettyserver.new_space.NGCMX	134217728.000000	1512110013
jvm.nettyserver.new_space.NGC	134217728.000000	1512110013
jvm.nettyserver.new_space.EU	13965107.200000	1512110013
jvm.nettyserver.new_space.S0U	1925324.800000	1512110013
jvm.nettyserver.new_space.S1U	0.000000	1512110013
jvm.nettyserver.old_space.OGCMX	939524096.000000	1512110013
jvm.nettyserver.old_space.OGC	402653184.000000	1512110013
jvm.nettyserver.old_space.OU	290748518.400000	1512110013
jvm.nettyserver.metaspace.MCMX	1128267776.000000	1512110013
jvm.nettyserver.metaspace.MCMN	0.000000	1512110013
jvm.nettyserver.metaspace.MC	61472768.000000	1512110013
jvm.nettyserver.metaspace.MU	58233856.000000	1512110013
jvm.nettyserver.metaspace.CCSC	8519680.000000	1512110013
jvm.nettyserver.metaspace.CCSU	8059801.600000	1512110013
```

### java-app側からの確認
* プロセスの確認
rmiregistryやjstadが起動しているかを確認します。

```
# ps
PID   USER     TIME   COMMAND
    1 quartette   0:00 /bin/sh -c eval 'rmiregistry -J-Djava.rmi.server.codebase=file://${JAVA_HOME}/lib/tools.jar &'  && eval 'jstatd -J-Djava.security.policy
    5 quartette   0:45 rmiregistry -J-Djava.rmi.server.codebase=file:///usr/lib/jvm/java-1.8-openjdk/lib/tools.jar
    6 quartette   2:00 jstatd -J-Djava.security.policy=/home/tlab-app/tools.policy -J-Djava.rmi.server.hostname=cefd4fb34c63
    7 quartette   7:06 /usr/lib/jvm/java-1.8-openjdk/bin/java -Xms128m -Xmx256m -Xmn32m -XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32m -Dfile.encoding=UTF-8
 2461 quartette   0:00 /bin/bash
 2466 quartette   0:00 ps
```
いくつか表示されていますが、PID　５、6の `rmiregistry` と `jstatd` が起動していれば問題ありません。
java-app側が問題ないはずなのにmackerel-agent側から値が取れない場合、EXPOSEでポートの指定が上手くいっていないか、コンテナ間リンクが上手くいっていない場合が多いです。

# 最後に
最近は運用をコンテナで行うことも増えてきて、コンテナ関連監視に悩んでいました。
コンテナ内にエージェントをインストールすることなく監視をしたい場合などにmackerelのコンテナベースのagentは便利だと思います。
DataDogやNewRelicなどでも同様のことはできるとは思いますが、Mackerelであれば日本語でのサポートも行ってくれるのでいいですね。