リトライと冪等性のデザインパターン

リトライを肴に一晩酒が飲める古橋です。

大規模なデータに触れることが日常茶飯事になっている今日この頃。この分野のおもしろいところは、いつまで経っても終わらないプログラムを簡単に作れてしまうことかもしれません。エラー処理リトライそして冪等性*1の3つを抑えていないプログラムは、小規模なデータなら問題ないが、データ量が多くなると使い物にならなくなる可能性が大です。

大規模データをバッチ処理するケース以外でも、リトライは一般にプログラムの信頼性に関わる重要な問題です。

そんなわけで、リトライに関わるいくつかのデザインパターンを、連載でまとめておこうと思います*2
では、第1回は背景から:

なぜリトライが必要なのか

プログラムは色々な理由で失敗する。例えば、

  • A) 通信先のプログラムが高負荷すぎて応答できなかった
  • B) メモリを消費しすぎてメモリ確保に失敗した。またはOOM KIllerに殺された
  • C) 通信先のサーバがハードウェア的に落ちていた
  • D) データの転送中にネットワークエラーが起きた
  • E) 読み込んだデータが壊れていた
  • F) 設定が間違っていた
  • G) プログラムがバグっていた

など。一部の問題はプログラミングや運用の努力で未然に回避できる可能性がある。A)はキャパシティプランニングでミスっているか監視が足りていないし、B)はメモリ管理の実装が甘い。
とは言え、正直なところそこまで完璧に実装していられないし、ハードウェア故障や想定できなかった突発的な高負荷など、どうしようも無いことも多い*3
堅牢なプログラムを手早く作ろうとすると、リトライでカバーするのが妥当なケースは数多い。

リトライと冪等性

しかし、不用意にリトライすると問題が起きる。具体的には、こんなことが起こりうる:

  1. クライアントが要求を発行した。
  2. サーバはリクエストを正常に受け付けたが、高負荷過ぎてすぐには処理できなかった。
  3. クライアントがリトライした。
  4. 同じリクエストが2度実行されてしまった。

例えば、新しいアイテムを作り、そのIDを返すという要求を不用意にリトライすると、サーバの負荷やネットワークエラーなどの状況によって、同じアイテムが1度に2つ作成されてしまったりする可能性がある。これではマズい。

そこで、リトライを実装する場合には処理を冪等にすることが重要になる。すなわち、同じ要求を複数回行っても結果が同じになるようにする。

例えば、新しいアイテムを作る処理は、ID=xyzで識別される新しいアイテムを作成する という処理に変更できないだろうか? これなら、同じ要求を何回繰り返しても同じアイテムが2つ作成されてしまうことは無い。

冪等にするやり方はアプリケーションによって変わってくる。トランザクションの粒度を考えるのと同じように、それなりに大きな処理をまとめないと冪等にできないことも多い。そこでここでは、リトライと冪等性に関するいくつかのパターンをまとめてみる。

パターン1:IDを付けてCREATEを冪等にする

CRUDのC(Create)を冪等にするには、リソースに一意な名前を付ければいい。前節で挙げた新しいアイテムを作成する処理は、IDが既に存在したらエラーを返すようにサーバを実装しておけば、クライアントは安全にリトライできる。

クライアント側のエラー処理は少し難しい。なぜなら、クライアントが「同じIDが既に存在する(ので新しいリソースを作成できない)」というエラーを受け取ったとき、以前から同じIDが存在していたのが原因なのか、リトライした結果として重複してしまったのかを区別することができない。これはケースバイケースで対処するしか無い。無視しても良いし、警告をレポートしても良いが、決定性なエラーなのでリトライしてはいけない

言い換えれば、Create系のAPIは、HTTPの409 Conflictに相当するようなエラーコードを定義しておき、クライアントはそれを他のエラーとは区別して扱う必要がある。クライアント側の実装は後回しにしてもいいが、もし何か新しいAPIセットを設計することがあれば、エラーコードや例外クラスにConflictを含めておいた方がいい。

