火曜日 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の名前が変わってしまうことがあります。次のエントリでは、なるべく環境に依存しない、より実用的なスクリプトに仕上げてみようと思います。
月曜日 3 19, 2007
WSIT(Web Services Interoperability Technologies) Milestone 3のチュートリアル(pdf版はこちら)がアップデートされています。2月5日版からの大きな変更は以下の通り:
上記により、チュートリアルの内容としては、WSITが実装予定のWebサービス仕様一覧を一通り網羅する形となっています。
火曜日 3 06, 2007
以前から、私は作成したドキュメントのアーカイブにはlhaを好んで使用していました。その理由は、ドキュメントのファイル名に日本語が使われていたとしても、lhaでアーカイブしておけば、どのOSにそのファイルアーカイブを持っていってもほぼ問題なく、もとの日本語のファイル名を復元することができるからです。tarやzipはファイル名に対する国際化対応がなされていないため、クロスプラットフォームでのドキュメント・アーカイブという目的には適していないというのが実情です。
しかし、どのOSでもlhaがいつでも手軽に使える状態にあるかというと、そうではありません。MacやWindowsならlhaのツールは簡単に入手できるのですが、Solarisでは別途(コンパイルを伴う)インストールが必要です。
また、lhaの漢字ファイル名対応にはもう一つ欠点があります。lhaでは、OS毎に漢字コードが決めうちになっており、UNIX版lhaではデフォルトでファイル名の漢字コードがEUCに固定されています。これは、lhaのコンパイル時に決定されるため、もし、SolarisやLinuxでEUC以外のロケールで運用する場合には、lhaのconfigureオプションで好みの漢字コードに変更してコンパイルし直す必要があります。参考までに、lhaのREADMEファイルに記述されている漢字ファイル名対応に関するconfigureオプションの記述を抜粋します。
・アーカイブ中の漢字ファイル名
オリジナルの LHa for UNIX 1.14i はアーカイブに格納するファイル名の漢
字コードに関して無頓着です。コンパイル時に MULTIBYTE_CHAR を定義した
ときでもアーカイブ中の Shift JIS ファイル名を EUC にすることもなく、
EUC コードのまま(正確にはシステムの漢字コードのまま)アーカイブに格納
したりします。
autoconf 版では、configure オプション --enable-multibyte-filename に
より漢字ファイル名が使用でき、アーカイブに格納されるファイル名の漢字
コードを SJIS 固定として扱います。
--enable-multibyte-filename の引数(システムのファイル名の漢字コード
指定)は、以下の通りです。
--enable-multibyte-filename=sjis
システムの漢字コードを SJIS として扱います。
--enable-multibyte-filename=euc
システムの漢字コードを EUC として扱います。
--enable-multibyte-filename=utf8
システムの漢字コードを UTF-8 として扱います。
今のところ Mac OS X でだけこのオプションをサポートします。
--enable-multibyte-filename=auto (または yes または引数なし)
システムの漢字コードを自動で判別します。自動といっても現状は、
Cygwin, MinGW, HP-UX の場合に SJIS、Mac OS X の場合 UTF-8、
それ以外を EUC とみなすだけです。
--enable-multibyte-filename=no
--disable-multibyte-filename
ファイル名のマルチバイトサポートを無効にします。
デフォルトは、auto です。
ファイル名の漢字コードを自由に変更できるUNIX系OSにおいては、lhaコマンドはアーカイブ解凍時のファイル名の漢字コードをランタイムに指定できないばかりか、UTF-8固定にすることもできません。
それでは、クロスプラットフォームなファイルアーカイバとして、lhaの代わりにJDK/JREに含まれるjarコマンドで代用するというアイデアはどうでしょうか。jarコマンドは、アーカイブ形式にzip形式を採用しており、アーカイブ内のファイル名は常にUTF-8でエンコードされるようになっています。UNIX系OS上では、jarコマンドは圧縮・解凍するときにLANG環境変数をみて、適切にファイル名の漢字コードを変換するようになっています。一見、jarコマンドをlhaの代わりに使用するのは良さそうに思えますが、いくつか不満な点があります。それは、
1.は解凍後に不用なMETA-INFフォルダを削除すればいいのですが、2.と3.については従来は回避方法がありませんでした。しかし、Java SE 6になって2.の問題は解決し、3.の問題はワークアラウンドが簡単に実現できるようになりました。
Java SE 6のjarがファイルのタイムスタンプを復元するようになったことは、Java SE 6のリリースノート:Jar and Zip Enhancementsにも記述されています。リリースノートには、64,000エントリ制限の解除、Windowsファイル名の256文字制限の解除などが明記されており、jarコマンドはクロスプラットフォームでのファイルアーカイバとしても実用的に使えるようになったと言えます。
残る問題はMac OS Xとのファイル名互換性問題だけですが、これもJava SE 6のUnicode Normalization APIのお蔭で簡単に回避できるようになりました。このワークアラウンドを紹介する前に、Mac OS Xのjarコマンドで何が問題なのかをもう少し詳しく見てみます。
例えば、Mac OS X上で「Česká」と「漢字コード」というファイル名を含むアーカイブをjarコマンドで作成します。これをWindows 2000/XPに転送し、jarコマンドで解凍すると下図のように文字化けを起こします。
このように、「ド」は「ト・」に、「Č」は「C・」に、「á」は「a´」に化けてしまっています。実は、Mac OS Xにおけるファイル名の漢字コードは、ロケールに関わらずUTF-8が使用されています。それはそれでよいのですが、このUTF-8が問題で、Mac OS XのファイルシステムではNFD(Normalization Form Decomposition)というUnicodeの方言が使用されています。NFDコーディングルールに従えば、
「ド(U+30C9)」 → 「ト(U+30C8)」+「゛(U+3099)」
「Č(U+010C)」 → 「C(U+0043)」+「ˇ(U+030C)」
のように分離されます。Mac OS Xのjarコマンドは、NFD形式の分離されたUTF-8のファイル名でjarアーカイブを作成してしまうのですが、これを他のWindows 2000/XPのjarコマンドで解凍する際、分離された個別の文字をそのままShift_JISにマッピングしようとするため、文字によっては2文字分に展開され、マッピングできない文字は「・」として表示されてしまったと言うことになります。
ちなみに、WindowsではVistaからNFD形式にも対応しているため、下図のようにUTF-8+NFD形式のファイル名であっても正しく表示することができます。
しかしながら、多くのOSやアプリケーションではロケールをja_JP.UTF-8にした場合でもNFDに対応していないものが多く、「が」のような文字を1つの文字として扱うNFC(Normalization Form Composition)の方が一般的に使用されているようです。
この問題のワークアラウンドとしては、Mac OS Xで作成されたNFD形式のjarのファイル名をNFC形式に変換してやればよいことになります。Java SE 6ではNormalization形式の異なる文字列間を変換するためのAPI java.text.Normalizerクラスが追加されました。このAPIを使用すれば比較的簡単に、NFD形式ファイル名のjarファイルをより一般的なNFC形式ファイル名のjarファイルに変換することができます。以下に、このjarファイル変換のためのサンプルコードを示します。
/*
* JarFileNameFixer.java
*
* Mac OS Xで作成されたjarファイルに含まれるファイル名のコーディング形式を
* NFD形式からNFC形式に変換する。要Java SE 6。
* 使い方:
* $ java JarFileNameFixer <old.jar> <new.jar>
*/
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Normalizer;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
public class JarFileNameFixer {
private void convertEntryNames(JarFile src, JarOutputStream dst)
throws IOException {
for (Enumeration<JarEntry> entries = src.entries();
entries.hasMoreElements();) {
JarEntry oldent = entries.nextElement();
// ディレクトリは無視
if (oldent.isDirectory()) {
continue;
}
// ファイル名をNFC形式に変換する。
String newname = Normalizer
.normalize(oldent.getName(), Normalizer.Form.NFC);
System.out.println(newname);
// 変換したファイル名でJarエントリを作成
JarEntry newent = new JarEntry(newname);
newent.setTime(oldent.getTime());
newent.setComment(oldent.getComment());
// Jarエントリの追加
dst.putNextEntry(newent);
// コンテンツの書き込み
BufferedInputStream is =
new BufferedInputStream(src.getInputStream(oldent));
byte buf[] = new byte[1024];
int count;
while((count = is.read( buf, 0, 1024)) != -1) {
dst.write(buf, 0, count);
}
is.close();
dst.closeEntry();
}
}
private void usage() {
System.out.println("Usage: java "+this.getClass().getName()
+" <old.jar> <new.jar>");
}
public static void main(String[] args) throws IOException {
JarFileNameFixer fixer = new JarFileNameFixer();
if (args.length != 2) {
fixer.usage();
System.exit(1);
}
JarFile srcjar = new JarFile(args[0]);
JarOutputStream dstjar = new JarOutputStream(
new FileOutputStream(args[1]));
fixer.convertEntryNames(srcjar, dstjar);
dstjar.close();
}
}
jarファイルの扱いが面倒なので若干長いソースコードになっていますが、ポイントは赤字で示した Normalizar.normalize(CharSequence,Normalizer.Form) メソッドの呼出し部分だけです。上記のツールでNFC形式に変換したMac OS Xで作成されたjarファイルは、Mac OS X、Windows、Linux、Solarisのいずれの環境でも漢字混じりのファイル名を適切に展開できるようになります。また、もしアプリケーション開発の中で上記のような文字化けを起こした場合は、Java SE 6のNormalization APIを思い出してください。
参考:
木曜日 2 22, 2007
2月12日にWSIT(Web Services Interoperability Technologies)のmilestone 3がリリースされましたが、これに合わせてチュートリアルドキュメント(pdf版はこちら)もアップデートされています。
チュートリアルの内容は全体的に大きく加筆修正されているのですが、新たに10章の「Data Contracts」が追加され、.NET WCFサービスとインターオペラブルなWebサービスを設計するときに気をつけた方がよいと思われる点についていくつかのポイントを説明しています。この章の内容は特にWSITに依存しているわけではないので、.NETと繋ぐ予定がなくても、将来に渡って長く使えるWebサービスインタフェースをJAX-WSベースでデザインしたいという目的にも意味のある内容です。是非チェックしてみて下さい。ここでは、簡単に10章の内容を紹介しておきます。
2.のBigDecimalの精度の違いは要注意です。また、5.の「Date/Calendarの代わりにXMLGregorianCalendarを使う」はちょっと抵抗がありますね。しかし、xs:dateTimeをDate/Calendarマッピングするのに、JAXB用のカスタムのバインディング定義をXMLで定義する手間を考えたら、プログラム側で変換のコードを書いた方が楽なのかも知れません。