msgpack javaについて

色々あってmsgpackいじってたりするのですが、とりあえず見つけたバグとかその他色々まとめてみる。


最近1、2ヶ月でいじりだした程度で、べつにmsgpackのエキスパートではないので、なにか間違ってたらツッコミください。

そもそもmsgpack javaの話だけがしたかったわけではなく、それを元にScalaのやつ新しくつくったり、もしくはJavaに依存させずに別途pure Scalaで作ってパフォーマンス比較したかったけど、Javaのやつがバグってるままだと話にならないので、まずJavaのやつを理解して安定させたい、ということで、こうやってblog書いたり、pull reqやissue報告してる状況です。


msgpack自体の説明はあまりしないので、知らない人はググってください。簡単に言っておくと、だいたいJsonっぽい*1けど、バイナリフォーマットなので、Jsonに比べてパースも速い、かつコンパクトになる、という利点があります。fluentdとかその他色々使われてるらしいです。


msgpackのjavaについては最近作者の人が発表してたのでこちらもどうぞ

Treasure Dataを支える技術 - MessagePack編


まず前提として、これ書いてる2014-12-28現在、msgpackのjavaのライブラリには、0.6と0.7と2つ系統があります。0.6系は最新が0.6.11で、0.7系は0.7.0-p2というのが最新です。
両者の違いをまとめると

  • 0.6
    • それなりに安定
    • (0.7にくらべて)設計が古いというか、0.7は作り直してるので綺麗
    • msgpackは1度だけ?仕様の拡張があったが、その拡張に完全に対応してない
    • パフォーマンスのために?javassist使用
  • 0.7
    • 0.6と大体同じ機能はあるが、コードベースは多分ゼロから作りなおしてる
    • sun.misc.Unsafeを使用
    • 現状、依存ライブラリはゼロ
    • 0.6に比べて速いらしい
    • 拡張された新しい仕様にも対応
    • もう実際にtreasuredata内で0.7使ってるらしい
    • コア部分は安定してるらしい?が、それ以外の部分ではまだバグがありそう

という感じだと思います。

msgpack-scalaもありますが、Javaの0.6系に依存してるやつがあるだけです。
ぶっちゃけ、現状のmsgpack-scala

  • Javaの0.6系に依存してる
  • scalapという黒魔術つかってる
  • リフレクションだらけで、全然型安全じゃないし、とてもコード読みにくい(型クラス使うようにしたい)

と色々とダメな点があると思ってて、全く使いたくないわけです。

というわけで、速いらしいし、Javaの0.7系直接使うか、それをラップして、Scalaのやつ作るかしたいわけですが、色々issue見つけたりしたので、つらつらと説明を書いてみる。

まずは、一部togetterにまとめたので貼っておきます

msgpack-javaのバグらしきものを見つけた話

とりあえず自分が報告した以下の2つは、すでに解決済みなので、そこまで詳しい解説はしません。

https://github.com/msgpack/msgpack-java/issues/154
https://github.com/msgpack/msgpack-java/issues/157

おおまかにいうと、(このあとにでてくるやつも一部そうだけど)両方ByteBuffer関連ですかね?
こういうコードでByteBuffer使うのはまぁ普通だと思うんですが、普段自分自身もそれほど使わないし、なおかつ、型だけみても

  • 使われてる箇所で、readonlyなのかどうかわからない
  • 受け渡すときに、コピーしたり、readonlyにして受け渡すべきなのかわからない
  • positionがどういう状態になっているべきなのか、変更してはいけないのか、逆に処理した分だけpositionを変更しないといけないのか?などがわからない

という感じで、注意深く全体のソースコード読まないとわからない、バグがあっても直しずらいのでつらいですね。もしくは、単に自分がScalaなimmutableな世界に慣れ過ぎただけで、この程度は普通のJavaプログラミングだったら当たり前だったのだろうか・・・


さて、あとは自分が報告済み(かつこれ書いてる時点では未解決)なissueが3つあるので、解説書いておきます。


RawStringValueImpl#equals is wrong?

RawStringValueをシリアライズしてデシリアライズしたら、もちろん同じものなはずなので、equalsで比較したらtrueになるべきですが、false返ってきました。これもByteBuffer関連ですねたぶん。
シリアライズするときに、内部のByteBufferのposition変わっちゃうみたいです。"なんとかValue" という名前のクラス達は、全部immutableなはずなので、position変わったらおかしいはずなので、変わらないようにしてpull reqしました
https://github.com/msgpack/msgpack-java/pull/163

繰り返しになるけど、ByteBuffer使われてると、どこ直せばいいのか、事前条件事後条件がどうなっていればいいのかわからない(コメントにも書いてない?)のでつらい・・・。pull reqしたはいいけど、これがベストな直し方だったのか自信ない
案の定、直すべき箇所はここではなかったようなので?一旦閉じました


StringValueImpl and RawStringValueImpl does not satisfy equals/hashCode laws

msgpackには、Stringとして扱えるようなものが2種類あります。そもそもサイズによっても内部実装的には2種類どころではなく何種類もありますが、そのあたりは面倒なので解説しません。
とにかく、equalsがtrueになるのに、hashCodeが同じにならないという、Javaの基本的なあれです。これ、msgpack0.6では新しい仕様に対応してないので、この問題発生しない?だろうけど、他の言語のmsgpackライブラリって、そもそもこの2種類を等価になるものとして扱ってるんでしょうか?誰か教えてください。

現状equalsがtrue返してるので、それを変えないなら、hashCodeを修正するべきもののはずです。


MapValue#equals too slow

これ報告した後に気が付きましたけど、0.6系でも同じ実装なんですね・・・。
ある意味仕様なのだろうか・・・。なぜ遅いのか?というと、内部をArrayで持つようなMapを独自に作ってるからです。メリットとデメリットまとめると

  • 内部をArrayで保持することのメリット
    • 順番を保持したままにすることが可能
    • 少ない数なら、パフォーマンス問題にならない
    • シリアライズするときの、メモリ消費や余計な処理が走らなくて、その際のコストは多分最小
  • 内部をArrayで保持することのデメリット
    • equalsとかその他getなどが遅くなる
    • equalsが遅くなるとは、nの2乗か指数オーダーなレベルで遅くなる?ので、それ知らずにサイズ大きいMapで使ってしまうと、実用的にかなり問題出るはず
    • getもO(n)
    • その他似たように遅くなるメソッドあるかも

選択肢としてArrayで保持するMapというのはありだともおもいますが、「Arrayをベースにしたデータ構造で保持するんだよー、パフォーマンス特性はこうなってるよー」というのがドキュメントやコメントに書いてないのが問題?な気がします。
内部Arrayで保持するものがあっても、それとはべつに簡単にHashMapのような普通のMapに変換できるメソッドなどがあったほうがよい気がします。(もし既にあって見落としてたらごめなさい)

このあたりも、他の言語のmsgpackのライブラリではこうしてるよー、というのがあったら教えてください。

というわけで、とりあえずissueの解説はこんな感じで。



ちなみにこれらのissueほぼ全部scalacheckが見つけたので、みんな(Javaのプロジェクトだとしても?)Scalacheckのようなライブラリでテスト書きましょう。


上記の問題がだいたい直ったら、scalacheckのテストをもっと追加してpull reqしようと思ってる(現状だと通らないので)

*1:完全互換とかではない