TL;DR
- 要素数が分からず、メモリに乗り切らない可能性がある配列をシリアライズする必要があったので、フォーマットとして CBOR を採用した。
- Protocol Buffers1 や Ion, MessagePack, BSON などの主要なバイナリフォーマットで配列を扱う場合、要素数またはバイト長を先頭などに記述する必要があるため、メモリに乗り切らず要素数が簡単には分からない配列を(単純には)シリアライズすることができない
- CBORは要素数不明の配列を表現する手段がある
- Java/Kotlinの場合、jackson の jackson-dataformats-binaryを利用してストリームパーサ等を実装することにより、データすべてをメモリに乗せずにシリアライズ・デシリアライズすることができる
- jackson-dataformats-binary ではシリアライザ等の実装を JSON と共通化しているため、 CBOR のシリアライザ・デシリアライザを実装すると、JSON 向け実装が自動的に出来上がっていて便利
- 巨大データの永続化やデータのストリーミングなどには、シリアライズフォーマットとして CBOR が使えることを知っておくと役立つかも
始めに
担当しているプロジェクトが多忙になってしまい、この1年半ほど本テックブログをまったく更新できていませんでした。
アドベントカレンダーの時期2ですし、前回記事を書いてから1年経ってしまいましたし、そろそろ何か記事を書かないとという気になってきたので、先日お仕事で遭遇したちょっと興味深い話を記事にします。
前提
現在書いているプロダクトで、メモリに乗り切る想定であったデータがメモリに乗り切らない状況が発生したため、メモリ上のデータを急遽ディスク上に退避させる必要が生じました。そのため、速やかにデータのシリアライズフォーマットを決定し、シリアライザ・デシリアライザを実装する必要に迫られました。
実装すべき機能やシリアライズすべきデータの特徴として、以下の点が挙げられました。
- 1要素あたり数KB程度のデータがネットワーク越しに逐次的に渡されて来るので、それを加工して次の処理に投げるようなよくある処理
- 諸事情により、受け取ったデータを一旦すべてバッファする必要がある
- 受け取るデータは全体として数十GBを超える大きさになり、メモリに乗り切らない恐れがある
- データ受け取り順を保存し、受け渡し時に再現しなければならない
- SQSのようなマネージドなキューイングサービスは利用できない
- 実装言語は Kotlin/JVM
選定の経緯
今回はライブラリとしてjacksonを使用し、データフォーマットにCBORを使用することに決定しましたのですが、その際に考慮した主な点が以下の通りです。
- 実装速度や今後の拡張性、メンテナンスコスト等を考えると、独自フォーマットの策定は当然なし
- データは人間が読む必要はない上、スループットやファイルサイズに多少気を配らなければならない状況であるため、バイナリフォーマットのほうが望ましい
- 文字列が多くを占めるデータをシリアライズするため、バイナリフォーマットの採用によるデータサイズの削減効果は正直期待していませんでしたが、実装後の測定で、CBOR でシリアライズしたデータは JSON の場合に比べてデータサイズを 20% から 30% 程度減らすことができていることを確認しました。
- Protocol BuffersやIon, MessagePack, BSON等のメジャーなバイナリフォーマットは、どれもデータの中に配列長やペイロード長を記述しなければならないため、ストリーム処理に向かない
- CBORというデータフォーマットは、不定長の配列をストリームミングするための表現形式がある
- Kotlinの準標準的な扱いのシリアライザライブラリである kotlinx-serialization では、 CBORはまだ Experimental 扱い
- jackson-dataformats-binary ならば、
JsonParser
やJsonGenerator
を利用することで CBOR をストリーム処理することができる
最後に
CBORは Wikipedia 日本語版に記事がないなど、日本国内では他と比べてまだマイナーである印象を拭えないフォーマットです。しかしながら、データをストリーミングしたり、巨大データをシリアライズしたりするようなケースでは、他のフォーマットに比べて有利になる場合がある、という事を今回の案件を通じて学ぶことができました。
-
厳密には、Protocol Buffersで配列に当たる
repeated
要素自体は、packed
形式にしない限りバイナリ中に要素数を含まないのですが、repeated
要素は必ずmessage
に内包されなければならず(i.e. JSONのように配列をルートにすることができない)、message
のバイナリエンコーディングは必ずペイロード長を持たなければならないため、結果的に配列のサイズをデータの先頭に書かなければならないことになります(そもそもProtocol Bufdfersは、1MBを超えるデータのシリアライズに用いるべきではないと公式に明言されています)。 ↩ -
弊社もアドベントカレンダーをやっておりますが(宣伝)、今年はエントリー時期に多忙を極めており参加する機会を逃してしまいました……。 ↩