Hatena::ブログ(Diary)

#9 Backyard RSSフィード

2012年04月27日

Java SE 7 Update 4 での変更点

Java 7 の Update 4 がリリースされたようですのでリリースノートから気になる点を拾い読み。

OS X サポート

Apple が Java の OS X 対応を止めてしまったために Java 7 リリースから半年ほど遅れましたが、このバージョンからやっと利用できるようになりました。いくつか制約があって:

  • OS X 10.7 Lion 以降、64bit 版のみの提供。
  • 「開発目的のみ」つまり製品版アプリケーションでの使用はサポートされない。
  • Java Plugin、Java Web Start などのデプロイは含まれていない。
  • 幾つかのサービス機能が機能しない。

Oracle としては初の OSX 対応だからまだ試験的な部分があるようです。とりあえずウチの Leopard は見捨てられているようですが… OS X へのインストール方法はこちら: http://docs.oracle.com/javase/7/docs/webnotes/install/mac/mac-jdk.html

Garbage-First GC サポート

今まで Experimental だった Garbage-First (G1) GC がようやく Full Support となりました。G1 GC は世代別 GC の進化版。広大なメモリとマルチプロセッサを備えたサーバ環境での GC の並列性能を高める事ができます。Stop the World として許容できる目標時間を指定できる安心感。ちなみに仕事場の Hadoop / HBase 6 物理ノード環境で検証していますが応答速度が 110〜150% 程度改善されました。

その他

あとはざっと気になる点を箇条書きで。

  • -XX:+UseParallelGC が有効な場合はデフォルトで -XX:+UseParallelOldGC が有効になるようになった。
  • jcmd という実行状況分析用コマンドが追加された。
  • JavaFX 2.1, JAXP 1.4.6, Java DB 10.8.2.2 へバージョンアップ。
  • -XX:+UnlockCommercialFeatures で商用ライセンスによりロックされていた機能を解除できるようになった。
  • -XX で未定義の拡張オプションが指定された時、エラーメッセージにそのオプションを表示するようになった。
  • もろもろのバグフィクス

2012年03月08日

JavaはCOBOLのように縮こまらないし死にもしないとOracleが主張

以下の記事:

Java won't curl up and die like Cobol, insists Oracle
http://www.theregister.co.uk/2012/03/07/oracle_java_9_10_roadmap/

において Java の将来について QCon 2012 で語られた内容が載っていたので昼休みを使って訳してみますた。間違ってたらコメント下さい。

QCon 2012
Java 8 は 2013 半ばにリリース予定。Oracle は既に Java 9, 10 を準備中であり、Cobolesque のスライドレポートは非常に誇張されていると抗議した。

Java 9 と 10 は Big Data、複数言語間の相互運用、クラウドとモバイルに取り組んでおり 2015 と 2017 年それぞれにリリースする予定だと Oracle は水曜日に述べた。

Sun Microsystems で現在 Oracle Java プロダクトマネージャである Simon Ritter は Java を 2 年サイクルリリースに戻すと水曜日のロンドン QCon 2012 で発表した。

Java は 2006 年に Sun の元でリリースされた Java 6 まで 2 年ごとにリリースされていたが、政治的な行き詰まりのせいで次の Java 7 がリリースされたのは 2011 年となった。

Java 9 と 10 のアウトラインについて Ritter はマイルストーンに設定していないと強調していたが、Oracle 関係者はデータベースジャイアント - Java のお得意様 - はプラットフォームが進化し続けることを望んでおり、それは企業だけではなく消費者とデバイスもであると言った。Oracle は Java への開発者をより誘致したいと考えている。

Ritter は Java がある種の退屈な黄昏モードに陥り、言語やプラットフォームの興奮が別の場所へ移る懸念を否定した。

「Java は新しい COBOL ではない」と Ritter は言う。「我々は Java が新しい COBOL であると無意味に誘導するアナリストレポートを見たがそれは本当ではないと思っている。」

