こんにちは、SWX3人目の熊谷(悠)です。
ZabbixやDatadogを入れるほどではないけど折角AWS使ってるんだしCloudWatchでグラフを見たい!
という事でスクリプトを作りました。
環境情報
まず、スクリプトからCloudWatchへ書き込みを行うために、EC2に割り当てるIAMロールにCloudWatchFullAccess等のポリシーをアタッチしておいてください。
$ 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
$ aws --v aws-cli/1.16.102 Python/2.7.14 Linux/4.14.77-81.59.amzn2.x86_64 botocore/1.12.92
$ 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コマンドが使用できないので無ければ以下コマンド等を用いてインストールください。
# yum -y install java-1.8.0-openjdk # yum -y install java-1.8.0-openjdk-devel
監視スクリプト
#!/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プロセスを2つ以上同時に起動していて一意に特定できない場合の為に、Java起動引数に-Dprocess.name=hoge等のダミー引数を付与した後、jpsコマンドに-vオプションを追加する事でprocess.name=hogeで対象プロセスを特定できるようにしています。
権限設定
作成した監視スクリプトを実行できるように実行権限を付与します。
# chmod +x /usr/local/cloudwatch/GC-Report/JVM-GC-Report.sh
cron設定
作成した監視スクリプトを定期実行する為に、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 メトリクス画面で指定したカスタム名前空間が追加されていると思います。

参考
JVMのメモリ使用量 - ふなWiki
put-metric-data — AWS CLI 1.16.140 Command Reference
CloudWatch のカスタムメトリクスで JVM の GC を監視する※スクリプトの大部分を参考にさせていただきました。
熊谷 悠司 (記事一覧)