JMXでTomcatのMBeanからメモリ/スレッド利用数を取得してCloudWatchへ通知する

# 本内容について

JMX(Java Management eXtentions)は稼働中のJavaに対する管理インターフェースを提供します。
今回はAWS EC2で稼働するLinuxサーバのTomcatでJMX設定を有効化し、Javaのヒープやスレッド数を取得、CloudWatchで可視化する内容です。

# プログラム概要

※Javaは1.2で学習が止まっているのと、そこからほぼ書いていなかったため、記述方法などについてはご容赦ください。

サーバ内へ常駐し、所定の間隔で情報を通知する形にしています。
ループ内でJMXの参照を取得、各メトリックを取得し、一括でCloudWatchに通知する流れです。

取得する対象のMemoryPoolMxBeanや、スレッドの名称などは、後述のCommand-line JMX Client等で確認し、環境に合った対象にしておく必要があります。(JavaのGC方式によって取得対象のMemoryPoolMxBeanの名称などが異なります。）

* 毎回 getMBeanServerConnection() を呼んでいるのは、Tomcatサーバの一時的な停止が発生した場合にも、起動後に取得できるようにしています。
* 本プログラムはログを出力していないので、必要であれば追加してください。(Exceptionを黙殺しているのはそのため・・・)

# 参考

JMXで具体的に何が取得できるのかは、「Command-line JMX Client」のお世話になった。

https://qiita.com/uzresk/items/9142c24f218003a4b2a6

# コード

```code.java
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.ThreadMXBean;
import javax.management.ObjectName;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClientBuilder;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.MetricDatum;
import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest;
import com.amazonaws.services.cloudwatch.model.PutMetricDataResult;
import com.amazonaws.services.cloudwatch.model.StandardUnit;

/**
 * リモートJVMからMemoryMBeanを取得する
 * */
public class JMXReporter{

        private static MBeanServerConnection remote = null;
        public static void main(String[] args) throws Exception {

                if (args.length != 4){
                        System.out.println("引数: ホスト名 取得対象tomcat service名称　ポート番号 間隔(秒)");
                        System.exit(1);
                }

                // JMX接続先ホスト名、ポート、接続間隔の取得・設定
                String host = "localhost";
                int port = Integer.parseInt(args[2]);
                int iv = Integer.parseInt(args[3]);

                final String serviceUrlStr = "service:jmx:rmi:///jndi/rmi://"
                                + host + ":" + port + "/jmxrmi";
                final JMXServiceURL target = new JMXServiceURL(serviceUrlStr);

                // 無限ループ
                while(true){

                       JMXConnector connector = null;
                        try {
                                // JMX接続のコネクション取得
                                // 対象の起動停止を考慮し、都度接続する。
                                connector = JMXConnectorFactory.connect(target);
                                remote = connector
                                        .getMBeanServerConnection();
                        } catch (IOException e) {
                                // 接続エラー時はスキップ
                                System.err.println("failed to connect: " + target.toString());
                                Thread.sleep(iv * 1000);
                                continue;
                        }

                        // MemoryPoolMxBeanからの情報取得(GC方式変更時は対象名称の修正が必要)
                        List<MemoryPoolMXBean> beans = new ArrayList<MemoryPoolMXBean>();

                        beans.add(getMemoryPoolMxBean("Par Survivor Space"));
                        beans.add(getMemoryPoolMxBean("Par Eden Space"));
                        beans.add(getMemoryPoolMxBean("CMS Old Gen"));
                        beans.add(getMemoryPoolMxBean("Metaspace"));
                        beans.add(getMemoryPoolMxBean("Compressed Class Space"));
                        beans.add(getMemoryPoolMxBean("Code Cache"));

                        // CloudWatchのクライアント取得
                        final AmazonCloudWatch cw =
                                AmazonCloudWatchClientBuilder.defaultClient();

                        // CloudWatchのディメンションを作成
                        List<Dimension> dimList = new ArrayList<Dimension>();
                        dimList.add( new Dimension()
                        .withName("host")
                        .withValue(args[0]));
                        dimList.add( new Dimension()
                        .withName("tomcat instance")
                        .withValue(args[1]));

                        // MemoryPoolの各beanから値を取り出してListへ積んでおく
                        List<MetricDatum> datumList = new ArrayList<MetricDatum>();

                        for(MemoryPoolMXBean bean : beans) {
                                if(bean == null){continue;}

                                datumList.add( new MetricDatum()
                                                .withMetricName(bean.getName())
                                                .withUnit(StandardUnit.None)
                                                .withValue(new Double(bean.getUsage().getUsed()))
                                                .withDimensions(dimList));
                        }

                        // スレッド数データも追加
                        ObjectName ajpThreadName = new ObjectName(
                                args[1] + ":name=\"ajp-nio-8009\",type=ThreadPool");

                        Double busyth = 0.0;
                        try {
                                busyth = new Double( remote.getAttribute(
                                                ajpThreadName, "currentThreadsBusy").toString());
                        }
                        catch(Exception e) {
　　　　　　　　　　　　　　　　// 黙殺
                        }

                        datumList.add( new MetricDatum()
                                .withMetricName("currentThreadsBusy")
                                .withUnit(StandardUnit.None)
                                .withValue( busyth )
                                .withDimensions(dimList));


                        // 積んだデータを一括でPut
                        PutMetricDataRequest request = new PutMetricDataRequest()
                                .withNamespace("CUSTOM JMX")
                                .withMetricData( datumList );

                        try {
                                PutMetricDataResult response = cw.putMetricData(request);
                        }
                        catch( Exception e) {
                            // 黙殺
                        }

                        if(connector != null){connector.close();}

                        Thread.sleep(iv * 1000);
                }
        }
        /**
         * MxBean名からMemoryPoolMXBeanインスタンスを取得します。
         * @param beanName MxBean名
         * */
        private static MemoryPoolMXBean getMemoryPoolMxBean(String beanName) throws IOException{
                MemoryPoolMXBean mpmxbean
                = ManagementFactory.newPlatformMXBeanProxy(
                        remote,
                        ManagementFactory.MEMORY_POOL_MXBEAN_DOMAIN_TYPE + ",name=" + beanName,
                        MemoryPoolMXBean.class);
                return mpmxbean;
        }
}


```