4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java アプリで任意のメトリクスをInstana で可視化する(JMX編)

4
Posted at

はじめに

Java アプリケーションを Instana で監視すると、ヒープメモリの使用量やGC の挙動、スレッド数など、JVM の標準メトリクスを即座に可視化することができます。
運用開始直後でも 「どの程度メモリを使っているのか」「GC がボトルネックになっていないか」 といった問いに答えやすく、トラブルシューティングや容量計画の初動を大きく短縮できます。

一方、現場ではフレームワークやミドルウェア、サードパーティの 特定ライブラリが公開するメトリクス(例:DB 接続プールの使用率、キュー長、再試行数など)を Instana 上に並べて見たい場面は少なくありません。Instana がセンサーを提供しているテクノロジーであれば少ない手順で可視化できますが、すべてのライブラリが網羅されているわけではありません。

そう言った場合、JMX(Java Management Extensions) を使って任意のメトリクスを可視化することができます。JMX は、ライブラリやアプリが公開している メトリクス(MBean属性)や操作に標準 API 経由でアクセスできる仕組みで、Instana はこの JMXメトリクスを「カスタム JMX メトリクス」として取り込み、ダッシュボードやアラートで活用できます。

今回は、このJMXを使って特定のメトリクスをInstana で可視化する手順をご紹介します。

検証用サンプルアプリ

Instana で Javaアプリ内のデータベースコネクションプールのメトリクスをJMX経由で可視化します。
以下の内容で、データベースにただひたすらデータを登録するアプリケーションを作成します。

環境

  • OS: RHEL9.6
  • Java: OpenJDK17
    • Connection Pool: Apache Commons DBCP2 2.13
    • JDBC Driver: IBM Db2 JDBC Driver (Type 4) 12.1.3
  • データベース: Db2 v12.1.3
  • Instana Agent: エージェントバージョン:2025.12.12.0748、ブートバージョン:1.2.49

データベースとInstana Agentは導入済みとします。

Instana Agent と Db2 の導入・監視については InstanaでDb2を監視してみよう! を参考にしてください。 

実装

概要

  • 常駐型プロセスとして起動し、Db2 の Test1 テーブルへランダムな整数を高速に INSERT
  • Apache Commons DBCP2 のコネクションプールを使用し、JMX で標準 MBean 名
    org.apache.commons.dbcp2:type=BasicDataSource,name=Db2Pool で登録
  • コネクションプールのメトリクス変動が見えるよう、並列処理 + 長時間保持 + バースト の負荷を生成

Db2 テーブル

Db2 に以下のテーブルを作成しておきます。

CREATE TABLE Test1 (
  ID INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
  Data INTEGER NOT NULL,
  CreatedDate TIMESTAMP NOT NULL
);
sudo su - db2inst1
db2 connect to SAMPLE
db2 "<上記SQL>"

JDK と Maven

dnf でインストールしておきます。

# OpenJDK 17 (JRE + JDKツール) を導入
sudo dnf install -y java-17-openjdk java-17-openjdk-devel

# Maven を導入
sudo dnf install -y maven

ソースコード

Java アプリを Mavenプロジェクトとして作成します。

ディレクトリ構成

├── pom.xml
├── run.sh
├── src/main/java/com/example/db2pool
    └── App.java

pom.xml

code
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>db2-dbcp-jmx-demo</artifactId>
    <version>1.0.0</version>
    <name>DB2 + Apache Commons DBCP + JMX Demo</name>
    <description>Java17 resident process that inserts into Db2 using commons-dbcp2 pool and exposes JMX metrics</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <main.class>com.example.db2pool.App</main.class>
    </properties>

    <dependencies>
        <!-- Apache Commons DBCP2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.13.0</version>
        </dependency>
        <!-- IBM Db2 JDBC Driver (Type 4) -->
        <dependency>
            <groupId>com.ibm.db2</groupId>
            <artifactId>jcc</artifactId>
            <version>12.1.3.0</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>${main.class}</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <overWriteReleases>true</overWriteReleases>
                            <overWriteSnapshots>true</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

App.java

code
package com.example.db2pool;

import org.apache.commons.dbcp2.BasicDataSource;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 常駐型プロセス:Db2 の Test1 テーブルへランダムなデータを INSERT。
 * DBCP2 の Connection Pool を利用し、JMX に標準の MBean 名で登録。
 * NumActive / NumIdle の変動が目視できるよう並列・待機を組み合わせています。
 */
public class App {
    private static final Logger LOG = Logger.getLogger(App.class.getName());

    // JMX の標準 MBean 名(ドメイン: org.apache.commons.dbcp2, type=BasicDataSource)
    private static final String JMX_OBJECT_NAME = "org.apache.commons.dbcp2:type=BasicDataSource,name=Db2Pool";