パターン2:エラーを区別してDELETEを冪等にする

次回 続・リトライと冪等性のデザインパターン - リトライはいつ成功するか に続く。

*1:読み方は『ベキトウ』。『冪』は常用漢字では無いので、『べき等』という書き方もよくされる。

*2:連載本数は未定

*3:そして客は必ず想定外のことをやるという事実。色々な意味で予想を超えてくるのがお客さんというものであるようです。

デシリアライズ速度の比較 ByteBuffer vs DirectBuffer vs Unsafe vs C

OpenJDK や Hotspot VM には sun.misc.Unsafe という内部APIがあり*1、これを使うと ByteBuffer.getInt や ByteBuffer.getLong よりも高速にバイト列から整数値をデコードできるという。これを駆使することで、Cで実装された拡張ライブラリに匹敵する速度を出せるらしい。

それが本当なら、データ圧縮ハッシュ関数、シリアライザ/デシリアライザなどの実装を高速化できる。例えば、lz4xxhashJava実装が Unsafe API を使用している*2jpountz/lz4-java
Prestoも、中間データのシリアライズ/デシリアライズにはすべて Unsafe API を使っている*3

そこで、実際にベンチマークしてみた。

ベンチマーク内容

  • 10MBのランダムなバイト列を生成する
  • 先頭から1バイト読み出す
  • その1バイトの先頭ビットが1なら:32bitの整数をbig endianで読み出す
  • その1バイトの先頭ビットが0なら:64bitの整数をbig endianで読み出す
  • データの末尾に達するまでループする

結果

f:id:viver:20140312003334p:plain

  • ByteBuffer heap: new byte[…]で確保したメモリを、ByteBuffer.wrapして整数値をデコード
  • ByteBuffer direct: ByteBuffer.allocateDirectで確保したメモリから、整数値をデコード
  • Unsafe heap: new byte[…]で確保したメモリから、Unsafe APIを使って整数値をデコード
  • Unsafe direct: ByteBuffer.allocateDirectで確保したメモリから、Unsafe APIを使って整数値をデコード
  • C shift: ビットシフトを使って、バイト列から整数値をデコード
  • C load: memcpyとエンディアン変換関数(bswap_64、__DARWIN_OSSwapInt64、_byteswap_uint64など)を使って、バイト列から整数値をデコード

確かにUnsafeを使うとCに匹敵する性能が出る。これは予想以上に速い。
一方でヒープメモリからのByteBuffer.getIntは、かなり遅いことが分かる。Unsafe APIではヒープメモリでもdirect bufferでも差はないので、ヒープメモリからのByteBufferをUnsafeに変更すれば、2倍近く性能が向上することになる。

Cにはstrict-aliasingルールがあるので、バイト列から整数値をデコードするコードは複雑になる。コンパイラが賢いので冗長に見える割には速度が出るのだが、ポータブルで安全なコードを書くのは結構難しい。strict-aliasingルールについて詳しくは:

実行環境


===
後日追記:あわせて読みたい何故JVM(HotSpot)のUnsafe APIは速いのか

*1:Dalvikにあるかどうかは調査していない。誰か教えてください。

*2:ちなみにlz4とxxhashの開発者は同一人物。

*3:Prestoが依存している airlift/slice ライブラリがUnsafe APIをラップしたクラスを実装している。airliftを開発しているのはPrestoと同じメンバー。

Presto 0.54を1台のサーバで起動する

Presto | Distributed SQL Query Engine for Big Data v0.54 を1台のサーバ上で動かす手順です。

Mac OS X でも動きます(と言うよりMac OS Xを想定しています)
JDK 7 が必要になるので、事前にインストールしておいてください。Oracle JDKMac OS X版)を使っていますが、OpenJDKでも動くはずです。

もし読むのが面倒なら、これを実行すればOKです:https://gist.github.com/frsyuki/8001572

githubからソースコードをcloneする

git clone https://github.com/facebook/presto.git
cd presto
git checkout v0.54

JDKのバージョンを7にする

