EC2内JVMのメモリ推移をCloudWatchで監視するスクリプトを作った

# はじめに
ZabbixやDatadogを入れるほどではないけど折角AWS使ってるんだしCloudWatchでグラフを見たい！
という事でスクリプトを作りました。

# 環境情報
まず、スクリプトからCloudWatchへ書き込みを行うために、EC2に割り当てるIAMロールに`CloudWatchFullAccess`等のポリシーをアタッチしておいてください。

```console:OS及びアーキテクチャ,カーネルバージョン
$ cat /etc/system-release
Amazon Linux 2

$ uname -a
Linux hogehuga.ap-northeast-1.compute.internal 4.14.77-81.59.amzn2.x86_64 #1 SMP Mon Nov 12 21:32:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
```
```console:AWSCLIインストールバージョン
$ aws --v
aws-cli/1.16.102 Python/2.7.14 Linux/4.14.77-81.59.amzn2.x86_64 botocore/1.12.92
```
```console:javaインストールバージョン
$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

$ yum list installed | grep java 
java-1.8.0-openjdk.x86_64             1:1.8.0.191.b12-0.amzn2        @amzn2-core
java-1.8.0-openjdk-devel.x86_64       1:1.8.0.191.b12-0.amzn2        @amzn2-core
java-1.8.0-openjdk-headless.x86_64    1:1.8.0.191.b12-0.amzn2        @amzn2-core
javapackages-tools.noarch             3.4.1-11.amzn2                 @amzn2-core
python-javapackages.noarch            3.4.1-11.amzn2                 @amzn2-core
tzdata-java.noarch                    2018e-3.amzn2                  @amzn2-core
```

openjdk-develもインストールしておかないと、`jps`と`jstat`コマンドが使用できないので無ければ以下コマンド等を用いてインストールください。

```console:JDKインストール
# yum -y install java-1.8.0-openjdk
# yum -y install java-1.8.0-openjdk-devel
```

# 監視スクリプト
```bash:JVM-GC-Report.sh
#!/bin/bash

function usage {
    cat <<EOF
Usage:
    $(basename ${0}) [options] <PROCESS_NAME> <NAMESPACE_NAME>
Description:
    $(basename ${0}) is a tool for Get JVM information and send report to cloudwatch
Required Args:
    PROCESS_NAME        Java Process name to report on
    NAMESPACE_NAME      Cloudwatch custom namespace name to report
Options:
    -h                  show this help, then exit
EOF
    return 0
}

# オプション解析
while getopts h OPT ; do
    case $OPT in
      h)
        usage
        exit 0
        ;;
    esac
done

if [ $# -ge 2 ]; then
    PROCESS_NAME="${1}"
    NAMESPACE_NAME="${2}"
elif [ $# -eq 1 ]; then
    echo 'Error: Argument <NAMESPACE_NAME> is Required'
    usage
    exit 1
else
    echo 'Error: Argument <PROCESS_NAME> is Required'
    usage
    exit 1
fi

PID=`jps -v | grep ${PROCESS_NAME} | awk '{ print $1 }'`

if [ -z "$PID" ]; then
    echo 'Error: Process Not Found'
    exit 1
fi

JSTAT=`jstat -gcutil ${PID} | tail -n 1`
S0=`echo ${JSTAT} | awk '{ print $1 }'`
S1=`echo ${JSTAT} | awk '{ print $2 }'`
EDEN=`echo ${JSTAT} | awk '{ print $3 }'`
OLD=`echo ${JSTAT} | awk '{ print $4 }'`
META=`echo ${JSTAT} | awk '{ print $5 }'`
FGC=`echo ${JSTAT} | awk '{ print $9 }'`
FGCT=`echo ${JSTAT} | awk '{ print $10 }'`

META_DATA='http://169.254.169.254/latest/meta-data'
INSTANCE_ID=`curl -sS ${META_DATA}/instance-id`
export AWS_DEFAULT_REGION=`curl -sS ${META_DATA}/placement/availability-zone | sed -e 's/.$//g'`

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Survivor area 0 usage rate' \
    --unit Percent \
    --value ${S0} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Survivor area 1 usage rate' \
    --unit Percent \
    --value ${S1} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Eden area usage rate' \
    --unit Percent \
    --value ${EDEN} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Old area usage rate' \
    --unit Percent \
    --value ${OLD} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Metaspace usage rate' \
    --unit Percent \
    --value ${META} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Full GC Count' \
    --unit Count \
    --value ${FGC} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Full GC Time' \
    --unit Milliseconds \
    --value ${FGCT} \
    --dimensions InstanceId=${INSTANCE_ID}

JSTAT=`jstat -gccapacity ${PID} | tail -n 1`
S0C=`echo ${JSTAT} | awk '{ print $4 }'`
S1C=`echo ${JSTAT} | awk '{ print $5 }'`
EC=`echo ${JSTAT} | awk '{ print $6 }'`
OC=`echo ${JSTAT} | awk '{ print $10 }'`

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Survivor area 0 usage capacity' \
    --unit Kilobytes \
    --value ${S0C} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Survivor area 1 usage capacity' \
    --unit Kilobytes \
    --value ${S1C} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Eden area usage capacity' \
    --unit Kilobytes \
    --value ${EC} \
    --dimensions InstanceId=${INSTANCE_ID}

aws cloudwatch put-metric-data \
    --namespace "${NAMESPACE_NAME}" \
    --metric-name 'Old area usage capacity' \
    --unit Kilobytes \
    --value ${OC} \
    --dimensions InstanceId=${INSTANCE_ID}
```
同じクラス名のJavaプロセスを２つ以上同時に起動していて一意に特定できない場合の為に、Java起動引数に`-Dprocess.name=hoge`等のダミー引数を付与した後、`jps`コマンドに`-v`オプションを追加する事で`process.name=hoge`で対象プロセスを特定できるようにしています。

# 権限設定
作成した監視スクリプトを実行できるように実行権限を付与します。

```
# chmod +x /usr/local/cloudwatch/GC-Report/JVM-GC-Report.sh
```

# cron設定
作成した監視スクリプトを定期実行する為に、crontabへ設定を追加します。

```:crontab
# CloudWatch JVM GC Report
* * * * * /usr/local/cloudwatch/GC-Report/JVM-GC-Report.sh Bootstrap 'JVM GC' > /dev/null 2>&1
```

# 確認
実行して特にエラーが返ってこなければOKです。

```
# /usr/local/cloudwatch/GC-Report/JVM-GC-Report.sh Bootstrap 'JVM GC'
```

以下画像のようにCloudWatch メトリクス画面で指定したカスタム名前空間が追加されていると思います。
![2020-08-24_21h26_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/8e9b3243-8a86-b43d-976e-55afcd3fe53f.png)

# 参考
[JVMのメモリ使用量 - ふなWiki](http://blue-red.ddo.jp/~ao/wiki/wiki.cgi?page=JVM%A4%CE%A5%E1%A5%E2%A5%EA%BB%C8%CD%D1%CE%CC)
[put-metric-data — AWS CLI 1.16.140 Command Reference](https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/put-metric-data.html)
[CloudWatch のカスタムメトリクスで JVM の GC を監視する](https://blog.manabusakai.com/2014/09/cloudwatch-jvm-gc)※スクリプトの大部分を参考にさせていただきました。