Oracle は現在構築されている Web 接続されたモバイルまたはサーバベースの超並列アプリを Java で構築することが適しているかの確認に取り組んでいると彼は述べた。

Java Development Kit (JDK) 10 以降に関して Java をオブジェクト指向にするための根本的な論議が行われている: プリミティブ型ではなく全てがオブジェクトとなるユニファイド型システムの導入があるようだ。JDK とは Oracle と IBM が先導する OpenJDK プロジェクトの成果をベースとした Oracle による公式開発キット。

「Java を真のオブジェクト指向言語にするにはより多くの論議が必要だ」と Ritter は OO への移行について示唆した。

パフォーマンス改善のため、JDK 9 のテーブルで Java Virtual Machine (JVM) をハイパーバイザ対応にするのと同様に、JDK 10 ではラージデータセットを扱うための 64bit アドレス指定可能な配列へ 32bit から移行する動きが取られている。

それらに先立って JDK 8 の到着は来年であり Web プログラミングとモバイルでのいくつかの大きな変更が見られるだろう。

JDK 8 では、JVM ライブラリを使用し既存の Rhino エンジンを置き換えるであろうプロジェクト NashornJavaScript エンジンを使用して Java 仮想マシン上での JavaScript パフォーマンスを搾り出すだろう。

JDK 8 では、デスクトップ用にビルドされた Standard Edition (Java SE) が、現在 Java Mobile Edition (Java ME) が動作しているいくつかのモバイルデバイスでも動くようになるだろう。Oracle はスマートフォンで使われるマルチコアチップや数百メガバイトのオンチップメモリの可用性は Java SE がデスクトップだけでなく携帯電話でも実行できると信じている。

これに対応するため Oracle は既に Java 8 がプロジェクト Jigsaw のモジュール的流儀でラップされるだろうとアナウンスしている。
このアイディアはクラスパスを必要としない Java アプリケーション/ライブラリがパッケージ可能で、プログラミングをよりシンプルでクリーンに行うことが出来る。Java プラットフォームはアプリケーションが、Java SE プロファイルでフルセットではなくかさばらない、必要なコンポーネントのみをインストールできるようモジュラー方式になるだろう。

これは興味深い動きだ。Oracle は Android 上での Google との法的な戦いで提訴しており、Android は一部を Apache Software Foundation の Harmony Java SE で実装している。OpenJDK は Java SE の実装だ。

「Java は 15 年前から出回っていた」と Ritter は QCon で語った。「我々はかつて 200 クラスファイルを宣言した。現在では 4,000 だ。もしあなたが非推奨メソッドを見るなら… いくつかのクラスは通常のメソッドより多くの非推奨メソッドを持つクラスもあるだろう。我々は Java がより大きく成長するためにそれらを捨てなければならない。」

「実行時のライブラリに誰も使用しない CORBA を誰が望んでいるだろうか? データベースアプリケーションをビルドするわけでもないのに何故ライブラリに JDBC を望むのだろうか? 我々は、モバイルデバイスの Java SE をターゲットとし、アプリの配置をより簡単にするためにそれらをモジュールへ分解する事を望んでいる。」

モジュール化された Java の推進は既に OSGi で行われている。OSGi バンドルが Java モジュールとして実行できるように Oracle は OSGi との互換性を模索していると Ritter は述べた。

「SE8 はかなり明確に定義され釘付けになる」と Ritter は述べた。「SE9 と SE10 - マイルストーンは何も置かれていない。」

SE9 と SE10 のアイディアは「我々は Java で行うことができ、Java で行う場合があるが、必ずしも Java で行うわけではない」事だと Ritter は語る。加えて、Java でのプログラミングをより困難にし、多くのエラーを引き起こすため「我々は Java に根本的な変更を加えたくない」とも。®

2012年02月29日

Log4j + syslog の設定

Log4j を syslog へ転送する時はファシリティと Syslog サーバが指定できる。log4j.xml の <appender> → <param> 指定は該当する Appender クラスのプロパティ名と同じなので、詳しくは API リファレンスの setter を参照。
SyslogAppender (Apache Log4j 1.2.17 API)