export JAVA_HOME="$(/usr/libexec/java_home -v 1.7.0)"

presto-serverをビルドする

version="$(xpath pom.xml "/project/version/text()")"
pushd presto-server
mvn package assembly:assembly -DdescriptorId=bin -Dtest=skip -DfailIfNoTests=false
popd

他のコンポーネントを改変したい場合は、presto-server/ディレクトリではなくルートディレクトリでビルド(assembly:assembly)する必要があります。

これで presto-server/target/presto-server-0.54-SNAPSHOT.tar.gz ファイルができあがるので、presto-deploy/ ディレクトリに展開しておきます:

rm -rf presto-deploy
mkdir -p presto-deploy
pushd presto-deploy
tar zxvf "../presto-server/target/presto-server-$version.tar.gz"

設定ファイルを書いたりする

最終的には、presto-discovery, presto-coordinator, presto-worker の3つのプロセスを動かします。今回は、それぞれにディレクトリを作ることにします。

それぞれのディレクトリには etc/ ディレクトリを作り、設定ファイルを置いておく必要があります。ここにサンプルの設定を固めた物を置いておきました:
config.tar.gz

これを展開、presto-coordinator, presto-worker ディレクトリを作成します:

wget https://gist.github.com/frsyuki/8001572/raw/c4524795d31a23f37365b0ad850c08899614ab98/config.tar.gz
tar zxvf config.tar.gz
cp -a presto-server-$version/* presto-coordinator/
cp -a presto-server-$version/* presto-worker/
rm -rf presto-server-$version

discovery-server を配備する

Prestoを起動するために必要なコンポーネントである discovery-server は、Prestoとは完全に疎結合化されており、別プロジェクトからリリースされています。これをダウンロード & 展開します:

curl "http://search.maven.org/remotecontent?filepath=io/airlift/discovery/discovery-server/1.16/discovery-server-1.16.tar.gz" | tar zxvf -
mv discovery-server-*/* discovery-server/
rm -rf discovery-server-*

起動する

3つのプロセスを起動します:

cd discovery-server
./bin/launcher run
cd presto-coordinator
./bin/launcher run
cd presto-worker
./bin/launcher run

クライアントを起動する

これで走ります:

java -jar presto-cli-executable.jar --server http://127.0.0.1:8880/ --catalog native --schema default

SELECT 1 などが動きます。

Next step

サンプルの設定では jmx コネクタと native コネクタを有効化してあります。
HDFSからデータを読み出すには hive-hadoop1 または hive-cdh4 コネクタを有効化する必要があります。



Running Presto v0.54 on a single server

This is a procedure to run Presto | Distributed SQL Query Engine for Big Data v0.54 on a single server.

Presto runs on Mac OS X (rather I assume Mac OS X).
You need to install JDK 7 in advance. I used Oracle JDK (for Mac OS X) but OpenJDK should also work well.

If you don't have time to read this short article, you can simplly run this script: https://gist.github.com/frsyuki/8001572

Clone source code from Github

git clone https://github.com/facebook/presto.git
cd presto
git checkout v0.54

Set default java version to JDK 7

export JAVA_HOME="$(/usr/libexec/java_home -v 1.7.0)"

Build presto-server

version="$(xpath pom.xml "/project/version/text()")"
pushd presto-server
mvn package assembly:assembly -DdescriptorId=bin -Dtest=skip -DfailIfNoTests=false
popd

If you modify other components, you need to build (run assembly:assembly) on the root directory of presto instead of presto-server directory.

OK, I got presto-server/target/presto-server-0.54-SNAPSHOT.tar.gz file. Extract it to presto-deploy/ directory:

rm -rf presto-deploy
mkdir -p presto-deploy
pushd presto-deploy
tar zxvf "../presto-server/target/presto-server-$version.tar.gz"

Writing configuration files

Presto needs at least 3 processes: presto-discovery, presto-coordinator, and presto-worker. Here creates one directory for each processes.

Each directory needs ./etc/ directory that contains some config files. I uploaded tar.gz of the files here: config.tar.gz