    public static void main(String[] args) throws Exception {
        BasicDataSource ds = buildDataSourceFromEnv();
        // プール名を JMX に登録(BasicDataSource は getConnection() で初回登録します)
        ds.setJmxName(JMX_OBJECT_NAME);

        // 先に 1 コネクション借用して JMX 登録を即座に発生させる
        try (Connection c = ds.getConnection()) {
            LOG.info("Warm-up connection acquired to trigger JMX registration");
        }

        // メトリクスの周期的なログ出力(JMX 経由と API 経由の両方)
        startMetricsLogger(ds);

        // NumActive を増減させるワークロードを投入
        runLoad(ds);

        // 常駐: メインスレッドは終了せず待機
        Thread.currentThread().join();
    }

    /**
     * 環境変数から接続設定を読み込み、BasicDataSource を生成します。
     * 必要な環境変数: DB2_URL, DB2_USER, DB2_PASSWORD
     */
    private static BasicDataSource buildDataSourceFromEnv() {
        String url = getenvRequired("DB2_URL");
        String user = getenvRequired("DB2_USER");
        String pass = getenvRequired("DB2_PASSWORD");

        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("com.ibm.db2.jcc.DB2Driver");
        ds.setUrl(url);
        ds.setUsername(user);
        ds.setPassword(pass);

        // プール設定(変動がわかりやすい値)
        ds.setInitialSize(2);
        ds.setMinIdle(2);
        ds.setMaxIdle(6);
        ds.setMaxTotal(8);
        ds.setMaxWait(Duration.ofSeconds(10));
        ds.setValidationQuery("SELECT 1 FROM SYSIBM.SYSDUMMY1");
        ds.setTestOnBorrow(true);
        ds.setTestWhileIdle(true);

        return ds;
    }

    private static String getenvRequired(String name) {
        String v = System.getenv(name);
        if (v == null || v.isBlank()) {
            throw new IllegalArgumentException("環境変数 " + name + " が未設定です");
        }
        return v;
    }

