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

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のレビューやテストや等々、色々な方々から数多くの協力を頂いています。ありがとうございます!
これからもよろしくお願いします^^;

イベントログ収集ツール fluent リリース!

こんにちは。Treasure Data の古橋です^^;
先日の Treasure Data, Inc. 壮行会 で、イベントログ収集ツール fluent をリリースしました!

Fluent event collector


fluentsyslogd のようなツールで、イベントログの転送や集約をするためのコンパクトなツールです。
ただ syslogd とは異なり、ログメッセージに テキストではなく JSON オブジェクト を使います。また プラグインアーキテクチャ を採用しており、ログの入力元や出力先を簡単に追加できます。


Twitterでも話題沸騰中です:イベントログ収集ツール #fluent 周りの最近の話題

背景

「ログの解析」は、Webサービスの品質向上のために非常に重要です。Apacheのアクセスログだけに限らず、アプリケーションからユーザの性別や年齢などの詳しい情報を集めることで、価値ある情報を数多く抽出することができます。

しかし、これらの イベントログをどうやって集めるか? という問題があります。ログを吐き出すアプリケーションは、複数のサーバに分散しています。それらのログを1箇所に集約してくる必要があります(とはいえ SPOF になってしまうのはイヤですね)
あるいは Amazon S3 や HDFS に書き出したくなります。その他にも Cassandra や MongoDB などに書き出したいケースもあります。ファイルに書き出すなら日付ごとにファイルを分けて欲しいし、当然圧縮もして欲しい。できればプラグインで拡張できると非常に嬉しいところです。


fluent は、これらの問題をシンプルに解決するイベントログ収集ツールです。

イベントログの転送とHA構成

fluent を使うと、↓このようにイベントログを転送したり、ルーティングすることができます。

fluent routing
fluent forwarding

転送先のサーバがダウンしていたら、バックアップサーバに切り替える機能もあります:

fluent HA forwarding


ちなみにラウンドロビンで負荷分散することもできます。これらの機能はすべてプラグインで拡張することができます:Fluent plugins

アーキテクチャ

fluent は全面的にプラグインアーキテクチャを採用しています。コア部分は小さいプログラムで、その他はすべてプラグインで成り立っています。

fluent plugin architecture


プラグインには次の3種類があります:

Input plugin
アプリケーションや他のサーバからログの受け取ったり、様々なデータソースから定期的にログを取り寄せてきます。
Buffer plugin
ログをバッファリングし、スループットを向上させたり、信頼性を向上させる役割を担います。
Output plugin
ログをストレージに書き出したり、他のサーバに転送したりします。


Input plugin や Output plugin は、Rubyを使って簡単に書くことができます:Writing plugins

また、RubyGemsでプラグインを配布・インストールすることができます。↓このコマンドで、現在リリースされているプラグインの一覧を見られます:

$ gem search -rd fluent-plugin


まだリリースから数日しか経っていませんが、実は既に MongoDB プラグインや S3 プラグインが公開されています^^; 非常に簡単に書けるようになっているので、バシバシと拡張してみて下さい。

fluent のリポジトリは github にあります:http://github.com/fluent

構造化ロガー

多くのアプリケーションでは、人間が読むことを前提としたテキスト形式のログを書き出していることが多いと思いますが、今後はプログラムで処理しやすくするために、構造化されたログに移行していく必要があるでしょう。
テキスト形式のログをパースして構造化する*1という方法もまだまだ必要そうですが、アプリケーションが直接構造化されたログを書き出す方がずっとシンプルです。

そこで、各種のプログラミング言語向けに、構造化されたログを書き出せるライブラリが欲しくなります。標準的なテキスト形式のロガーを拡張する方法や、別に作って両方使う方法など、色々な実装手段がありそうです(腕の見せ所ですね)


というわけで、Ruby で fluent 向けの構造化ロガーを実装しました。fluent 向けと言っても、設定次第でファイルやsyslogに書き出すこともできます。

fluent/fluent-logger-ruby - GitHub


「こんなAPIの方が使いやすい」「それは俺が昔通った道」「PHP版作った」などのコメントをお待ちしています^^;