Extract it and create presto-coordinator and presto-worker directories:

wget https://gist.github.com/frsyuki/8001572/raw/c4524795d31a23f37365b0ad850c08899614ab98/config.tar.gz
tar zxvf config.tar.gz
cp -a presto-server-$version/* presto-coordinator/
cp -a presto-server-$version/* presto-worker/
rm -rf presto-server-$version

Deploy discovery-server

Presto depends on a process called discovery-server but it's completely separated from Presto itself. Another OSS project releases it. Download it & extract:

curl "http://search.maven.org/remotecontent?filepath=io/airlift/discovery/discovery-server/1.16/discovery-server-1.16.tar.gz" | tar zxvf -
mv discovery-server-*/* discovery-server/
rm -rf discovery-server-*

Launch

I ran 3 processes:

./discovery-server/bin/launcher run
./presto-coordinator/bin/launcher run
./presto-worker/bin/launcher run

Run client

java -jar presto-cli-executable.jar --server http://127.0.0.1:8880/ --catalog native --schema default

That's it. You can run SQL like SELECT 1.

Next step

The sample configuration I uploaded enables jmx connector and native connector.
You need to enable hive-hadoop1 or hive-cdh4 connector to read data from HDFS.

Fluentd Casual Talks #3

Fluentd Casual Talks #3 でしゃべってきました。会場提供していただいた DeNA さん、主催の@tagomorisさんありがとうございましたm(_ _)m

Ustreamで録画されているので、見逃した人はここで見られます:

v11 preview1リリース

fluentd v11 perview1 をリリースしました。
会場でもデモしましたが、インストール方法は:

gem install fluentd -v v0.11.0.preview1

です。まだαリリースな状態ですが、とりあえず起動すると何が起こるのか分かる状況にはなっています。
コードはこちら:https://github.com/fluent/fluentd/tree/v11

データの更新履歴をRDBMSからfluentdに流すfluent-plugin-sql

Fluentd Advent Calendar 9日目。担当の古橋です。
Fluentd v11の情報は Fluentd Casual Talks #3 at :D でお話しすることにして、今回はFluentdの大幅な性能向上を可能にするMultiprocessプラグインを紹介…しようと思っていたら@niku4i さんに先を越されてしまったので!今回はSQL inputプラグインを紹介します。

SQL inputプラグインとは?

SQL inputプラグインは、SELECT文を定期的に実行することで、RDBMSから最近更新されたレコード最近追加されたレコードを定期的に取り出してFluentdに流すことができるプラグインです。内部では"前回読み出したレコード"を記憶しており、前回読み出したタイミングより後になって更新/追加されたレコードを定期的に読み出します。

SQL input plugin for Fluentd event collector - github

何に使える?

例えば、ユーザー情報を保存するテーブルがあったとします。アプリケーションはレコードを更新するたびにupdated_atカラムを更新することにします。ここでSQL inputプラグインを使えば、ユーザー情報の更新ログをFluentdに流し込むことができます。もちろんその後、HDFSに書き込んだりKibana+ElasticSearchに保存して検索可能にしたりできますね:

f:id:viver:20131209110556p:plain

設定

先述の例の場合、次のような設定ファイルになります:

<source>
  type sql

  # RDBMSの設定
  adapter mysql2
  host localhost
  database app_test
  username root
  password xyz

  # SELECTを実行する間隔
  select_interval 6s

  # 1回のSELECTで読み出すレコード数
  select_limit 500

  # 最後に読み込んだレコードの保存先
  state_file /var/run/fluentd/sql_state

  # 対象のテーブル
  <table>
    table users
    tag users.updated

    # 更新判定に使うカラム
    update_column updated_at

    # ログの時刻に使うカラム
    time_column updated_at
  </table>
</source>

update_columnパラメータに更新判定に使うカラムを指定してください。ここにAUTO INCREMENTなプライマリキーなどを指定すると、最近更新されたレコードの代わりに、最近追加されたレコードを読み出すことができます。

