火曜日 5 08, 2007
火曜日 5 08, 2007
アプリケーション・サーバのパフォーマンス・チューニングを行なう場合、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)をはじめ、GroovyやJRubyなど非常に多くの実装が利用可能になっています。今回は、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の名前が変わってしまうことがあります。次のエントリでは、なるべく環境に依存しない、より実用的なスクリプトに仕上げてみようと思います。