*1:[http://twitter.com/#!/doryokujin:title=@doryokujin]さんや[http://twitter.com/#!/tagomoris:title=@tagomoris]さんをフォローしていると、このパースする処理のメンテナンスがどれだけ大変かを窺い知ることができます…

MessagePack-Hadoop Integration (HBase勉強会)

Hbase勉強会(第二回)で発表したスライドを公開しました:

MessagePack+Hadoop (HBase-study 2011-06-16 Japan) - Scribd
http://www.scribd.com/doc/58144810/MessagePack-Hadoop-HBase-study-2011-06-16-Japan


MessagePackとHadoopを連携させるプロジェクトは、github の msgpack/msgpack-hadoop で進行中です。
HBase や Hive で、非構造化データを効率よく扱えるようにすることを目指しています。データはとりあえず突っ込んで、スキーマやクレンジングは後で考えたい(変更したい) というニーズにピッタリ合うハズです。


ログ収集ツールFluentも、オープンソースで公開するべく現在準備中です。


Fluent は、Facebook が開発したログ収集ツールである Scribe と似たツールです。Scribe がメッセージの表現として文字列しか使用できないのに対し、Fluent は JSON で表現可能なあらゆるオブジェクトを扱える点が大きく異なります(もちろん、内部では高速な MessagePack を使用しています)。

ログ収集ツールというよりは、汎用的なイベント収集ツールと言った方が良いのかもしれません。

プラグイン機構も異なります。Scribe はプラグインでデータの書き出し先を追加することができますが、Fluent ではデータの入り口も自由に追加することができます(Scribe では Thrift しか使えない上に、Thriftのインストールが面倒なのが難点で…)。


乞うご期待^^;

MessagePack IDL 仕様案

先日MessagePackハッカソンで議論した、MessagePack IDL の仕様についてまとめます。
実際のユースケースを元に、大規模な分散アプリケーションまでカバーできる実践的な仕様を目指しました。

基本的な文法

IDLは、大きく分けて 型の定義サービスの定義 に分かれます。

型の定義では、RPCでやりとりしたりログに保存したりするメッセージの構造を定義します。この構造の定義から、各言語のクラス定義や、シリアライズ・デシリアライズを行うコードを生成したりするのが、IDL処理系の役割の一つです。

サービスの定義では、RPCのインタフェースを定義します。この定義からクライアントやサーバのコードを生成します。

コメント
# これはコメント
// これもコメント
/* これもコメント /* ネストしても */ 良いではないか */
名前空間

名前空間は、Javaではパッケージ名、C++ではnamespace、Rubyではmoduleになります。
言語ごとに名前空間に関する文化が異なるので、言語ごとに定義を上書きできるようにしています。

namespace com.example    # 全言語向けの定義 区切りは.
namespace ruby Example   # 特定の言語で定義を上書き

型の定義

message

メッセージ型は↓このように定義します:

# 組み込み型
message BasicTypeExample {
    # <id>: <type> <name>
    1:  byte   f1
    2:  short  f2
    3:  int    f3
    4:  long   f4
    5:  ubyte  f5
    6:  ushort f6
    7:  uint   f7
    8:  ulong  f8
    9:  float  f9
    10: double f10
    11: bool   f11
    12: raw    f12
    13: string f13
}

# 総称型
message ContainerTypeExample {
    1: list<string> f1
    2: map<string,string> f2
    3: map<string, list<string,string>> f3  # 総称型はネスト可能
}

# optionalとrequired
message OptionalExample {
    1: string f1           # required non-nullable   # デフォルトはrequired
    2: required string f2  # required non-nullable
    3: optional string f3  # optional non-nullable
}

# ? を付けるとnullableになる
message NullableExample {
    1: string? f1           # required nullable   # デフォルトはrequired
    2: required string? f2  # required nullable
    3: optional string? f3  # optional nullable
}
exception

exceptionは例外を定義します。messageとほぼ同じですが、生成されるコードが変わってきます:

exception NotFound {
    1: string message
} # この例外の文字列表現は "NotFound"

# 継承ができる
exception KeyNotFound < NotFound {
    # 基底クラスの最大のID(この場合は1: string message)以下のIDは使えない
    2: raw key
} # この例外の文字列表現は "NotFound.KeyNotFound"
enum
enum EnumExample {
    0: RED
    1: GREEN
    2: BLUE
}

enumは、Javaではクラス名の中で定義される(EnumClass.FIELDとアクセスする)一方、Cでは外側の名前空間に定義されます(FIELDとアクセスする)。

MessagePack IDLではJava風の方が良さそうです。C++では以下のように生成することになりそうです:

struct EnumExample {
    enum type {
        RED   = 0,
        GREEN = 1,
        BLUE  = 2,
    };
};

拡張仕様

採用されるかもしれないけど、とりあえず最初の実装では見送る仕様です。

デフォルト値

messageのフィールドや関数の引数にデフォルト値を設定できると便利です。
パーサの実装は難しくないのですが、右辺値と左辺値の型があっているかどうか検査する実装が大変そうな気がします(そうでもないかも)。コンテナ型にデフォルト値を設定できるようにすると、mapやlistのリテラルが存在しない言語でコード生成が大変になります。
実装する場合でも、コンテナ型のデフォルト値は設定できないようにする案が有力です。

message DefaultValueExample {
    1: optional ulong flag = 1   # デフォルト値の指定
    2: optional raw? value       # nullableなフィールドのデフォルト値はnull
    3: optional string message   # nullableでないフィールドのデフォルト値は0,空文字列,falseなど
}
typedef

型に別名を付ける構文です。typedefはIDL処理系の中で完結し、実際のコードの中には現れないという案と、現れた方がいいという案があります(Javaではどうすれば良いのでしょう…)

typedef ulong NodeId
typedef map<string,string> PropertyMap
typespec

言語やアプリケーションごとに型をカスタマイズする構文です。
よく使う型には便利なメソッドが実装されているクラスを使いたいし、クラスを詰め直す処理も書きたくない場合など、欲しくなる機能です。

typespec cpp PropertyMap std::tr1::unordered_map<std::string,std::string>  # typedefに対するtypespec
typespec MyApp1 DefaultValueExample.value myapp::Value                     # フィールドのtypespec
typespec MyApp2 DefaultValueExample myqpp::MyClass                         # 型に対するtypespec

サービスの定義

RPCインタフェースは service で定義し、複数のserviceをまとめて application を宣言します。
service間では名前空間が分かれており、serviceが異なれば同名の関数を宣言できます。
serviceは複数のバージョンを宣言することができ、互換性を保ったまま新しいバージョンを追加ができます:

service StorageService:0 {  # <name>:<version>
    raw? get(1: raw key)
    void add(1: raw key, 2: raw value) throws DiskFullError
    ulong getDiskFreeSize()
}

service StorageService:1 {
    # 前のバージョンのメソッド一覧も全部書く
    raw? get(1: raw key)
    void add(1: raw key, 2: raw value)

    # 無くなったメソッドは書かない

    # 追加したメソッドも普通に書く
    map<raw,raw>? getAttributes(1: raw key)
    void setAttributes(1: raw key, 2: map<raw,raw> attrs) throws DiskFullError
}

service StatusService:0 {
    ulong getDiskFreeSize()
}

application MyApp {
    # <service>:<version> <scope> default?
    StorageService:1 storage default
    StatusService:0 status
}
サーバ側のコード生成

新しいバージョンの関数は、デフォルトでは古いバージョンの関数を呼んで欲しいが、古いバージョンと挙動が異なることもあります。
オブジェクト指向言語ではなかなか実現が難しそうですが、関数を関数オブジェクトとして扱えばスマートに解決できそうです:

// serviceは、関数の一覧をメンバ変数としてコード生成
class StorageService_0 {
    Function<raw? (raw)> get
    Function<void (raw, raw)> add
    Function<ulong ()> getDiskFreeSize
}

class StorageService_1 {
    Function<raw? (raw)> get
    Function<void (raw, raw)> add
    Function<map<raw,raw>? (raw)> getAttributes
    Function<void (raw, map<raw,raw>)> setAttributes
}

class StatusService_0 {
    Function<ulong ()> getDiskFreeSize
}

// applicationは、シングルトンのインスタンス
object MyApp {
    // 宣言されたserviceを、過去のバージョンを含めて保持
    StorageService_0 storage_0;
    StorageService_1 storage_1;
    StatusService status_0;

    // serviceの関数には初期値が設定されている:
    // 古いバージョンの関数が存在すれば、古いバージョンの関数を呼び出す関数
    storage_1.get = (raw key) => storage_0.get(key)
    storage_1.add = (raw key, raw value) => storage_0.add(key, value)

    // そうでなければ、例外を投げる関数
    storage_0.get = (raw key) => throw new org.msgpack.rpc.NotImpelmentedError
    storage_0.add = (raw key, raw value) => throw new org.msgpack.rpc.NotImpelmentedError
    storage_0.getDiskFreeSize = () => throw new org.msgpack.rpc.NotImpelmentedError
    storage_1.getAttributes = (raw key) => throw new org.msgpack.rpc.NotImpelmentedError
    storage_1.setAttributes = (raw key, map<raw,raw>) => throw new org.msgpack.rpc.NotImpelmentedError
    status_0.getDiskFreeSize = (raw key, raw value) => throw new org.msgpack.rpc.NotImpelmentedError
}


関数を実装する場合は、このように関数オブジェクトを代入します:

// 関数を代入して処理を実装
MyApp.storage_0.get = (raw key) => {
    return ...
}

// 新しいバージョンで挙動が変わった場合でも対応可能
MyApp.storage_1.get = (raw key) => {
    return ...
}

// 互換性を維持するためには、古い関数も実装しておく(実装を残しておく)
MyApp.storage_0.getDiskFreeSize = () => {
    return ...
}

MyApp.status_0.getDiskFreeSize = MyApp.storage_0.getDiskFreeSize

Scalaだと非常にきれいに実装できそうですが、C++OCamlでも何とかなりそう、です。

クライアント側のコード生成

クライアント側では、バージョンの選択に加えて、どの名前空間(scope)の関数を呼ぶか? という点が問題になってきます。

まず、アプリケーションではこのように使います:

// アプリケーションからはこのように使う:
MyApp remote = new MyApp(host, port)

StorageService_1 storage = remote.storage().version1()
storage.add("key", "value")    // add:storage:1 を呼ぶ
storage.get("key")             // get:storage:1 を呼ぶ

StatusService_0 stat = remote.status().version0()
stat.getDiskFreeSize()         // getDiskFreeSize:stat:0 を呼ぶ


これは↓このようなコードを生成すると実現できます:

// serviceは、RPCを行う関数を実装したクラス(直接インスタンス化はできない)
abstract class StorageService_0 {
    raw? get(raw key)
    void add(raw key, raw value) throws DiskFullError
    ulong getDiskFreeSize()
}

abstract class StorageService_1 {
    raw? get(raw key)
    void add(raw key, raw value)
    map<raw,raw> getAttributes(raw key)
    void setAttributes(raw key, map<raw,raw> attrs) throws DiskFullError
}

interface StorageService {
    StorageService_0 version0()  // バージョン0のRPCインタフェースを返す
    StorageService_1 version1()  // バージョン1のRPCインタフェースを返す
}

abstract class StatusService_0 {
    ulong getDiskFreeSize()
}

interface StatusService {
    StatusService_0 version0()  // バージョン0のRPCインタフェースを返す
}

// applicationはファクトリークラス
class MyApp {
    MyApp(host, port);

    StorageService storage()
    StatusService status()
}

拡張仕様

採用されるかもしれないけど、とりあえず最初の実装では見送る仕様です。

メタサービス

サービスの情報を取得するサービスがあると非常に便利です。例えば、サーバ側のバージョンを取得して処理を分岐したりできます。
applicationには暗黙的に組み込みのメタサービスが存在することにすると、きれいにまとまりそうです:

# メタ関数を提供する組み込みのサービスが定義されていることにする
service BuiltInService:0 {
    # 指定されたscopeの最新バージョンを返す
    int getVersion(1: string scope)
}

# applicationの定義では...
application MyApp {
    StorageService:1 storage default
    StatusService:0 status
    BuiltInService:0 _   # このscopeが暗黙的に存在することにする
}


サーバ側では、BuiltInServiceの実装はコード生成器が自動的に生成します。


クライアント側では、このような関数を提供します:

MyApp remote = new MyApp(host, port)

int v = remote.storage().getVersion()  // メタ関数の呼び出し
// この呼び出しと同じ: int v = remote._().version0().getVersion("storage")

// 接続先のバージョンによって処理を分岐するクライアントを書ける
switch(v) {
case 0:
    return remote.storage().version0()

defaut: /* 将来もversion 1がサポートされ続けることを期待 */
    return remote.storage().version1()
}


同様にして、serviceに定義されている関数の一覧を取得する機能も実現できそうです。Rubyで使いたくなりそうですね。

interface

RPCのクライアント側で、ある特定の関数群だけを使うメソッドを書くことは良くあります。
すべての関数を1つのserviceに記述していると、serviceはバージョンアップ時にクラス名が変わってしまうため、そのserviceを使っているすべてのコードを書き換える必要があります(静的型付け言語の場合)。
interfaceを導入すると、この問題を回避できます:

typedef map<raw,raw> Row

interface TableInterface:0 {
    void insert(1: Row row)
    Row? selectMatch(1: raw column, 2: raw data)
    Row? selectPrefixMatch(1: raw column, 2: raw prefix)
}

interface KeyValueInterface:1 {
    void set(1: raw key, 2: raw value)
    raw? get(1: raw key)
}

service StorageService:2 {
    implements TableInterface:0
    implements KeyValueInterface:1
    uint getNumEntries()
}

application MyApp {
    StorageService storage default
}

interfaceにもバージョン番号を付けられますが、実際にこのバージョン番号がネットワーク上を流れることはありません。コード生成されるクラス名だけに使われます。


サーバ側のコード生成は、単にserviceの中にinterfaceの関数群を取り込んだものを生成します。

# この定義と同じコードを生成:
service StorageService:2 {
    void insert(1: Row row)
    Row? selectMatch(1: raw column, 2: raw data)
    Row? selectPrefixMatch(1: raw column, 2: raw prefix)

    void set(1: raw key, 2: raw value)
    raw? get(1: raw key)

    uint getNumEntries()
}


クライアント側では、serviceをinterfaceにアップキャストして利用できるようにコード生成します。

interface TableInterface_0 {
    void insert(Row row)
    Row? selectMatch(raw column, raw data)
    Row? selectPrefixMatch(raw column, raw prefix)
}

interface KeyValueInterface_1 {
    void set(raw key, raw value)
    raw? get(raw key)
}

abstract class StorageService_2 implements TableInterface_0, KeyValueInterface_1 {
    void insert(Row row)
    Row? selectMatch(raw column, raw data)
    Row? selectPrefixMatch(raw column, raw prefix)

    void set(raw key, raw value)
    raw? get(raw key)

    uint getNumEntries()
}
scopeなしで使う

1つのapplicationを複数のserviceに分割して定義する機能は、複数のモジュールで構成される大規模なプログラムには必要になりますが、ちょっとしたプログラムを書きたいときには不便です。
scopeなしでも使えると、カジュアルなユースケースにも対応できそうです。


サーバ側では、serviceを定義と共に暗黙的に同名のapplicationが定義されると考えれば、シンプルに対応できそうです。

object StorageService {
    StorageService_0 _0   // scopeが無名
    StorageService_1 _1   // scopeが無名
    BuiltInService __0    // メタ関数サービス

    _1.get = (raw key) => _0.get(key)
    _1.add = (raw key, raw value) => _0.add(key, value)

    _0.get = (raw key) => throw new org.msgpack.rpc.NotImpelmentedError
    _0.add = (raw key, raw value) => throw new org.msgpack.rpc.NotImpelmentedError
    _0.getDiskFreeSize = () => throw new org.msgpack.rpc.NotImpelmentedError
    _1.getAttributes = (raw key) => throw new org.msgpack.rpc.NotImpelmentedError
    _1.setAttributes = (raw key, map<raw,raw>) => throw new org.msgpack.rpc.NotImpelmentedError
}

StorageService._0.get = (raw key) => {
    return ...
}

StorageService._1.get = (raw key) => {
    return ...
}

StorageService._0.getDiskFreeSize = () => {
    return ...
}


クライアント側では、serviceごとに生成するクラスにクラスメソッドを追加するだけで良さそうです。

abstract class StorageService_0 {
    static StorageService_0 open(host, port, scope="")
}

abstract class StorageService_1 {
    static StorageService_1 open(host, port, scope="")
}

第3回MessagePackハッカソン開催報告

4月3日、MessagePackハッカソン第3回を開催しました。
14人のユーザーと開発者が集まり、実際のユースケースを元にしながら多くの問題が解決(への方針が決定)しました。

http://preferred.jp/technology_component.html

RPCのバージョニングサポート

背景

http://togetter.com/li/83252


ソフトウェアはバージョンアップを繰り返すものですが、分散型のアプリケーションでは、単一のシステムの中で新旧のバージョンが混在して運用されることがあります。昨今のスケーラブルな分散システムでは、システム全体を停止せずに部分的にプログラムをバージョンアップさせていく ローリングアップデート と呼ばれる手法が利用されており、このようなケースでも 新旧のバージョンが互換性を保ったまま相互に通信可能である必要があります。


Thrift では、関数の引数やstructのメンバに optional という修飾子を付けることで、互換性を保ったまま引数やメンバの追加ができるようになっています。

しかし実際には、引数やメンバの追加だけではなく、もっと大きな変更を加えたいケースが多くあります。例えば、バージョン1で A という関数があったが、バージョン2で B と C の2つの関数に分離され、A はまだ呼び出せるが、未来のバージョンで削除される予定なので呼び出すべきではない、というケースがあります。他にも、引数や返り値の型を変更したい場合には、Thriftの方法では対処できません。

  • 異なるバージョンで同名の関数を呼び分けたい
  • ある機能を実現する一連の関数群があるとき、機能の単位でバージョンを管理したい
  • 古いバージョンが呼び出されたときは、警告を表示するようにしたい
プロトコル

MessagePack-RPCのリクエストメッセージは、[REQUEST, msgid, method, args] という4要素の配列です。methodは文字列で、関数名を表しています。

このmethodを、: のように、後ろにバージョン番号を付与できるように拡張します(プロトコルの拡張というよりは、既存のプロトコル仕様の上位に新たな標準を加える)

例: "get:0", "get:1"

互換性の維持のために(また使い勝手のために)、versionは省略可能とします。サーバはversionが省略されていたら、同名の関数の中で最新のバージョンを呼び出すことにします。

IDL

他の要求は IDL で解決します。これは後述します。

RPCの名前空間サポート

背景

http://togetter.com/li/83252

1つのサーバプログラムが複数のモジュールから構成されていることは良くあります。それぞれのモジュールを別々の人が設計・実装していることも多いでしょう。それらのモジュールで関数名が重複しないようにするのは、大変な作業です。

RPCの関数名に名前空間を導入することで、このような複雑なアプリケーションをシンプルに実装することができます。

プロトコル

前述のバージョンと同様に、methodを次のように拡張します:::
互換性の維持のために、scopeは省略可能とします。サーバはscopeが省略されていたら、デフォルトの名前空間を使うことにします。;


scopeとversionは、どちらも省略可能です。片方だけが省略されていた場合は、次のルールで識別します:
scopeの命名規則:/[a-zA-Z][a-zA-Z_]*/(先頭に数字は使えない)
versionの命名規則:/[0-9]+/(先頭は数字)

RPCのエラー処理

背景

マトモなアプリケーションでは、マトモなエラー処理は必須ですね。返り値でエラーを返すのは、実装もデバッグも大変です。
現在のプロトコルでエラーを返すには、[RESPONSE, msgid, error, result] というメッセージを返します。error と result は任意のオブジェクトです。
このプロトコルでエラーの種類や詳細を返すには、アプリケーション側で対処が必要です。この方法を標準化することで、相互互換性を高めることができます。


考慮すべきことは、エラーの種類はプログラムのバージョンアップ時に変化する可能性があるという点です。

例えば、エラーを細分化(特化)させたいケースがあります。例えば、NotFound というエラーを次のバージョンで KeyNotFound extends NotFound と BucketNotFound extends NotFound に細分化するようなケースです。

プロトコル

エラーを返すプロトコルを [RESPONSE, msgid, error_type, error_object] と拡張します(これもプロトコルの拡張というよりは、既存のプロトコル仕様の上位に新たな標準を加えている)


error_type はエラーの種類を表し、エラーの種類のグループ関係をドットで区切った文字列です。例:"NotFound.KeyNotFound"。
error_object はエラーの詳細を表し、通常は配列です。例:["key not found", "key1"]


クライアントは未知のエラーを受け取ったとき、より親のグループで処理することが可能です。例えば KeyNotFound を知らない古いクライアントが、新しいサーバから "NotFound.KeyNotFound" を受け取った場合でも、NotFound として扱うことができます。

組み込みのエラー

アプリケーション定義のエラーとは別に、MessagePack-RPCのライブラリ側で扱う組み込みのエラーが必要になります。
次のような区分が提案されています:

RPCError
|
+-- TimeoutError
|
+-- ClientError
|   |
|   +-- TransportError
|   |   |
|   |   +-- NetworkUnreachableError
|   |   |
|   |   +-- ConnectionRefusedError
|   |   |
|   |   +-- ConnectionTimeoutError
|   |
|   +-- MessageRefusedError
|   |   |
|   |   +-- MessageTooLargeError
|   |
|   +-- CallError
|       |
|       +-- NoMethodError
|       |
|       +-- ArgumentError
|
+-- ServerError
|   |
|   +-- ServerBusyError
|
+-- RemoteError
    |
    +-- RemoteRuntimeError
    |
    +-- (user-defined errors)

アプリケーションに近い型の扱い

Javaで、Dateクラスを扱いたいという提案がありました。

今後も様々な型を扱えるようにしたいという要求がどんどん出てくることが予想されます。アプリケーション層に近い型は、その要求の詳細は今後変化していくでしょう。

しかし、下層(MessagePackの型)に新たな型を追加すると、多言語対応が難しくなる、仕様が複雑になる、JSONと相互変換できなくなるなどの問題が発生します。具体的に言えば、BSONようになってしまうという意味です。"Function" や "MD5" という型がプリミティブとして定義されています。Min key という謎の型*1もあります。SHA1は? 多言語対応はできるのでしょうか?


しかし、"対応している言語で" 合意された標準的な方法があれば、相互互換性が高まることは確かです。


そこで、アプリケーションの型をMessagePackの型と対応付けるガイドラインを用意していくことで対応します。
実装としては、MessagePackの型に新たな型を追加する代わりに、MessagePackの型と言語の型を変換するところに新しいコードを追加します。


日付型

7つの案が出ました:

  • 基本:epocからの経過時刻を保存する。タイムゾーンUTCに固定する
  • 案1:経過秒を整数で保存
  • 案2:経過ミリ秒を整数で保存
  • 案3:経過マイクロ秒を整数で保存
  • 案4:経過を浮動小数点数で保存(精度は秒またはミリ秒またはマイクロ秒)
  • 案5:(経過秒, マイクロ秒)を2要素の配列で保存
  • 案6:(整数, 精度)を2要素の配列で保存
  • 案7:実装しない(アプリケーションごとに対応する)


ユースケースとしては、「人間」が入力した時刻を記録するにはミリ秒程度の精度があれば十分だが、プログラムが扱うタイムスタンプにはマイクロ秒以上の精度が欲しくなります。
決定的な案が出なかったので、とりあえずJava版では案2の「ミリ秒を整数で保存する」方法でDateクラスのシリアライズを実装してみることになりました。

decimal型

Java版で、BigDecimalクラスを扱いたいという提案がありました。ユースケースとしては、お金の計算に使います。

  • 案1:文字列で保存
  • 案2:Binary Coded Decimal (BCD) で保存する
  • 案3:Densely Packed Decimal (DPD) で保存する(DPDはIEEE 754-2008で標準化されている)
  • 案4:実装しない(アプリケーションごとに対応する)


とりあえずJava版で案1と案2(余裕があれば案3?)を実装してみることになりました。

難しいですねぇ…。

Java版:Templateプリコンパイラ

Java版では、ユーザー定義クラスのシリアライズ/デシリアライズを行う Template を、実行時にコード生成してコンパイル・ロードする機能が実装されています。

しかし、Android(DalvikVM)ではクラスを動的にロードできないため、動かない(リフレクションベースの遅い実装にフォールバックする)という問題がありました。また、初回利用時に少し時間がかかるというデメリットもあります。

現在実装中です。

Scala版、OCaml版、Go版

それぞれリポジトリに追加されました!

圧縮

長いテキストを扱う全文検索エンジンや、インターネット越しにメッセージのやりとりを行いたいシステムでは、データを圧縮することで性能を向上させることができます。

  • 案1:MessagePackの仕様で対応
    • 例:0x04 0x00 DEFLATE_STREAM ...
    • メリット:アプリケーションは少ない変更で圧縮の効果を得られる
    • デメリット:オブジェクトごとの圧縮になるので、圧縮率が低くなる
    • デメリット:圧縮アルゴリズムを追加するたびに互換性は失われ、他の言語への移植が難しくなる
  • 案2:RPCのプロトコルで対応
    • 例:[START_COMPRESSION, DEFLATE] deflate([REQUEST, msgid, method, args]) ...
    • メリット:圧縮を使う前にネゴシエーションが可能。相手がサポートしている圧縮アルゴリズムだけを使える
    • デメリット:ネゴシエーションの実装は比較的大変な部類に入る
    • デメリット:ネゴシエーションは実装すると遅くなる
    • デメリット:オブジェクトごとの圧縮になるので、圧縮率が低くなる
  • 案3:RPCのトランスポート層にプラグインする
    • 例:圧縮TCPトランスポート
    • メリット:ストリーム全体で圧縮するので、圧縮率が高くなる
    • メリット:トランスポート層のプラグイン機構を使えるため、既存のライブラリは変更せずに済む
    • デメリット:サーバとクライアントで同じ圧縮アルゴリズムを使うように、人間が設定する必要がある
      • ただしエラーの表示は可能(TransportError)
  • 案4:実装しない(アプリケーションごとに対応する)


案3の方針が有力ですが、トランスポート層の仕様までは決まらず、現状ではプラグインの機構に従ってアプリケーションで対応しつつ、良い実装が現れれば標準仕様に取り込むことになりそうです。

プロジェクト

JIRA(チケット管理)とConfluence(WIki)を導入しました。
JIRAはチケットのネストができます。日付型が欲しい→Java版で日付型を実装 というように、言語をまたいだ共通の提案→各言語の実装 という要求にぴったり合います。


それぞれ以下のURLにあります:

IDL

大いに議論が盛り上がりました。次のエントリで詳しくまとめます。

*1:MongoDBで使うようです

Webサイトをgithubで管理してpush時に自動的に同期する方法

Webサーバに Subversion のサーバを立てておき、HTML や CSS を commit することでWebサイトを更新する方法は、良く知られているテクニック、らしいですね*1。更新の履歴を残すことができるし、ましてチマチマとFTPやsftpでアップロードするよりずっと簡単です。


しかし SVN の代わりに git を使おうとすると、pushしてもリポートリポジトリではファイルを更新してくれません。
また、リポジトリはWebサーバ上に作るよりも、便利な管理インタフェースがある github(や噂のgitosis)に置いておきたいところです。


そこで、github の Post-Receive Hook を使うと、リポジトリに変更を push すると同時に、Webサーバにも同期させることができます*2

Webサーバに同期する前に、Sphinxでドキュメントを整形したり、SassをCSSに変換したり、あるいはJavaのソースからJavaDocを生成したりと、色々な前処理を走らせることもできます。


流れは下図のようになります。Post-Recieve Hook を設定しておくと、リポジトリに変更が push されたときに指定しておいたURLに POST リクエストを送信してくれます。この POST リクエストを受け取ったタイミングで github から変更を pull し、色々な前処理を実行した後に、Webサーバにアップロードすれば完了です。


http://help.github.com/post-receive-hooks/


実際に、msgpack.org ではこの方法を使ってWebサイトを管理しています。

サーバスクリプト

まず、github から Post-Recive Hookを受け取って、同期スクリプトを起動する簡単なWebサーバを用意します。
POSTされるデータには、payload というパラメータ名でJSON形式のデータが含まれています。

RubySinatra を使って書くと↓こうなります。

#!/usr/bin/env ruby
require 'sinatra'
require 'json'

here = File.dirname(__FILE__)
SYNC_SCRIPT = "#{here}/update-website.sh"

post '/' do
   begin
      push = JSON.parse(params[:payload])
      system(SYNC_SCRIPT)
      "ok."
   rescue
      "error."
   end
end

同期スクリプト

次に、githubから変更をpullしてWebサーバに同期するスクリプトを用意します。
同期する方法は、同期先のWebサーバで使える方法を選択してください。ここでは ssh + rsync を使ってみます:

  • update-website.sh
#!/bin/sh
tmpdir=/home/viver/gitsync/work/msgpack-website
repo=git://github.com/msgpack/website.git
rsyncto=viver@example.com:htdocs/
rsync='rsync -e "ssh -i ~/.ssh/id_rsa_nopass" -vrtl --delete'

if [ -d "$tmpdir" ];then
    cd "$tmpdir"
    if git pull; then
        $rsync ./ "$rsyncto"
        exit 0
    fi
fi

dirname=`dirname "$tmpdir"`
basename=`basename "$tmpdir"`

mkdir -p "$dirname"
cd "$dirname"
rm -rf "$basename"

if git clone "$repo" "$basename"; then
    cd "$basename"
    $rsync ./ "$rsyncto"
fi

githubの設定

最後に、githubで Post-Receive Hook を発行するURLを設定します。
リポジトリのトップページで [スパナアイコン] Admin ボタンをクリックし、サイドバーから Service Hook を選んで、Post-Receive URLs にサーバスクリプトへのURLを入力してください。


http://help.github.com/post-receive-hooks/

Service Hookの用途

Webサイトにコンテンツを同期する代わりに、push時にテストケースを走らせたり、新しいtagが作成されたタイミングでリリースビルドを作成するなど、発想次第で色々なことができそうです。

面倒な作業はどんどん自動化してみてください^^;

*1:自分ではやったことがない^^;

*2:gitosisや生のgitリポジトリでも、post-receiveスクリプトを使って同じことが(もっと簡単に)できるはずです。