対応しているRDBMS

実装にはActiveRecordを使っているので、ActiveRecordが対応しているRDBMSなら使えます。
fluent-plugin-sql gemはデフォルトでMySQL(mysql2)とPostgreSQLのドライバに依存しているので、これらのRDBMSであれば追加でgemをインストールせずに使えます。

インデックス必須!

更新判定に使うカラム(update_column)には、インデックスが設定されていないと、毎回フルテーブルスキャンが走ることになるので注意しましょう。

次は、@k1LoW さんです!:しまった!!いきなり「Apacheのアクセスログを提出して欲しい」と言われたら? (Fluentd Advent Calendar 2013 Day10)

「分散システムのためのメッセージ表現手法に関する研究」 - 筑波大学大学院を卒業しました

http://cdn-ak.f.st-hatena.com/images/fotolife/v/viver/20120401/20120401160645_original.jpg

このたび筑波大学大学院を卒業し、修士号を取得しました。卒業にあっては本当に多くの方々にご助力いただきました。この場を借りて御礼申し上げます。ありがとうございました。
現在は起業して、12月からアメリカに在住しています。新たな価値を生み出すべく "下から上まで" システムの設計と開発に携わっており、エキサイティングな毎日を送っています。

修論シーズンに日本にいなかったので、修士論文はメールで送って提出し、卒業式にも出席していないというありさまなので、本当に卒業できたのかどうか実感がないのですが、友人によれば「学位記はあった」らしいので、きっと大丈夫でしょう。(写真はカリフォルニア州マウンテンビューにて)


さて、せっかく時間を割いて書いたので、修士論文を公開することにしました。
分散システムのためのメッセージ表現手法に関する研究と題して、バイナリ形式のシリアライズ形式である MessagePack について述べています。特にこの文章では、MessagePack の型変換モデルについて詳しく述べています。実は MessagePack は、空間効率を高めたフォーマットだけでなく、各言語の型システムを相互に変換する機構をその設計思想に含んでいるのですが、文章にしたのはこの修士論文が初めてかもしれません。

目次

第1章 はじめに ... 1

第2章 関連研究 ... 3
 2.1 XDR
 2.2 SunRPC
 2.3 JSON
 2.4 Thrift
 2.5 ProtocolBuffers
 2.6 Avro
 2.7 JavaRMI
 2.8 CORBA
 2.9 既存のメッセージ交換手法の課題

第3章 MessagePack のモデルと設計 ... 8
 3.1 メッセージ交換手法の課題
 3.2 MessagePackの型変換モデル
 3.3 型システムの設計
 3.4 表現形式の設計
   3.4.1 型情報の格納
   3.4.2 データサイズの削減

第4章 プログラミング言語バインディング ... 13
 4.1 動的型付けオブジェクトAPI
 4.2 型変換テンプレート
   4.2.1 型変換テンプレートの拡張
   4.2.2 実装の難易度と実装の段階的な拡張

第5章 MessagePack の実装 ... 19
 5.1 実装上の課題
   5.1.1 ストリームの取り扱い
   5.1.2 コピーの削減
   5.1.3 メモリ確保の最適化
   5.1.4 リフレクションの削減
 5.2 Ruby実装の最適化
   5.2.1 ストリームデシリアライザ
   5.2.2 コピーの削減
 5.3 Java実装の最適化
   5.3.1 ストリームデシリアライザ
   5.3.2 コピーの削減
   5.3.3 直接型変換によるメモリ確保の最適化
   5.3.4 動的コード生成によるリフレクションの削減
 5.4 C,C++実装の最適化
   5.4.1 メモリゾーンによるメモリ確保の最適化

第6章 評価 ... 28
 6.1 機能評価
   6.1.1 異種の言語をまたいだメッセージの交換
   6.1.2 移植性
   6.1.3 動的型付けオブジェクトAPI
   6.1.4 型変換テンプレート
 6.2 性能評価
   6.2.1 C,C++実装の性能評価
      メモリゾーンの効果
   6.2.2 Ruby実装の性能評価
   6.2.3 Java実装の性能評価
 6.3 アプリケーションの開発
   6.3.1 kumofs
   6.3.2 LS4
   6.3.3 Fluentd

