CLOVER🍀

That was when it all began.

TomcatをJMXで覗いてみる

仕事で、JMXを利用したアプリケーション監視をやることになりそうな感じです。今までJMXは情報自体は見聞きしていたのですが、実際にコードを書いたことはなかったのでこれを機にちょっと勉強したいと思います。

監視対象はTomcatになりそうなので、最初からTomcatを使用します。

まずはTomcatをダウンロード。この記事執筆時点での最新版は、7.0.21です。
http://tomcat.apache.org/download-70.cgi
自分は、tar.gz版をダウンロードして解凍しました。

で、JMX Remoteを使用するために、設定を追加。以下のページの「Enabling JMX Remote」に習い、パラメータを記載。
http://tomcat.apache.org/tomcat-7.0-doc/monitoring.html

自分は、setenv.shを作成してこちらに書きました。

$ cat setenv.sh 
export CATALINA_OPTS="-Dcom.sun.management.jmxremote \
    -Dcom.sun.management.jmxremote.port=9012  \
    -Dcom.sun.management.jmxremote.ssl=false  \
    -Dcom.sun.management.jmxremote.authenticate=false"

ポートが9012なのは、同ページ内のその後のAnt Taskのサンプルで、ポートが9012を使用するように設定してあったので、そちらに習いました。

次は、Tomcatを起動して、JMX Remoteが利用できるか確認。これにはJConsoleを使用しました。

ローカルの欄にTomcatのBootstrapが見えていますが、とりあえず気にしない(笑)。

とりあえず、JConsoleで繋がることは確認できました。

これでTomcatにJMX Remoteで接続できることは確認できたので、続いてJMX Remoteに接続していろいろと情報を取得するプログラムを書いてみましょう。

参考にさせていただいたのは、こちらのページです。
http://tech.orz.at/index.php?Java%2FTomcat%E3%81%A8JMX
ただ、移植の際にはScalaで書き換え、あとTomcatのMXBeanの名前はTomcat 7用に変更してあります。

作成したプログラムは、以下です。

import java.lang.management.{ManagementFactory, MemoryMXBean, MemoryPoolMXBean, MemoryUsage, OperatingSystemMXBean, ThreadMXBean}
import javax.management.{MBeanServerConnection, ObjectName}
import javax.management.remote.{JMXConnector, JMXConnectorFactory, JMXServiceURL}

object JmxTomcatMonitor {
  val URL: String = "service:jmx:rmi:///jndi/rmi://localhost:9012/jmxrmi"

  def main(args: Array[String]): Unit = {
    try {
      val monitor = new JmxTomcatMonitor(URL)
      monitor.manageConnect {
        monitor.printThreadMXBean()
        monitor.printMemoryMXBean()
        monitor.printMemoryPoolMXBean()
        monitor.printOperatingSystemMXBean()
        monitor.printCatalinaHttp()
        monitor.printCatalinaAjp()
      }
    } catch {
      case e: Exception => e.printStackTrace()
    }
  }
}

class JmxTomcatMonitor(val jmxUrl: String) {
  val jmxServiceUrl: JMXServiceURL = new JMXServiceURL(jmxUrl)
  var jmxConnector: Option[JMXConnector] = None

  def manageConnect[A](body: => A): A = {
    try {
      jmxConnector = Some(JMXConnectorFactory.connect(jmxServiceUrl, null))
      body
    } finally {
      try {
        jmxConnector.foreach(_.close())
      } finally {
        jmxConnector = None
      }
    }
  }

  protected def requiredConnector[A](body: MBeanServerConnection => A): A = jmxConnector match {
    case Some(connector) => body(connector.getMBeanServerConnection())
    case None => throw new IllegalStateException("Not Manage Connection")
  }

  def printThreadMXBean(): Unit = requiredConnector {
    connection =>
      val threadMXBean = ManagementFactory.newPlatformMXBeanProxy(connection,
                                                                  ManagementFactory.THREAD_MXBEAN_NAME,
                                                                  classOf[ThreadMXBean])

      println("ThreadCount:%d:".format(threadMXBean.getThreadCount))
      println("DeamonThreadCount:%d".format(threadMXBean.getDaemonThreadCount))
  }

  def printMemoryMXBean(): Unit = requiredConnector {
    connection =>
      val memoryMXBean = ManagementFactory.newPlatformMXBeanProxy(connection,
                                                                  ManagementFactory.MEMORY_MXBEAN_NAME,
                                                                  classOf[MemoryMXBean])

      println("HeapMemory:%d".format(memoryMXBean.getHeapMemoryUsage.getUsed))
      println("NonHeapMemory:%d".format(memoryMXBean.getNonHeapMemoryUsage.getUsed))
  }