例えば

  • <param name="facility" value="local4"/> … ファシリティ local4 を使用
  • <param name="facilityPrinting" value="false"/> … ファシリティを出力しない
  • <param name="header" value="false"/> … タイムスタンプとホスト名を出力しない

など。

タグを指定するプロパティは存在しないが、これはケースバイケースで対応可能。

固定値で良い場合
PatternLayout 上で直接指定 ("[myapp] %m%n")
プロセスごとに変えたい場合
起動時のシステムプロパティで指定 ("[${syslog.tag}] %m%n"、起動オプションに -Dsyslog.tag=myapp 追加)
スレッドごとに変えたい場合
アプリケーションから NDC や MDC で指定 ("[%X{tag}] %m%n"、アプリ内でスレッド開始時に MDC.put("tag", "myapp"); 追加)
出力ごとに変えたい場合
アプリで出力するたびにフォーマット (logger.info("[" + myapp + "] ...") )

コードは通常の Log4j と変わらず。設定ファイルも概ね以下のようなイメージで構成できる。

import org.apache.log4j.*;
public class A{
  public static void main(String[] args){
    Logger logger = Logger.getLogger("org.koiroha.myapp");
    logger.info("hello, world");
    return;
  }
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  <appender name="syslog" class="org.apache.log4j.net.SyslogAppender">
    <param name="threshold" value="info"/>
    <param name="syslogHost" value="localhost"/>
    <param name="facility" value="user"/>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="conversionPattern" value="${org.koiroha.appname} %m%n"/>
    </layout>
  </appender>
  <root>
    <priority value ="info"/>
    <appender-ref ref="syslog"/>
  </root>
</log4j:configuration>

$ java -Dlog4j.configuration=log4j.xml -Dorg.koiroha.appname=sample1 A
2012-02-29 16:30:37,425  INFO myapp- [sample1] hello, world
$ java -Dlog4j.configuration=log4j.xml -Dorg.koiroha.appname=sample2 A
2012-02-29 16:30:41,633  INFO myapp- [sample2] hello, world

ただし 1024 バイト以上のメッセージとなる場合は 2 以上のパケットに分割されるため、2つめ以降にタグは付けられない。

2012年01月20日

OutOfMemory でスレッドの起動に失敗する件

十分なメモリを割り当てているにもかかわらず Hadoop や Thrift ゲートウェイで OutOfMemoryError が発生しスレッド生成に失敗する事案。

2012-01-18 19:54:16,620 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: DatanodeRegistration(192.168.121.93:50010, storageID=DS-1008921043-xxx.xxx.xxx.xxx-xxxxxx-xxxxxxxxxxxxxxx, infoPort=50075, ipcPort=50020):DataXceiverServer: Exiting due to:java.lang.OutOfMemoryError: unable to create new native thread
        at java.lang.Thread.start0(Native Method)
        at java.lang.Thread.start(Thread.java:640)
        at org.apache.hadoop.hdfs.server.datanode.DataXceiverServer.run(DataXceiverServer.java:131)
        at java.lang.Thread.run(Thread.java:662)

2012-01-18 19:54:16,620 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: PacketResponder 0 for block blk_5441514272352034349_78759 terminating
2012-01-18 19:54:16,620 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: Exiting DataXceiverServer
2012-01-18 19:54:16,623 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: PacketResponder 0 for block blk_4867391491560614348_78761 terminating
2012-01-18 19:54:16,624 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: Deleted block blk_6316283104492897854_78701 at file /var/lib/hadoop/dfs/data/sdb1/current/blk_6316283104492897854
…

GC 時に出力される空きメモリ量は余裕があるし、OS に割り当てているメモリ量も該当 Java VM のネイティブ使用量コミで十分収まる見積もりなのに何故? と悩んでいましたが、単に ulimit で設定している起動プロセス数の上限に達したというオチでした。まぁ Hadoop や Java VM に限った話ではなかったですが。

torao@clove$ ulimit -a
…
max user processes              (-u) 1024

Hadoop のデータノードで起きると自動的にシャットダウンしようとして七転八倒するし、Thrift でも通信不能の接続が残ってしまってプロセス再起動が必要と後始末が大変。

2012年01月15日

Java 非同期 I/O のデザインパターン (クライアント編)

New I/O の非同期処理を実装する時にいつも使うパターンの個人的設計まとめ。非同期 I/O はアプリ要件によって設計を柔軟に変える必要があるので定石というわけではありませんし、安全に組み替えるにはそれなりの知識が必要です。
この記事が説明のベースにしているサンプルソースは My Design Pattern for Asyncronous I/O に置いてあります。

非同期 I/O といえば 1 つのスレッドで複数のソケット I/O を管理する方法です。

非同期 I/O を生かした設計というものは必ず Producer/Consumer 型、イベント駆動型設計となります。これはパフォーマンスと引き換えにオブジェクト指向の汎用設計化を低下させ、特定の困難なバグを生む余地を増やします。個人的には C10K 問題を想定する必要がない程度 (同時に扱うソケット数が 1000 やそこいら程度) であればソケット数分のスレッドで同期 I/O を使用する方法をお勧めしております。

仮定要件と設計

この記事で仮定するクライアントの挙動は以下の通り:

  • 送信した 1 行分のデータをそのまま返信してくる echo サービスに対するクライアント。つまりデータ単位が「行」でその区切りは改行 (CRLF, CR, LF 兼用)。
  • クライアントの実装は 1 秒ごとに任意の文字列を送信し、それが返ってくることを期待する (任意のタイミングで送信データが発生する事を想定)。
  • あるデータに対する応答が返ってくるまで次の行の送信は行わない (パイプライン化を行わない)。
  • 1スレッドで複数のクライアント (通信端点; ソケット) を操作する。クライアントは任意のタイミングで参加が可能。
  • クライアントの処理中に例外が発生したらそのクライアントのみを切り捨てる。全体をダウンさせない。

f:id:kuromoyo:20120115101552p:image
各クラスの役割は以下の通り:

Client
送信データが発生した時の通知と、送信データの参照、データ受信時の通知をアプリケーション側で実装するための抽象クラス。インスタンス内部に SocketChannel と I/O 用のバッファを保持している。通信端点。
Worker
Selector を保持し送受信可能になった Client に対して送受信処理を行わせるためのスレッド。

Worker スレッド

Worker スレッドのインスタンス変数とコンストラクタ:

public class Worker extends Thread {
  private final Selector selector;
  private final List<Client> queue = new ArrayList<Client>();
  public Worker() throws IOException {
    selector = Selector.open();
  }
  …
}
クライアントの追加

実行中の Worker スレッドへ新しいクライアントを追加するにはキューを使用します。これは Selector#select() と SelectableChannel#register() が同じ Mutex を使用しているため、Worker スレッド内で select() 待機中に別スレッドから register() を行うと処理が停止する (設計によってはデッドロックとなる) ためです。このため、キュー経由でクライアントを渡し Worker スレッド内で register() を行うよう実装しています。

public void add(Client endpoint){
  synchronized(queue){
    queue.add(endpoint);
  }
  selector.wakeup();
}
select 処理

Worker スレッドが扱うソケットのいずれかが OP_READ/OP_WRITE の状態になったかを知るには Selector#select() を使用します。ただしそれ以外のも以下のケースで呼び出しが終了します (詳しくは API リファレンス参照):