第7章 おわりに ... 37

付録 MessagePack の表現形式 ... 42
 A.1 Nil
 A.2 Boolean
 A.3 Integer
   A.3.1 PositiveFixnum
   A.3.2 NegativeFixnum
   A.3.3 uint8、uint16、uint32、uint64
   A.3.4 int8、int16、int32、int64
 A.4 Float
   A.4.1 float
   A.4.2 double
 A.5 Raw
   A.5.1 FixRaw
   A.5.2 raw16、raw32
 A.6 Array
   A.6.1 FixArray
   A.6.2 array16、array32
 A.7 Map
   A.7.1 FixMap
   A.7.2 map16、map32


分散システムのためのメッセージ表現手法に関する研究 [PDF]

MessagePack for Java ついに 0.6 リリース!

先日の fluent に引き続き、新しいソフトウェアのリリースです。
満を持して、MessagePack for Java 0.6 をリリースしました! 9ヶ月ぶりのメジャーバージョンアップです。


以前のバージョン 0.5 の API をすべて見直し、互換性を維持しながらも、数多くの機能を新たに搭載しました。動的オブジェクトAPIリフレクション機能の強化JRubyとの連携JSONサポート などなど。もちろん、性能も大きく向上しています。

このバージョン 0.6 のリリースによって、MessagePack の応用範囲は大きく広がります。MessagePackは、クラウドシステムからモバイルデバイスデバイスまで、多種多様なシステムの連携と統合をサポートする、新しいデータ表現形式です。


さて、新機能の詳細をご覧下さい:

JSONシリアライザ・デシリアライザを統合

MessagePack の型システムは JSON と互換性があり、言い換えれば、MessagePack は「速いJSON」「コンパクトなJSON」として利用することができます。既にJSONを利用しているシステムや、手軽にJSONを使いたいが性能が心配だというケースでは、MessagePackを利用することで高速化を図ることができます。

とは言え、「MessagePack に完全に依存するのはちょっと厳しいので、JSONも同時に利用したい」というケースは多いと思います。


新しい MessagePack for Java 0.6 では、JSONを扱うことができます。JSONとMessagePackを両方サポートし、性能が重要な場面では MessagePack を利用するという使い方が可能です。どちらも同じ API で透過的に扱うことができ、ファクトリメソッドを置き換えるだけで切り替えられます。


これは同時に、後述する 型変換機能や動的型付けオブジェクト、アノテーション機能 などの MessagePack の機能を、JSON でも利用できるということを意味します。実は JSON しか使わないというケースであっても、0.6 は非常に有用です。

Maven Central リポジトリ

Maven Central リポジトリからを MessagePack をダウンロードできるようになりました!
QuickStart for Java にあるように依存関係を記述すれば、自動的にインストールされます。

動的オブジェクトAPI

MessagePack for Java 0.6 では、新たに Value というクラスを提供します。
このクラスは 動的に型付けられたオブジェクト を表現するクラスで、RubyPython のような動的型付け言語のオブジェクトと同じように扱うことができます。もし必要であれば、前述の型変換 API も適用して 静的な型に変換することも可能 です。


本質的には、デシリアライズされたオブジェクトは静的には型が決定しておらず、デシリアライズされたタイミングで(=実行時に=動的に)型が決定します。それらの型を扱うプログラムを書くことで、ファイルフォーマットを完全にコントロールできるようになります。

Value クラスは↓こんな感じで利用できます:

MessagePack msgpack = new MessagePack();
Unpacker unpacker = msgpack.createStreamUnpacker(System.in);
for(Value v : unpacker) {
    if(v.isArrayValue()) { // 配列だったら...
        ArrayValue av = v.asArrayValue();
        List<Value> list = av;  // ArrayValue は List<Value> を implements している!
        Value e = list.get(0);
        ...
    }  else if(v.isIntegerValue()) {    // 整数だったら...
        IntegerValue iv = v.asIntegerValue();
        Number n = iv;    // IntegerValue は Number を implements している!
        short s = n.shortValue();
        ...
    }
    System.out.println(v);  // 値をダンプ
}

