土曜日 5 12, 2007

Custom JMX Client using JRuby (Part 2)

前回のエントリ(Part 1)では、JRubyを用いてアプリケーションサーバに含まれるJMXサーバに接続し、HTTPリスナMBeanからスレッド数を取得するまでの基本的な流れを示しました。

ここでの目的は、自分が興味のある監視項目を一定間隔で取得するツールに仕上げることですので、前回のサンプルに以下の2つの機能を追加することになります。

  • 興味のあるMBean名と属性名の一覧を定義し、定義された全ての属性値を取得する
  • 1.の処理をinterval秒間隔で定期的に実行する
  • MBean名と属性名の一覧定義には、簡略に定義できるように以下のような2次元配列(MBean:属性=1:nなので、実際には3次元配列)の表現を用いることにします。

    # 監視対象のMBean名と属性の定義
    
    mbeandefs = [
      # [<MBean名>, [<属性1>, <属性2>,...]]
      ["com.sun.appserv:name=http-listener-1," +
       "virtual-server=server,type=http-listener,category=monitor,server=server",
       ["currentthreadcount-count", "currentthreadbusy-count"]
      ]
    ].each { |def| def[0] = ObjectName.new(def[0]) }
    

    上記の例では、Sun Java System Application Server (glassfish)におけるhttp-listener-1のMBeanに対して、プール内のスレッド数("currentthreadcount-count")とその中で実際に処理中のスレッド数("currentthreadbusy-count")を取得することを定義しています。なお、ここではRuby配列に対してeachメソッドを適用し、配列内のMBean名を予めObjectNameに変換しておきます。

    後は、ループを定義して、mbeandefs配列に定義された全ての監視項目についての属性値の取得を、一定間隔毎に実行する定義をすればよいことになります。

    # 各MBeanの属性を取得・表示する
    
    while true
      mbeandefs.each do |mbeandef|
        name, attrs = mbeandef[0], mbeandef[1]
        values = con.getAttributes(name, attrs.to_java(:String))
        values.each do |data|
          print "#{data.value}¥t"
        end
      end
      puts
      sleep interval
    end
    

    getAttributes(ObjectName,String[])メソッドの戻り値はAttributeListオブジェクトですが、JRubyではJavaのCollectionクラスであってもeachメソッドを用いたループが定義できる点が嬉しいところです。なお、Rubyの配列はJavaのString[]に自動変換されないようなので、to_javaメソッドを用いて明示的にString[]に変換しています。

    後は、mbeandefs配列に監視したいMBeanの定義を追加していけばいいのですが、実際にはもう少し汎用性を高める必要があります。

    CompositeDataタイプの属性に対応する

    MBeanには、単純にgetAttribute(ObjectName,String)メソッドで単純な値を返してくれるものだけではありません。たとえば、JVMに標準で含まれるメモリ概要に関するMBean("java.lang:type=Memory")の属性"HeapMemoryUsage"を取得すると、複数のプロパティをもったCompositeDataとなっていることが分かります。

    irb(main):032:0> data = con.getAttribute(
    irb(main):033:1\*   ObjectName.new("java.lang:type=Memory"), "HeapMemoryUsage")
    => #<#<Class:01x3228a1>:0x10980e7 @java_object=javax.management.openmbean.Compos
    iteDataSupport(compositeType=javax.management.openmbean.CompositeType(name=java.
    lang.management.MemoryUsage,items=((itemName=committed,itemType=javax.management
    .openmbean.SimpleType(name=java.lang.Long)),(itemName=init,itemType=javax.manage
    ment.openmbean.SimpleType(name=java.lang.Long)),(itemName=max,itemType=javax.man
    agement.openmbean.SimpleType(name=java.lang.Long)),(itemName=used,itemType=javax
    .management.openmbean.SimpleType(name=java.lang.Long)))),contents={committed=415
    66208, init=0, max=518979584, used=33731368})>
    

    このようなCompositeDataオブジェクトに対しては、更にget(String)メソッドを用いてサブプロパティのキーを与えてあげる必要があります。

    irb(main):034:0> data.get("used")
    => 29729624
    

    そこで、監視項目の定義mbeandefs[]の仕様を見直し、CompositeDataの属性値を取得する場合は、属性値とサブプロパティのキーを"."で結合した"A.e"の形式で定義することにします。

    # 監視対象のMBean名と属性の定義
    
    mbeandefs = [
      # [<MBean名>, [<属性1>, <属性2>,...]]
      ["java.lang:type=Memory" ["HeapMemoryUsage.used"]
    
    ].each { |def| def[0] = ObjectName.new(def[0]) }
    

    属性値を取得する時には、属性名に"."が含まれているかどうかで以下のように条件分岐すれば、シンプルな属性とCompositeDataの値をまとめて取得することができます。MBean属性値を取得するループの部分をCompositeDataにも対応できるように改良すると以下のようになります。CompositeDataの場合であってもJavaのようなキャストは必要なく、そのままget(String)メソッドを呼び出せるのは嬉しいところです。

    # 各MBeanの属性を取得・表示する
    
    while true
      mbeandefs.each do |mbeandef|
        name = mbeandef[0]
        # "<属性>.<サブ属性>"を"."で分解し、それぞれattrs[]、subattrs[]にまとめる
        attrs, subattrs = [], []
        mbeandef[1].each do |attrdef|
          attr = attrdef.split(".")
          attrs << attr[0]
          subattrs << attr[1] # "."がなければnil
        end
        # MBeanの属性値をまとめて取得する
        values = con.getAttributes(name, attrs.to_java(:String))
        # 属性値のリストを表示する
        for i in 0..(values.size-1)
          if subattrs[i].nil? then
            print "#{values[i].value}¥t"
          else
            print "#{values[i].value.get(subattrs[i])}¥t"
          end
        end
      end
      puts
      sleep interval
    end
    

    状況によって名前が変わるMBeanに対応する

    MBeanの中には、環境が変わると名前が変わってしまうものがあります。例えば、JVMのメモリ概要が取得できるtype=MemoryPoolなMBeanは、デフォルトでは以下のような名前で取得されます。

    jmx-mem-normal

    もし、JVMでパラレルGCを有効にすると、"Eden Space"と"Survivor Space"は、それぞれ"Par Eden Space"と"Par Survivor Space"という名前に変わります。

    jmx-mem-parallelgc

    また、コンカレントGCを有効にすると、"Tenured Gen"と"Perm Gen"が"CMS Old Gen"と"CMS Perm Gen"に変わります。

    jmx-mem-concgc

    このように環境によって名前が変わるMBeanを監視対象にする場合、監視項目定義であるmbeandefs[]の管理が面倒になります。このような場合は、MBean名の中で変更される可能性のある部分をワイルドカード"\*"に置き換え、queryNames(ObjectName,QueryExp)メソッドを用いて実行時に目的のMBeanを見つけ出すようにするとよいでしょう。MemoryPoolを監視対象とする場合は、以下のようなコードになります。

    objnames = con.queryNames(ObjectName.new("java.lang:type=MemoryPool,\*"), nil)
    objnames.each do |name|
      mbeandefs << [name, ["Usage.used", "Usage.max"]]
    end
    

    また、アプリケーションを構成する各コンポーネント(Servlet、JSP、EJBなど)を監視対象にしたい場合では、全てのコンポーネントのMBean名をmbeandefs[]に列挙するのは面倒ですし、コンポーネントの名前が変われば、それに合わせてmbeandefs[]もメンテナンスしなければなりませんん。このような場合も同様にワイルドカードが役に立ちます。例えば、Sun Java System Application Server (glassfish)のEJBコンテナ上で動作している全てのEJBについて、EJBプールの使用状態を監視するには、以下のような定義をすればよいことになります。

    objnames = con.queryNames(
      ObjectName.new("com.sun.appserv:type=bean-pool,\*"), nil)
    objnames.each do |name|
      mbeandefs << [name, ["numbeansinpool-highwatermark",
                           "numbeansinpool-current"]]
    end
    

    いかがでしょうか。JRubyのようなスクリプト言語がJavaの実行環境で利用できるようになったことにより、多彩なJavaのAPIをスクリプト言語の簡潔なシンタックスを用いてアクセスできるようになりました。これにより、アプリケーション開発をサポートするためのカスタムツールを簡単に作成できるようになったメリットは大変大きいものがあります。JMXカスタムクライアントは、Javaにおけるスクリプト言語サポートを有効に利用する1つの代表的な例であるということができるでしょう。ここで紹介したサンプルのJRubyスクリプトを以下のリンクからダウンロードできるようにしました。

    上記スクリプトはまだまだ改善の余地が沢山ありますので、興味のある方は自由にダウンロードして、ご自身の開発プロジェクト用にカスタマイズして使ってみてください。

    月曜日 5 07, 2007

    Custom JMX Client using JRuby (Part 1)

    アプリケーション・サーバのパフォーマンス・チューニングを行なう場合、JMXを用いるとサーバにほとんど負荷をかけることなく各種リソースの使用状況をリアルタイムに監視することができます。

    Sun Java System Application Serverの場合、バージョン8.1からデフォルトでJMXサーバの機能が利用可能になっており、いつでも任意のJMXクライアントを接続することができるようになっています。JMX接続のための情報は、asadminコマンドでstart-domainサブコマンドを実行した時のコンソール出力か、ドメインのserver.logを参照することで得られます。

    $ asadmin.bat start-domain domain1
    Starting Domain domain1, please wait.
              :
    Standard JMX Clients (like JConsole) can connect to JMXServiceURL:
    [service:jmx:rmi:///jndi/rmi://hostname:8686/jmxrmi] for domain management purposes.
              :
    

    ここで、service:jmx:で始まる特殊なURLがJMXクライアントから接続する場合のJMX URLになります。また、接続時に使用するログインユーザとパスワードにはアプリケーション・サーバの管理者のものを使用します(開発環境でデフォルトを変更していなければ、ユーザがadmin、パスワードがadminadmin)。例えば、JDKに含まれるjconsoleコマンドをJMXクライアントとして、以下のように接続すると、

    下図のように、メモリの使用状況や各種リソース毎のMBeanの値を参照することができます。

    しかし、監視したい項目は複数のMBeanに分散している一方、jconsoleはある1つのMBeanしか一度に見ることができません。しかも、メモリやスレッド以外の表示はある一時点のスナップショットでしか得られないため、時系列のデータを取得することができません。

    UNIXにおけるvmstatコマンドのように、監視したい特定の項目を一定時間毎に取得し、コンソールに出力できるようなJMXクライアントがあれば、出力データを後からいくらでも加工できるため好都合です。このような希望を叶えるJMXクライアントがないものか、しばらくInternetを検索してみましたが、適当なソフトウェアを見つけることができませんでした。

    このような簡単なコンソール・アプリケーションであれば、スクリプト言語を使用して自作してしまった方が早いです。特にJMX監視監視のような目的では、監視したい項目を思考錯誤しながらカスタマイズすることが多いため、スクリプトを修正してすぐに試すというサイクルをストレスなく繰り返すことができます。

    現在、Javaと連係ができるスクリプト言語は、Java SE 6にデフォルトで含まれるJavaScript(Rhino)をはじめ、GroovyJRubyなど非常に多くの実装が利用可能になっています。今回は、JRubyをスクリプト言語に使用して、簡単なカスタムJMXクライアントを作成してみます。

    JRubyのインストールは、既にJDK 1.5以上がインストールされており、JAVA_HOMEが正しく設定されていれば、JRubyのバイナリのアーカイブを展開し、binディレクトリへのパスを環境変数PATHに追加してあげるだけです。

    $ tar zxvf jruby-bin-1.0.0RC1.tar.gz
    $ export PATH=~/jruby-1.0.0RC1/bin:$PATH
    

    JRubyには、Rubyにおけるrubyコマンドとirbコマンドに相当する、jrubyコマンドとjirbコマンドが用意されています。jrubyコマンドは作成済みのRubyスクリプトを実行するために使用し、jirbはインタラクティブにオブジェクトやメソッドの操作を試すために使用します。今回のように小規模のツール開発のコーディングでは、jirbで小規模のコマンド(ステートメント)列を動作確認した後、スクリプトに少しずつ書き貯めていき、jrubyコマンドでまとめて動作確認する、という手順をとっていきます。

    尚、最近リリースされたNetBeans 6 Milestone 9では、JRuby対応がかなりよくなっており、jirbコンソールが使えるだけでなく、エディタでクラス名やメソッド名のコンプリーションができたり、デバッガでブレイクポイントを設定できたりするので、お勧めの環境と言えます。

    それでは、jirbコマンドを使って、JMXクライアントの作成をしていきましょう。まず、JRubyでJavaのクラスライブラリを使用するためのお決まりのおまじないです。

    $ jirb
    irb(main):001:0> require 'java'
    irb(main):002:0> include_class 'javax.management.ObjectName'
    irb(main):003:0> include_class 'javax.management.remote.JMXConnectorFactory'
    irb(main):004:0> include_class 'javax.management.remote.JMXServiceURL'
    

    include_classの呼び出しは、Javaでいうところのimport文と同じ意味ですので、フルパッケージを含めてクラスを指定するなら宣言する必要はありません。また、例外クラスや他のメソッドの戻り値から得られるオブジェクトのクラスは必ずしも明示する必要はないので、Javaのプログラムを記述する場合のimport文に較べてinclude_classの行数は圧倒的に少なくて済みます。

    次に、JMX接続のための各種パラメータですが、コマンドライン引数や設定ファイルにはせず、単純に変数宣言するだけにします(変更が必要であれば、いつでもスクリプトファイルを直接編集すればいいのですから)。

    # MBeanサーバ接続用パラメータ
    
    irb(main):005:0> hostname = "localhost"		# 接続先ホスト名
    irb(main):006:0> port     = "8686"		# 接続先ポート番号
    irb(main):007:0> username = "admin"		# ユーザ名
    irb(main):008:0> password = "adminadmin"	# パスワード
    irb(main):009:0> interval = 5			# 監視項目取得間隔(sec)
    

    次に、JMXサーバへ接続してMBeanServerConnectionオブジェクトを取得する手順です。

    # MBeanサーバに接続する
    
    irb(main):010:0> url = JMXServiceURL.new(
    irb(main):011:1\*       "service:jmx:rmi:///jndi/rmi://" + hostname + ":" + port + "/jmxrmi")
    irb(main):012:0> cred = java.lang.String[2].new
    irb(main):013:0> cred[0], cred[1] = username, password
    irb(main):014:0> env = {"jmx.remote.credentials" => cred}
    irb(main):015:0> connector = JMXConnectorFactory.connect(url, env)
    irb(main):016:0> con = connector.getMBeanServerConnection
    

    JMXConnectorFactory.connect(JMXServiceURL,Map)メソッド呼び出す場合、ログインIDとパスワードは第2引数のMapオブジェクトにキー名"jmx.remote.credentials"で、値がユーザIDとパスワードからなるStringの配列となっているエントリを入れてあげる必要があります。JRubyでは、Javaのオブジェクトにアクセスする際、Rubyの汎用的なクラスはJavaの対応するクラスに自動的に変換されます。上記のJMXConnectorFactory.connect(url,env)の第2引数にはJavaのMapオブジェクトが必要ですが、RubyのHashオブジェクトはJavaのMapに自動変換されますので、サンプルコードの変数envにはマップの定義が簡単なRubyのHashオブジェクトを使用しています。

    ここで注意すべき点は、Ruby⇔Java間のオブジェクト変換はインタフェースから類推可能な範囲に限定されることです。Map内に入れるべきユーザIDとパスワードを以下のようにRubyの配列を使用したくなりますが、JRubyランタイムはRubyの配列をString[]にすべきという知識を与えられていないため、自動変換はされません。

    irb(main):017:0> env = {"jmx.remote.credentials" => [username, password]}
    irb(main):018:0> connector = JMXConnectorFactory.connect(url, env)
    NativeException: java.rmi.MarshalException: error marshalling arguments; nested 
    exception is: 
            java.io.NotSerializableException: org.jruby.RubyArray
            from UnicastRef.java:122:in `sun.rmi.server.UnicastRef.invoke'
                   :
    

    ここでは、以下のように明示的にJavaのString配列を生成してこの問題を回避します。なお、RubyにはJavaと同じStringクラスがあるため、これと区別するためにフルパッケージ名java.lang.Stringを使用します。

    cred = java.lang.String[2].new           # JavaのString[]を明示的に生成
    cred[0], cred[1] = username, password    # 代入時にRubyのStringからJavaのStringに自動変換
    env = {"jmx.remote.credentials" => cred} # Ruby(Hash)とJava(String[])の混成オブジェクトは問題ない
    

    MBeanServerConnectionオブジェクトが取得できたら、後はgetAttribute(ObjectName,String)メソッドに目的のMBean名と属性名を指定して呼び出せば、現在の値を取得することができます。例えば、SJS Application Server 9.x(glassfish v1/v2)における8080ポートのリスナのMBean名は"com.sun.appserv:name=http-listener-1,virtual-server=server,type=http-listener,category=monitor,server=server"で、このMBeanの属性"currentthreadcount-count"の値を取得したい場合は以下のように記述します。なお、このMBeanはアプリケーションサーバの管理コンソールで、"HTTP Service"のモニタリングレベルをLOW以上に設定しないと取得することができませんので、以下のコードを実行する前に管理コンソールにログインして、「Configuration > Monitoring (構成 > 監視)」ツリーをクリックし、モニタリングレベルを変更しておきます。モニタリングレベルの変更は直ちに反映されますので、サーバの再起動は必要ありません。

    irb(main):022:0> objname = ObjectName.new("com.sun.appserv:name=http-listener-1," +
    irb(main):023:1\* "virtual-server=server,type=http-listener,category=monitor,server=server")
    irb(main):024:0> con.getAttribute(objname, "currentthreadcount-count")
    => 2
    

    ここまでの動作確認ができたら、これまでの実行手順をjmxstat.rbというファイル名でスクリプトを保存します。

    # jmxstat.rb
    #
    
    require 'java'
    include_class 'javax.management.ObjectName'
    include_class 'javax.management.remote.JMXConnectorFactory'
    include_class 'javax.management.remote.JMXServiceURL'
    
    # MBeanサーバ接続用パラメータ
    
    hostname = "localhost"		# 接続先ホスト名
    port     = "8686"		# 接続先ポート番号
    username = "admin"		# ユーザ名
    password = "adminadmin"		# パスワード
    interval = 5			# 監視項目取得間隔(sec)
    
    # MBeanサーバに接続する
    
    url = JMXServiceURL.new(
        "service:jmx:rmi:///jndi/rmi://" + hostname + ":" + port + "/jmxrmi")
    cred = java.lang.String[2].new
    cred[0], cred[1] = username, password
    env = {"jmx.remote.credentials" => cred}
    connector = JMXConnectorFactory.connect(url, env)
    con = connector.getMBeanServerConnection
    
    # MBeanの値を取得する
    
    objname = ObjectName.new("com.sun.appserv:name=http-listener-1," +
      "virtual-server=server,type=http-listener,category=monitor,server=server")
    attr = "currentthreadcount-count"
    puts "Current thread count=#{con.getAttribute(objname, attr)}"
    

    上記のサンプルの最後の部分で、putsメソッドで標準出力に結果を表示するように変更していますが、RubyのString内では、#{...}の形式を用いるとその部分をRubyスクリプトとして評価してくれます。ちょうど、JSPにおけるEL表現(よりも実際には制約がないのですが)と同等の機能だと考えてください。

    スクリプトファイルjmxstat.rbを実行するときは、jrubyコマンドを用いて以下のように実行します。

    $ jruby jmxstat.rb 
    Current thread count=2
    

    いかがでしょうか。以上でJMXクライアントの基本的な流れは出来上がりました。このような簡単なツールであれば、クラスを定義する必要もないし、Javaのコードを書くときのように例外ハンドリングにもそれほど神経質にならずに、どんどんロジックを書き進めていきながら動くプログラムを作成していくことができます。

    実際の場面では、環境が変わると監視対象のMBeanの名前が変わってしまうことがあります。次のエントリでは、なるべく環境に依存しない、より実用的なスクリプトに仕上げてみようと思います。

    About

    Takashi Nishigaya
    Principal Consultant
    Technology Solution Consulting
    Oracle Consulting Services

    Search

    Categories
    Archives
    « 4月 2014
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
       
           
    今日