    /**
     * DBCP の NumActive/NumIdle が変動するような並列処理を実行します。
     */
    private static void runLoad(BasicDataSource ds) {
        int parallelWorkers = 12; // プール maxTotal=8 を超える並列で揺らす
        ExecutorService exec = Executors.newFixedThreadPool(parallelWorkers);
        CountDownLatch start = new CountDownLatch(1);
        Random rnd = new Random();

        for (int i = 0; i < parallelWorkers; i++) {
            int workerId = i;
            exec.submit(() -> {
                try {
                    start.await();
                    while (true) {
                        // たまに長時間保持して輻輳を起こす(NumActive を増やす)
                        boolean longHold = rnd.nextDouble() < 0.20; // 20% は長めに保持
                        insertBatch(ds, 200 + rnd.nextInt(400), longHold ? 1500 : 100);
                        // 次のバーストまで待機
                        Thread.sleep(200 + rnd.nextInt(600));
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        start.countDown();
    }

    /**
     * 1 コネクションを借用して INSERT を繰り返し、任意の時間保持します。
     */
    private static void insertBatch(BasicDataSource ds, int rows, int holdMillis) {
        try (Connection con = ds.getConnection();
             PreparedStatement ps = con.prepareStatement(
                 "INSERT INTO Test1 (Data, CreatedDate) VALUES (?, CURRENT_TIMESTAMP)")) {
            Random rnd = new Random();
            for (int i = 0; i < rows; i++) {
                int val = 1 + rnd.nextInt(100000);
                ps.setInt(1, val);
                ps.executeUpdate();
                // 少しウェイトを入れることで借用時間を伸ばす
                if (i % 50 == 0) {
                    Thread.sleep(5);
                }
            }
            // コネクションを追加で保持してプールの空きを詰まらせる
            Thread.sleep(holdMillis);
        } catch (SQLException e) {
            LOG.log(Level.SEVERE, "DB エラー", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    /**
     * JMX と API の両方から NumActive / NumIdle を 1 秒毎に出力します。
     */
    private static void startMetricsLogger(BasicDataSource ds) throws Exception {
        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName on = new ObjectName(JMX_OBJECT_NAME);

        ses.scheduleAtFixedRate(() -> {
            try {
                Object numActiveJmx = mbs.getAttribute(on, "NumActive");
                Object numIdleJmx = mbs.getAttribute(on, "NumIdle");
                int numActiveApi = ds.getNumActive();
                int numIdleApi = ds.getNumIdle();
                LOG.info(String.format("[Pool] NumActive JMX=%s API=%d / NumIdle JMX=%s API=%d",
                        numActiveJmx, numActiveApi, numIdleJmx, numIdleApi));
            } catch (Exception e) {
                LOG.log(Level.WARNING, "JMX メトリクス取得に失敗", e);
            }
        }, 0, 1, TimeUnit.SECONDS);
    }
}

run.sh

#!/usr/bin/env bash
set -euo pipefail
mvn -DskipTests package
export DB2_URL=jdbc:db2://localhost:25010/SAMPLE
export DB2_USER=<input db2 username>
export DB2_PASSWORD=<input db2 password>
java -cp target/db2-dbcp-jmx-demo-1.0.0.jar:target/lib/* com.example.db2pool.App

Instana 側設定

Instana Agent にて、JMXの監視有効化と対象の明確化が必要になります。
com.instana.plugin.javaを検索し、以下を記載します。
object_name は Javaソースコード内でsetJmxNameメソッドで指定した文字列ですね。

com.instana.plugin.java:
  jmx:
    - object_name: 'org.apache.commons.dbcp2:type=BasicDataSource,name=Db2Pool'
      metrics:
        - attributes: 'NumActive'
          type: 'absolute'
        - attributes: 'NumIdle'
          type: 'absolute'
        - attributes: 'MaxTotal'
          type: 'absolute'
        - attributes: 'MaxIdle'
          type: 'absolute'
        - attributes: 'MinIdle'
          type: 'absolute'

通常、configuration.yamlを更新するとInstana は自動的にconfigのreloadを行なってくれるのですが、jmxに関しては明示的にAgentもしくは対象JVMの再起動が必要です。
念の為、Instana Agentの再起動を行います。

sudo systemctl restart instana-agent

configuration.yamlのコメントにJMX is NOT hot-reloaded and needs to be set before a JVM is discovered.という記載があります。
(意訳:JMX はホットリロードされません。JVM が検出される前に設定しておく必要があります)
再検出=JVM が「新規に見つかる」タイミングでJMX設定が適用されるということです。

参考:
https://www.ibm.com/docs/ja/instana-observability/current?topic=technologies-jmx-custom-metrics

実行

では Javaアプリを起動します。

./run.sh

Mavenビルドの後に、以下のような出力が出続ければOKです。数分待ちます。

Dec 18, 2025 2:23:04 AM com.example.db2pool.App main
INFO: Warm-up connection acquired to trigger JMX registration
Dec 18, 2025 2:23:04 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=0 API=0 / NumIdle JMX=2 API=2
Dec 18, 2025 2:23:05 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0
Dec 18, 2025 2:23:06 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0
Dec 18, 2025 2:23:07 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0
Dec 18, 2025 2:23:08 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0
Dec 18, 2025 2:23:09 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=7 API=7 / NumIdle JMX=1 API=1
Dec 18, 2025 2:23:10 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=6 API=6 / NumIdle JMX=2 API=2
Dec 18, 2025 2:23:11 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=5 API=5 / NumIdle JMX=3 API=3
Dec 18, 2025 2:23:12 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=6 API=6 / NumIdle JMX=2 API=2
Dec 18, 2025 2:23:13 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0
Dec 18, 2025 2:23:14 AM com.example.db2pool.App lambda$startMetricsLogger$1
INFO: [Pool] NumActive JMX=8 API=8 / NumIdle JMX=0 API=0

上記を見る限り、NumActive(ConnectionPoolの該当時間の利用数)とNumIdle(ConnectionPoolの該当時間の空き数)が変動していることがわかりますね。
これがInstana でも表示されて欲しいですね。

Instana での可視化

まずはインフラストラクチャーを確認します。
JVMがちゃんと認識されました。
スクリーンショット 2025-12-18 12.00.18.png
com.example.db2pool.Appをクリックしてダッシュボードを開くを選択します。
JVMのスレッド数やヒープ・メモリーなどを見ることができる画面に遷移しました。これらはデフォルトで参照可能です。
スクリーンショット 2025-12-18 12.00.50.png
JMXを設定すると、「カスタムJMXメトリクス」という項目がこの画面の一番下に追加されます。
今回、Instana Agentのconfigration.yamlで設定した5つのattributeがカスタムJMXメトリクスとして取得できていることがわかります。
NumActiveNumIdleについては、変動している様子をグラフで簡単に見ることができますね!
スクリーンショット 2025-12-18 12.01.41.png

ということで、JMXを使ってApache Commons DBCP2 の ConnectionPoolのメトリクスをInstanaで可視化できることがわかりました。

カスタムダッシュボード

カスタムJMXメトリクスを表示できることはわかったものの、見るためにはJVMの奥まで遷移しなければならず、面倒ですよね。
そういった場合はカスタムダッシュボードを使うとより容易にカスタムJMXメトリクスを参照することができます。
以下は完成例です。
JMXで出力したConnection PoolのActive数、Idle数、最大数(MaxTotal)、そしてDb2側のConnection数を1つのグラフで見ることが可能です。
(10秒間の平均で描画するため少しなまってしまうのが弱点...)
スクリーンショット 2025-12-18 13.26.50.png
カスタムJMXメトリクスはカスタムダッシュボードでメトリックとして選択肢に表示されるため、他のメトリックと同様にGUIから選択することができます。
スクリーンショット 2025-12-18 13.27.11.png
スクリーンショット 2025-12-18 13.27.36.png

最後に

JMXを使って、Instanaが対応していない任意のメトリクスをInstana上で可視化し、さらにカスタムダッシュボードを使って簡単に見れるようにしました。
もちろん他のメトリクスも同様に可視化できますので、「あ、Instanaではこのライブラリのこのメトリクス取れないんだ…」となりましたら、ぜひJMXをお試しください!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?