このプログラムで使用しているように、各 Value クラスは List や Number などのインタフェースを implements しています。使い込むほどに、Value クラスは非常に便利なことがお分かりいただけると思います。JRubyJava の連携にも有用です。


また非常に面白いのは Value の toString() はJSONを返す という点です。↓このようにデシリアライズしたオブジェクトを簡単にダンプして表示することができます:

Value v = ValueFactory.createArrayValue(
    new Value[] {
      ValueFactory.createIntegerValue(1),
      ValueFactory.createRawValue("ok?"),
    });

System.out.println(v);  //=> [1, "ok?"]

コードをデバッグをしていると、とりあえず値を表示させたいことがよくありますが、Valueクラスであれば System.out.println に渡すだけで値を表示できます。


さらに 0.6 では、 Value クラスは equals() と hashCode() を真面目に実装しています。
この挙動は MessagePack の型システムの意味論に基づいており、Ruby などの動的型付け言語との相互運用性を意識した挙動になっています。例えば、Java のShortとLongは同じ値でも hashCode が異なりますが、MessagePack ではどちらも 整数型 であり、同じ値であれば同じ hashCode を返します。

充実したテストケース

後述する Packer/Unpacker クラスや Value クラスを中心に、新しい機能が増えました。すると心配になるのは、その実装の品質ですね。
MessagePack for Java の開発では、品質向上に大きく注力しています。バージョン 0.6 は前バージョンのテストケースをすべてパスし、さらに大量のテストケースを追加しています。

ソースコードmsgpack/msgpack-java にあります。

Apache License 2.0

MessagePack for JavaJavassist というライブラリに依存していますが、ライセンス上の問題があって使えないという話が発生していました。
このたび、ライセンスの問題はきれいに解決しました。なんと MessagePack のために、Javassist のライセンスを変えていただきました^^; Javassist-3.15.0.GA は、MPL、LGPLApache License のトリプルライセンスでリリースされます。MessagePack for Java は、依存ライブラリを含めて Apache License の元で利用可能です。

型変換

MessagePack は、「自己記述的」なデータ形式です。シリアライズされたデータにオブジェクトの型情報も併せて埋め込むため、インタフェース定義ファイル(IDL)を別途用意する必要がありません。これは Protocol Buffers や Thrift とは異なり、MessagePack の大きな利点となっています。

ただし、受け取ったデータが正しい型であるかどうかチェックしたり、Listなどの静的な型に変換する仕組みは、依然として必要です。

MessagePack for Java は、この型変換の実装を強力にサポートする仕組みを備えています。0.6 では API を整理し、より使いやすくなっています。


まずシリアライズするには、単に Packerクラス の write メソッドにオブジェクトを渡して下さい:

import org.msgpack.MessagePack;
import org.msgpack.packer.BufferPacker;
public class Main {
    public static void main(String[] args) throws IOException {
        List<String> target = new ArrayList<String>();
        target.add("some");
        target.add("elements");

        // BufferPackerを作成
        MessagePack msgpack = new MessagePack();
        BufferPacker pk = msgpack.createBufferPacker();

        // シリアライズ
        pk.write(target);
        byte[] data = pk.toByteArray();
    }
}

↓こう書いてもOK:

import org.msgpack.MessagePack;
import org.msgpack.packer.BufferPacker;
public class Main {
    public static void main(String[] args) throws IOException {
        List<String> target = new ArrayList<String>();
        target.add("some");
        target.add("elements");

        // ちなみに MessagePack msgpack = new JSON();
        // と書くとシリアライズ結果がJSONになります
        MessagePack msgpack = new MessagePack();
        byte[] data = msgpack.write(target);
    }
}


