「分散システムのためのメッセージ表現手法に関する研究」 - 筑波大学大学院を卒業しました
このたび筑波大学大学院を卒業し、修士号を取得しました。卒業にあっては本当に多くの方々にご助力いただきました。この場を借りて御礼申し上げます。ありがとうございました。
現在は起業して、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
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 というクラスを提供します。
このクラスは 動的に型付けられたオブジェクト を表現するクラスで、Ruby や Python のような動的型付け言語のオブジェクトと同じように扱うことができます。もし必要であれば、前述の型変換 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 クラスは非常に便利なことがお分かりいただけると思います。JRuby と Java の連携にも有用です。
また非常に面白いのは 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 Java は Javassist というライブラリに依存していますが、ライセンス上の問題があって使えないという話が発生していました。
このたび、ライセンスの問題はきれいに解決しました。なんと MessagePack のために、Javassist のライセンスを変えていただきました^^; Javassist-3.15.0.GA は、MPL、LGPL、Apache 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 にあり、JavaDoc は msgpack.org/javadoc/current にあります。
バグトラッカは jira.msgpack.org にあります。
MessagePack for Java 0.6 の実装は、私ふるはし(@frsyuki)と、西澤無我さん(@muga_nishizawa)によるものです。またAPIのレビューやテストや等々、色々な方々から数多くの協力を頂いています。ありがとうございます!
これからもよろしくお願いします^^;
イベントログ収集ツール fluent リリース!
こんにちは。Treasure Data の古橋です^^;
先日の Treasure Data, Inc. 壮行会 で、イベントログ収集ツール fluent をリリースしました!
fluent は syslogd のようなツールで、イベントログの転送や集約をするためのコンパクトなツールです。
ただ syslogd とは異なり、ログメッセージに テキストではなく JSON オブジェクト を使います。また プラグインアーキテクチャ を採用しており、ログの入力元や出力先を簡単に追加できます。
Twitterでも話題沸騰中です:イベントログ収集ツール #fluent 周りの最近の話題
背景
「ログの解析」は、Webサービスの品質向上のために非常に重要です。Apacheのアクセスログだけに限らず、アプリケーションからユーザの性別や年齢などの詳しい情報を集めることで、価値ある情報を数多く抽出することができます。
しかし、これらの イベントログをどうやって集めるか? という問題があります。ログを吐き出すアプリケーションは、複数のサーバに分散しています。それらのログを1箇所に集約してくる必要があります(とはいえ SPOF になってしまうのはイヤですね)
あるいは Amazon S3 や HDFS に書き出したくなります。その他にも Cassandra や MongoDB などに書き出したいケースもあります。ファイルに書き出すなら日付ごとにファイルを分けて欲しいし、当然圧縮もして欲しい。できればプラグインで拡張できると非常に嬉しいところです。
fluent は、これらの問題をシンプルに解決するイベントログ収集ツールです。
イベントログの転送とHA構成
fluent を使うと、↓このようにイベントログを転送したり、ルーティングすることができます。
転送先のサーバがダウンしていたら、バックアップサーバに切り替える機能もあります:
ちなみにラウンドロビンで負荷分散することもできます。これらの機能はすべてプラグインで拡張することができます:Fluent plugins
アーキテクチャ
fluent は全面的にプラグインアーキテクチャを採用しています。コア部分は小さいプログラムで、その他はすべてプラグインで成り立っています。
プラグインには次の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
MessagePack-Hadoop Integration (HBase勉強会)
Hbase勉強会(第二回)で発表したスライドを公開しました:
MessagePack+Hadoop (HBase-study 2011-06-16 Japan) - Scribd
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のインタフェースを定義します。この定義からクライアントやサーバのコードを生成します。
コメント
# これはコメント // これもコメント /* これもコメント /* ネストしても */ 良いではないか */
型の定義
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"
拡張仕様
採用されるかもしれないけど、とりあえず最初の実装では見送る仕様です。
デフォルト値
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
クライアント側のコード生成
クライアント側では、バージョンの選択に加えて、どの名前空間(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人のユーザーと開発者が集まり、実際のユースケースを元にしながら多くの問題が解決(への方針が決定)しました。
RPCのバージョニングサポート
背景
ソフトウェアはバージョンアップを繰り返すものですが、分散型のアプリケーションでは、単一のシステムの中で新旧のバージョンが混在して運用されることがあります。昨今のスケーラブルな分散システムでは、システム全体を停止せずに部分的にプログラムをバージョンアップさせていく ローリングアップデート と呼ばれる手法が利用されており、このようなケースでも 新旧のバージョンが互換性を保ったまま相互に通信可能である必要があります。
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の名前空間サポート
背景
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)ではクラスを動的にロードできないため、動かない(リフレクションベースの遅い実装にフォールバックする)という問題がありました。また、初回利用時に少し時間がかかるというデメリットもあります。
現在実装中です。
圧縮
長いテキストを扱う全文検索エンジンや、インターネット越しにメッセージのやりとりを行いたいシステムでは、データを圧縮することで性能を向上させることができます。
- 案1:MessagePackの仕様で対応
- 例:0x04 0x00 DEFLATE_STREAM ...
- メリット:アプリケーションは少ない変更で圧縮の効果を得られる
- デメリット:オブジェクトごとの圧縮になるので、圧縮率が低くなる
- デメリット:圧縮アルゴリズムを追加するたびに互換性は失われ、他の言語への移植が難しくなる
- 案2:RPCのプロトコルで対応
- 案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サーバにアップロードすれば完了です。
実際に、msgpack.org ではこの方法を使ってWebサイトを管理しています。
サーバスクリプト
まず、github から Post-Recive Hookを受け取って、同期スクリプトを起動する簡単なWebサーバを用意します。
POSTされるデータには、payload というパラメータ名でJSON形式のデータが含まれています。
Ruby の Sinatra を使って書くと↓こうなります。
#!/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を入力してください。
Service Hookの用途
Webサイトにコンテンツを同期する代わりに、push時にテストケースを走らせたり、新しいtagが作成されたタイミングでリリースビルドを作成するなど、発想次第で色々なことができそうです。
面倒な作業はどんどん自動化してみてください^^;