  def printMemoryPoolMXBean(): Unit = requiredConnector {
    connection =>
      val names = List("Code Cache",
                       "PS Eden Space",
                       "PS Old Gen",
                       "PS Perm Gen",
                       "PS Survivor Space")

      for (name <- names) {
        val memoryPoolMXBean = ManagementFactory.newPlatformMXBeanProxy(connection,
                                                                        "%s,name=%s".format(ManagementFactory.MEMORY_POOL_MXBEAN_DOMAIN_TYPE, name),
                                                                        classOf[MemoryPoolMXBean])
        println("%s:%d".format(name, memoryPoolMXBean.getUsage.getUsed))
      }
  }

  def printOperatingSystemMXBean(): Unit = requiredConnector {
    connection =>
      val osMXBean = ManagementFactory.newPlatformMXBeanProxy(connection,
                                                              ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME,
                                                              classOf[OperatingSystemMXBean])
      println("AvailableProcessors:%d".format(osMXBean.getAvailableProcessors))
      println("LoadAverage:%f".format(osMXBean.getSystemLoadAverage))
  }

  def printCatalinaHttp(): Unit = requiredConnector {
    connection =>
      val httpThreadName = new ObjectName("Catalina:type=ThreadPool,name=\"%s\"".format("http-bio-8080"))
      println("Catalina httpCurrentThreadCount:%s".format(
        connection.getAttribute(httpThreadName, "currentThreadCount")))
      println("Catalina httpCurrentThreadsBusy:%s".format(
        connection.getAttribute(httpThreadName, "currentThreadsBusy")))
  }

  def printCatalinaAjp(): Unit = requiredConnector {
    connection =>
      val ajpThreadName = new ObjectName("Catalina:type=ThreadPool,name=\"%s\"".format("ajp-bio-8009"))
      println("Catalina ajpCurrentThreadCount:%s".format(
        connection.getAttribute(ajpThreadName, "currentThreadCount")))
      println("Catalina ajpCurrentThreadsBusy:%s".format(
        connection.getAttribute(ajpThreadName, "currentThreadsBusy")))
  }
}

上から順に、JavaのプラットフォームMBeanの情報(スレッド、メモリ、メモリプール、OS情報)、Tomcatのスレッドプールの情報を出力するプログラムになっています。

JavaプラットフォームMBeanのAPIは、java.lang.managementパッケージ内の各クラスを参照してくださいね。

参考にさせていただいたページとはTomcatのバージョンが違うのか、TomcatのThreadPoolのname属性を"(ダブルクォート)で囲まないといけませんでした。あと「bio」という文字(たぶん、Blocking IO)も入ってましたね。

コードに埋もれていますが、JMX Remoteへの接続URLはこの部分です。

  val URL: String = "service:jmx:rmi:///jndi/rmi://localhost:9012/jmxrmi"

Scalaへ書き換えた時のアレンジとしては、JmxTomcatMonitorクラスのmanageConnectメソッドに渡す関数内でないと各種printメソッドが呼べないようになっています。まあ、各種情報をダンプする箇所をメソッドに切りたかったのですが、都度Connectionを開くのが嫌だった、それだけなんですけど。

あとは、java.lang.managementパッケージのクラス以外で登録されている情報を扱う場合には、ObjectNametとMBeanServerConnectionを利用するということがわかりました。やっぱり書いてみるもんですね。

実行結果はこちらです。

> run
[info] Compiling 1 Scala source to /xxxxx/jmx-tomcat-monitor/target/scala-2.9.1.final/classes...
[info] Running JmxTomcatMonitor 
ThreadCount:32:
DeamonThreadCount:31
HeapMemory:25088312
NonHeapMemory:29564424
Code Cache:2249664
PS Eden Space:17745232
PS Old Gen:7343080
PS Perm Gen:27314760
PS Survivor Space:0
AvailableProcessors:2
LoadAverage:0.570000
Catalina httpCurrentThreadCount:10
Catalina httpCurrentThreadsBusy:0
Catalina ajpCurrentThreadCount:0
Catalina ajpCurrentThreadsBusy:0
[success] Total time: 1 s, completed Sep 18, 2011 7:01:46 PM

MemoryPoolのnameとかどこから調べたのかなぁ?と思ったのですが、JConsoleでMBeansの欄を見たらあっさり分かりました。

これに目を通していけば、登録されているMBeanは取得できそうですね。