  1. Selector#wakeup() が呼び出された (この設計では新しいクライアントがキューに投入された)。
  2. スレッドが割り込まれた (この設計では Worker スレッドに終了要求が出た)。

select() から処理が戻ってきた後に Worker スレッドの終了要求が出されていないかを調べています。次にキューに保存されているクライアントを全て Selector に登録しています。try ブロックで囲っていますが、この中で発生する IOException は Selector が予期しない状態に陥っているケースです。

private boolean select(){
  try {
    selector.select();
    if(this.isInterrupted()){
        return false;
      }
    synchronized(queue){
      while(! queue.isEmpty()){
        Client endpoint = queue.remove(0);
        SelectionKey key = endpoint.join(selector);
        key.attach(endpoint);
      }
    }
    return true;
  } catch(IOException ex){
    ex.printStackTrace();
    return false;
  }
}
送受信の実行
  1. 上記 select() メソッドが false で終了したら Worker スレッドの処理を終了。
  2. selector から通知が行えるようになった SelectionKey のセットを参照。
  3. Iterator を使用して列挙。列挙済みのものは it.remove() で通知対象から削除する (これを行わないと interestOpts() で通知フラグを下ろしても次回以降に残ってしまう仕様)。
  4. SelectionKey から処理対象の Client インスタンスを参照して read() または write() を実行。
  5. 例外が発生した場合はそのクライアントのみを close() して切り離し。

Client インスタンスがそれぞれバッファを持っているため、読み出しに使用する ByteBuffer はスレッドに対して一つあればよい。また 1024 というサイズにあまり意味はなく、一度に想定する受信データサイズを想定して 数バイト〜channel.socket().getReadBufferSize() バイトの範囲にあれば良いと思います。

@Override
public void run(){
  ByteBuffer readBuffer = ByteBuffer.allocate(1024);
  while(true){
    if(! select()){
      break;
    }
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> it = keys.iterator();
    while(it.hasNext()){
      SelectionKey key = it.next();
      it.remove();
      Client endpoint = (Client)key.attachment();
      try {
        if(key.isReadable()){
          endpoint.read(readBuffer);
        } else if(key.isWritable()){
          endpoint.write();
        }
      } catch(Exception ex){
        ex.printStackTrace();
        endpoint.close();
      }
    }
  }
}

Client クラス

インスタンス変数とコンストラクタは以下の通り:

public abstract class Client {
  private final SocketChannel channel;
  private ByteBuffer out = null;
  private final ByteArrayOutputStream inBuffer = new ByteArrayOutputStream();
  private SelectionKey key = null;
  private volatile boolean writeReady = false;

  public Client(String server, int port) throws IOException{
    this(new InetSocketAddress(server, port));
  }
  public Client(SocketAddress address) throws IOException{
    this(SocketChannel.open(address));
  }
  public Client(SocketChannel channel) throws IOException{
    this.channel = channel;
    channel.configureBlocking(false);
  }
  …
}
- in は Client ではなく Worker で良い。
  • 送信用 ByteBuffer は送信ごとにデータ長が変わるため都度作成しています。

  • writeReady のフラグはサブクラス (クライアント実装) が送信データの準備ができた時に ture となるフラグです。この変数のみ Worker スレッド以外から参照/変更されるため volatile 属性を付加しています。
クライアント実装部分

abstract 宣言されているものはサブクラスで実装するものです。
select() 待機中の selector に OP_XXX を変更した場合は wakeup() で呼び起こす必要があります。

public abstract void init();
public abstract String send();
public abstract void receive(String line);
protected void writeReady(boolean ready){
  writeReady = ready;
  if(ready){
    key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
    key.selector().wakeup();
  }
}
データの送信

データ送信部分を実装する上での注意点など。

  • 一度の SocketChannel#write() 呼び出しで ByteBuffer 内の全データが受け付けられるとは限らない。渡し切れなかった残りのデータを次回以降の送信可能時のために保持しておく必要があります。
  • write() の呼び出しが完了しても ByteBuffer 内の該当データが送信されている保証はありません。ただし送信内容は OS かソケットの送信バッファにコピーされているため、送信要求が完了した ByteBuffer の領域を即時書き換えたとしても直前の送信には影響はありません。
public void write() throws IOException {
  if(out == null){
    String data = send() + "\r\n";
    out = ByteBuffer.wrap(data.getBytes("UTF-8"));
  }
  channel.write(out);
  if(out.remaining() == 0){
    out = null;
    if(! writeReady){
      key.interestOps(SelectionKey.OP_READ);
    }
  }
}
  1. 送信バッファにデータが残っていない場合はサブクラスにデータを要求 (write() が呼び出された時点でクライアント側には送信データがある)。
  2. チャネルに対して非同期出力を実行。
  3. 内部バッファのデータを送信し終えたら参照を切り離す。この時、実装側がこれ以上送信データを持っていなければ OP_WRITE フラグを下ろす。
データの受信

データ受信部分を実装する上での注意点など:

  • 一度の読み込みでアプリケーションの定義するデータ単位を読み出せるとは限らない。逆に一度の読み出しで複数のデータ単位を読み出す可能性もある。このため ByteBuffer とは別の可変長バッファ (ByteArrayOutputStream) にデータ区切りを検出するまでのデータを保持する。
  • データ単位の区切り (例えば改行など) を検出した時点で可変長バッファを文字列化し実装側へコールバックする。
public void read() throws IOException{
  channel.read(in);
  in.flip();
  while(in.remaining() > 0){
    byte ch = in.get();
    if(ch != '\r' && ch != '\n'){
      inBuffer.write(ch & 0xFF);
    } else {
      if(ch == '\r' && in.remaining() > 0 && in.get() != '\n'){
        in.position(in.position() - 1);
      }
      String line = inBuffer.toString("UTF-8");
      inBuffer.reset();
      receive(line);
    }
  }
  in.clear();
}
  1. ByteBuffer へ読み出し。
  2. 読み込んだデータを 1 バイトごとに可変長バッファへ移動。この時、改行文字を検出したらその部分までを文字列化し実装側に通知する。

ちなみにこの実装だと CR と LF の間に読み込みの分割が入ったらバグになりますね…

切断の検知

サーバ側からの切断やネットワークによる切断は OP_READ の通知として呼び出されます。今回の設計では SocketChannel#read() からの例外発生時の挙動として処理しています。
切断時の処理が必要な場合は close() をオーバーライドする必要があります。

通知のタイミング

SelectionKey.OP_READ, SelectionKey.OP_WRITE を使用した書込可能と読込可能通知に注意する必要がある。
OP_READ フラグは相手がデータを送ってきた時のみ select() が働くため常に立てっぱなしという設計でも問題になる事はあまりない。しかし OP_WRITE に関しては殆どの時刻で書き込み可能であるため上げ下げを適時行わなければならない。

監視するソケットを追加

Selector#select() と SelectableChannel#register() は共に同じ Mutex を使用するため、スレッド A が select() 中にスレッド B で register() を行うとスレッド B が停止し設計によってはデッドロックを引き起こす。このため、別スレッドからのソケット追加はキューを使用しなければならない。

private final List<SocketChannel> queue = new ArrayList<SocketChannel>();
public void join(SocketChannel channel){
  synchronized(queue){
    queue.add(channel);
  }
  selector.wakeup();
  return;
}
int count = selector.select();
if(count == 0){
  synchronized(queue){
    if(queue.size() > 0){
      while
    } else {
      break;
    }
  }
}
クライアント実装

今回の設計でクライアント側で実装する処理は以下の通り:

  1. 1 秒ごとに送信可能な状態へ遷移するためにタイマーを設定
  2. 送信データが参照されるたびに送信可能フラグを下ろす。
  3. 受信したデータを標準出力へ表示。
class TimeProducer extends Client{
  private final Timer timer = new Timer();
  public TimeProducer() throws IOException{
    super("localhost", 8976);
    timer.scheduleAtFixedRate(new TimerTask(){
      @Override
      public void run(){
        writeReady(true);
      }
    }, 1000, 1000);
  }
  @Override
  public void init(){ }
  @Override
  public String send() {
    writeReady(false);
    return DateFormat.getDateTimeInstance().format(new Date());
  }
  @Override
  public void receive(String line) {
    System.out.println(line.length());
  }
}