このデータをデシリアライズするには、UnpackerクラスTemplatesクラス を使用します。↓このように "テンプレート" を渡します:

import org.msgpack.MessagePack;
import org.msgpack.unpacker.BufferUnpacker;
import static org.msgpack.template.Templates.tList;
import static org.msgpack.template.Templates.TString;
public class Main {
    public static void main(String[] args) throws IOException {
        // BufferUnpackerを作成
        MessagePack msgpack = new MessagePack();
        BufferUnpacker u = msgpack.createBufferUnpacker(data);

        // デシリアライズ
        List<String> deserialized = u.read(tList(TString));
    }
}


このように Templates を使って総称型にデシリアライズすることができます。

使いやすくなったアノテーション:Modelの実装を強力にサポート

JavaのコードでIDLを記述することができます。
@Optional@NotNullable@Ignore アノテーションをフィールドに付加することで、シリアライズ方法を制御することができます。


例えば、次のようなクラスを定義します:

public class User {
    public String name;
    public int age;
}

このUserクラスをシリアライズ可能にするには、単に @Message を付ければOKです:

import org.msgpack.annotation.Message;
@Message
public class User {
    public String name;
    public int age;
}


このクラスは 名前 と 年齢 のフィールドしか持っていませんが、新たに 性別 のフィールドを 後から追加しなければならない という話は、非常に良くあることです。このときに、既存のシステムやデータと 互換性を保ったまま 拡張しなければならないというケースもまた、良くあることです。
こうなると、途端に実装は複雑になってきます。この処理を自分で実装することを考えると…嫌気が差してきますね。まったくエキサイティングでない上に、バグが入りやすいコードです。


MessagePack では、そのあたりを自動的にうまく処理します。次のように単に String gender フィールドを追加して下さい:

import org.msgpack.annotation.Message;
@Message
public class User {
    public String name;
    public int age;
    public String gender;
}

もしフィールドが追加されていない古いデータを受け取ったときは、gender フィールドには null が入ります。
この挙動を許さずに、null が入らないようにチェックを自動的に行うには、↓このように @NotNullable アノテーションを追加してください:

import org.msgpack.annotation.Message;
import org.msgpack.annotation.NotNullable;
@Message
public class User {
    @NotNullable public String name;
    public int age;
    public String gender;
}

プリミティブ型はデフォルトで @NotNullable です。省略を許すには明示的に @Optional を指定するか、参照型を使って下さい。

その他の改善

テンプレートプリコンパイラ
実行時に Javassist を使ってシリアライザを生成・ロードする代わりに、実行前にコンパイルしておくことが可能。Android で MessagePack を使いたい場合に有効。
@Beansアノテーション
JavaBeans仕様を満たしたクラスをシリアライズ・デシリアライズ可能
バッファリングの最適化
バイト列へのシリアライズする際のバッファの管理方法を改善。シリアライズ速度が大幅に高速化。
ByteBuffer
ByteBufferからのデシリアライズをサポート。MappedByteBuffer から高速にデシリアライズが可能に。イベント駆動アプリケーションとの親和性も向上。
InputStreamとの統合
ストリームデシリアライザの内部バッファを廃止し、InputStream を直接利用するように変更。これにより MessagePack とそれ以外のデータ(ヘッダやメタデータなど)が入り交じったファイルフォーマットを扱うことが可能。
逆型変換
静的なオブジェクトから動的型付けオブジェクト(Value)への逆型変換をサポート。

リポジトリとドキュメント

MessagePack for Javaリポジトリは、msgpack/msgpack-java に移りました。
ドキュメントは MessagePack Wiki にあり、JavaDocmsgpack.org/javadoc/current にあります。
バグトラッカは jira.msgpack.org にあります。


MessagePack for Java 0.6 の実装は、私ふるはし(@frsyuki)と、西澤無我さん(@muga_nishizawa)によるものです。またAPIのレビューやテストや等々、色々な方々から数多くの協力を頂いています。ありがとうございます!
これからもよろしくお願いします^^;