protobuf の Marshal/Unmarshal や Any 型について確認していた
あるメッセージの送り元と送り先でスキーマを共有してそのスキーマに基づいたメッセージをやり取りしたいことが多々あると思います。 そんな際にスキーマの記述を protobuf で行い、かつメッセージをシリアライズして送る構想について調査したメモを本稿で記載します。
背景
IoT だとか Microservices Architecture だとかウェブ・アプリサービスのユーザ行動分析だとか、AWS,GCP だとか何とか、世の中には多くのデータを生成するソースやデータ格納ストレージが生まれてきています。 そんな中、どのようなスキーマ・フォーマットでデータを送信するかという点は未だ多くの開発者を悩ませているのではと考えます。
これがビジネス上スケジュールの要件が厳しかったり、プロダクトの変化が大きかったり、データ送信ロジックを書く開発者が蓄積したデータを活用するシーンでサポートできるのであれば、スキーマレスデータベースにひとまずデータを格納しまくるのも良いかと思います。 しかしながら個人的にはそうではない状況や、将来的な組織・人員の拡充を考えるとスキーマがあって欲しい気持ちが高まります。 とはいえ、なるべくスキーマの変更には柔軟に対応できるようにあって欲しいものです。
スキーマ定義をどうするかについて最初に思いついたのは JSON Schema
なのですが、人が読み書きするのには適さないと感じます。
その次に本稿で触れる、最近は gRPC メソッド定義などでもよく用いられる protobuf によるスキーマ定義を思いつきました。
(実際にいくつかのサンプルに触れる前に、以下の記事に影響を受けました。 protobuf にこれから入門してみたい方は読んでみていただけると!)
今さらProtocol Buffersと、手に馴染む道具の話
確認したかったこと
protobuf によるスキーマ定義自体は上記の記事より良さげだなと思っていたのですが、スキーマの更新の柔軟性、具体的には以下の点が気になっていました。
- メッセージのフィールドを増やした際の受信側の挙動
- タグの値が書き換わるような更新が起こった際の受信側の挙動
- Any による型情報付きメッセージの受信側の挙動
これらを以下のシナリオのもと動作確認してみることにします。
- Twitter みたいなサービスのユーザの行動ログを protobuf で定義する
- 最初は v1 として
Event
型で行動ログのスキーマを定義する - ある程度時期が経過した際、ツイートの URI のような追加情報を足したくなったので
RichEvent
という新しい型でログを扱うことにした - 更に時期が経過した際、根本的に構造を見直したくなり、新しい v1
Event
型を定義してログを扱うことにした - ログ送信コードを触る人と送信されたログを収集する人は異なり、システムも異なり、うまく連携できず受信側のスキーマ更新作業が遅れる場合がある
確認結果
NOTE: ここでは Go の protobuf packages を使ったりすること前提で書いています
実験用コードを以下に記載します。
異なるバージョン間の Marshal/Unmarshal
あまり綺麗にまとまっていませんが、以下のような形になりました。フィールドのタグと型が一致している部分に関してはバージョン更新が行われなかった場合でも最低限拾ってくれます。 しかしながら protobuf は NON NULL 指定をして Unmarshal 時にエラーを起こすようなことができない(はず?なにか良い方法があればコメントください)ので各言語向けに protoc plugin が吐き出す getter などで自前でチェックする必要がありそうです。
Check handling between messages might be compatible : v1 -> v1.5 id:1 created_at:<seconds:1528128844 nanos:914007623 > event_type:TWEET user_id:1 value:"test tweet!" v1.5 -> v1 id:1 created_at:<seconds:1528128844 nanos:914007623 > event_type:TWEET user_id:1 value:"test tweet!" Check handling between messages might be NOT compatible : v1 -> v2 id:1 event_at:<seconds:1528128844 nanos:914007623 > event_type:TYPE_TWEET v2 -> v1 id:1 created_at:<seconds:1528128844 nanos:914007623 > user_id:1
Any
protobuf の Any 型は message の型情報を付与してくれるので、 Marshal するメッセージをこれで wrap してみます。
またメッセージを受け取る側では Unmarshal 前に型チェックを行うようにしてみます。
型チェックは Go では ptypes package の ptypes.Is()
で行えます。
これに基づき、 v2 Event
を Unmarshal する際の動作は以下のようになりました。
v2 のメッセージを v1, v1.5 のメッセージとして Unmarshal しようとした際に ptypes.Is()
のチェックが通らなくなっています。
Check handling Any : type_url:"type.googleapis.com/logging.v2.Event" value:"\010\001\022\014\010\314\312\325\330\005\020\307\314\352\263\003\032\014\010\314\312\325\330\005\020\307\314\352\263\003 \001(\0020\0018\002B\013test tweet!" Any is not loggingv1.Event Any is not loggingv1.RichEvent id:1 event_at:<seconds:1528128844 nanos:914007623 > processed_at:<seconds:1528128844 nanos:914007623 > event_type:TYPE_TWEET event_source:SOURCE_PUBLIC_TIMELINE user_id:1 user_agent:UA_IOS value:"test tweet!"
Any に wrap することによるバイナリサイズ増加
実験に用いた v1 Event
型で言うと、そのメッセージを直接 Marshal した場合と Any で wrap した場合とでは以下のようなサイズ差がありました。
当然ながら Any だと多少サイズが増えています。が、メッセージのフィールドが大きければ相対的に増加具合が目立たなくなるレベルになるかもしれません。
Check marshaled size w/ Any : len(v1bin) : 33 len(v1anyBin) : 73
protobuf の周辺環境
夜に出ている protoc plugin を使って、 protobuf によるスキーマ定義を周辺ツールと連携して行うなど強力な開発・データ分析環境が構築できるかと思われます。
- https://github.com/pseudomuto/protoc-gen-doc
- proto ファイルからドキュメント生成
- https://github.com/GoogleCloudPlatform/protoc-gen-bq-schema
- proto ファイルから BigQuery の schema を生成
- https://github.com/chrusty/protoc-gen-jsonschema
- proto ファイルから JSON Schema を生成
おわりに
protobuf によるスキーマ定義、なかなか良さそうに感じます。特に(メッセージサイズが増えることが許容できるなら) Any 型で包んでやり取りすると必要な際に簡単にメッセージ型チェックを行うことができ、便利だと思われます。 また protoc plugin も多数存在し、単に protobuf を直接使う以外にも他のスキーマ定義を生成するシンプルな DSL として使うのもありかも知れません(JSON Schema とか)。
この分野では Apache Avro と比較した検証ができていないので、どこかのタイミングでしてみたい気持ちもあります。