JSON
protobuf
ProtocolBuffers
スキーマ

今さらProtocol Buffersと、手に馴染む道具の話

Protocol Buffersは別に新しい技術ではない。同時にそれは、未だ知られざる、未だに可能性を秘めた先端のソフトウェア技術基盤である。

新しくないのは事実で、GoogleがProtocol Buffersをオープンソース化したのは2008年のことだし、オープンソース化前に社内で使われ出したのは更に昔に遡るだろう。たぶん。
デザイン的にも、将来JSONが隆盛を極めることなんか全然想定していなかったのが透けて見えて古くさい。

しかし、同時にどうも情報に聡い人であってもなかなかその真価を実感し得ておらず、ある意味で未知の技術であるらしい。ならば、Protobuf (Protocol Buffersの略)を解説した文書は幾多あれども、それに1を加えるのもやぶさかではない。

Protocol Buffersとは

Protobufはスキーマ言語だ!

一般的にはProtobufは「Googleが内部で利用しているシリアライゼーション形式」とか解説されていて、それは嘘ではない。ただ正直なところ、幾つかの理由でシリアライゼーションはどうでも良いと思う。1
プロセス内部の簡素なデータ構造をシリアライズするためにスキーマ定義用のドメイン特化言語があって、こいつがなかなか優秀だというのがProtobufが生き残っている理由だろう。

スキーマ言語はざっとこんな形式をしている。

syntax = "proto3";
package example.protobuf;

message SimpleMessage {
    message HeaderItem {
        string name = 1;
        string value = 2;
    }
    enum Type {
        START = 0;
        BLOB = 1;
        END = 2;
    }

    uint64 id = 1;
    Type message_type = 2;
    repeated HeaderItem headers = 3;
    bytes blob = 4; 
}

別に何と言うことはない。現代のプログラマなら誰でも普通に意味を読み取れるし、見よう見まねで書けることだろう。

あえて言うと、 フィールド定義の行末に = 1 とか書いてあるのは何だろうかと不思議に思うぐらいだろうか。これはフィールドのタグナンバーと呼ばれていて、そのフィールドに対して時間を通じて不変な一意識別子を与える。
Google内部ではリファクタリングの結果によりフィールドの名前が変わるのは良くあることで、ただしそれによってシリアライズされた保存済みデータを処理できなくなるのは困るので、バイナリへのシリアライズ時にはフィールド名ではなくタグが記録される。

とはいえ、シリアライゼーションはどうでも良いので、タグもやはりどうでもよい。予約領域にだけ注意して適当な正整数を一意に指定すればそれで良い。2

なぜそれが重要なのか

なぜ、スキーマ言語が重要なのか。なぜProtobufってやつがなかなか良い感じなのか。

スキーマ言語はなぜ重要なのか

最初の問いに答えると、そういう時代になったからだ。
我々が単一RDBに接続する単一のWebアプリだけを書いていられる時代はとうの昔に終わった。データはあちこちのいろんなストレージ技術で保存されているかもしないし、バックエンドも単一サービスではなくて分割されているかもしれないし、クライアントはweb版, iOS版, Android版があってひょっとしたらそれぞれ別の言語で実装されており、外部開発者向けにAPIも公開しなければならない。

だから、そこら中でデータをシリアライズするしデシリアライズするし、通信の両端で解釈に矛盾が無いようにすり合わせる必要がある。でも、すりあわせの目的でいちいち人間と自然言語で会話するのは苦痛なので、私たちは機械処理可能なコードで語りたい。だから、どういうデータがやってくるのかきちんと宣言的DSLで定義しておきたい。そこでスキーマ言語だ。

JSON Schemaが広がりつつあるのもたぶんそういう理由だし、そもそもProtobuf自体もGoogleが社内で同じ問題にぶち当たって発明されたんだったような気がする。

自分の管轄領域内に閉じたデータ構造であれば、スキーマとか型宣言とか細かい縛りなしで軽量に進めるのもありだ。しかし、他人の領域との界面はしっかり定義しておいた方が良い。スキーマ定義という税金を支払わないとその分のツケはどこかで回ってきて、1時間掛かるE2Eテストがこけるとか、週次変更レビュー合同会議の発足とか、そういうやつで支払うことになる。

スキーマ定義さえあれば、webクライアント開発用の, バックエンド開発用の, アプリ開発用の言語に向けて対応するデータ構造定義を自動生成して、矛盾なくすべてをメンテナンスできる。

通信の両端でスキーマさえ合致していれば、シリアライゼーション形式を合わせるのはむしろ間違えづらい。だから、JSONでもProtobufのデフォルトのそれでも、MessagePackでもXMLでも好きにすれば良い。ちなみに、Protobufで定義したデータ構造はProtobuf標準形式の他にJSONにもきちんとシリアライズできる。3

Protobuf

スキーマ言語が必要なのはおわかり頂いたとして、なぜProtobufが良いのか。
簡単に言えば、シリアライゼーション形式としてJSONが良いのと同じ理由だと思う。

簡素で可読で、プログラミング言語から独立で、任意のデータを表現できるわけではないが十分に適用可能範囲が広い。すべてのニーズを満たそうとして仕様が膨れあがったりしていない。
仕様が小さいので、実装間の意図せぬ非互換性で苦しめられることが少ない。そして、周辺ツールを簡単に開発して足りない物を自分で補える。

シリアライゼーション形式としては、(理由のすべてではないが)こういう理由でJSONが広まった。
スキーマ言語としては、こういう理由でProtobufってやつはなかなか良くできている。

JSON Schemaも頑張ってはいるし、JSONのSchemaをJSONで書くという世界観はなかなか美しい。
GoogleのLingua francaがProtobuf(のシリアライズ形式)であるようにwebのLingua francaがJSONである以上、無視できない要因だ。

ただ、それはあまりにもXML Schemaじみている。XML Schemaのときも思ったけど、本気でJSON Schemaが可読だとはどうしても思えない。
3ヶ月ばかり開発して、その過程でそれなりに大きくなったSchemaをいじくり回して、6ヶ月ほど他の場所を見て回って、ある日そのコードに戻ってきたとしよう。
半年ぶりに見たそのJSON Schema、本当に一瞥して何が書いてあるか理解できるんだろうか。情報密度と、重要な部分に注視を促す文法が足りてなくないだろうか。

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "description": "an example schema",
  "type": "object",
  "properties": {
    "id": {
      "$ref": "http://json-schema.org/draft-06/schema#/definitions/nonNegativeInteger"
    },
    "message_type": {
      "enum": [
        "START",
        "BLOB",
        "END"
      ],
      "default": "START"
    },
    "headers": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/HeaderItem"
      }
    },
    "blob": {
      "type": "string",
      "contentEncoding": "base64",
      "contentMediaType": "application/octet-stream"
    }
  },
  "definitions": {
    "HeaderItem": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "value": {
          "type": "string"
        }
      }
    }
  }
}

古典的な汎用言語とドメイン特化言語の対立だけど、スキーマはとても重要な物なので汎用データ形式に埋め込むよりドメイン特化言語で宣言した方がよいと思う。
だから、Protobufが良い。試しに3ヶ月くらいいじくり回して、半年後に戻ってきてみて欲しい。

簡素で可読で、割と何にでも使えて、しかしすべてをカバーしようとして膨れあがっておらず、ツールを拡張可能。とりわけ何かがすごく良いという訳でもないけれども、すこし使い込めばこの素朴さが手に馴染みやすい。

かくしてProtobufのスキーマはいろいろに使われていて、JSONにシリアライズされるデータ構造を記述するのにも使えるし、BigQuery用のスキーマも書けるRPCを記述するにも良い。

まとめと補遺

  • Protobufは(gPRCまで含めると)、JSON, JSON Schema, OpenAPI, SwaggerCodegenあたりの組み合わせと同じ領域をカバーしようとしている。
  • Protobufが面白いのはJSON Schemaや(もしgRPCを使いたいなら)OpenAPIあたりの領域なので、そこに注目すると良い。無理にJSONを置き換える必要はなく、むしろProtobufスキーマ+JSONとかProtobufスキーマ+MessagePackとか、そういう使い方もよくある。
  • スキーマの領域でも何かがすごく良いというわけではないが、欲張りすぎていないデザインが手に馴染むのでまずは使ってみると良い。この良さは、JSONの良さと同じ形をしている。
  • よくあるProtobufの使い方は、
    • 複数言語間でやり取りされるJSONにスキーマを与える
    • 複数言語間でやり取りされるJSONの、シリアライズ元/デシリアライズ先のクラスや構造体を自動生成する
    • BigQuery用のスキーマ記述がJSON Schemaでもなくて辛いので、もうちょっと汎用性のある言語でスキーマを書く
    • 複数のスキーマ言語間で一貫性を保つ必要があるとき、それらのスキーマ群をProtobufから自動生成する4

続く

次回はProtobufの周辺ツール拡張として、protocプラグインの話でもしようかと思う。


  1. 理由の1つは、今さらJSONの地位に成り代われないこと。それから、Google自身がProtobufスキーマをProtobufのシリアライズ形式以外を記述するのに使いまくっていること。他ならぬ「ProtobufのRPCライブラリ」であるgRPCにしても、シリアライズ形式としてProtobufのやつの他にJSONも普通に選択できるし、ちょっと頑張ればMessagePackだって利用できる。更に、このスキーマ言語としての応用力の強さこそがProtobufの力であるので、膨大なProtobufデータを抱え込んだGoogle以外にとってはシリアライゼーションは極めてどうでも良い。 

  2. あと覚えるべき機能には option があるものの、これは後で良い。あと、面倒なのは extensions だけれども、これももうオワコンの機能なのでどうでも良い。 

  3. あと、Googler以外ろくに使ってない上にドキュメントもあまりない、Protobufのテキスト形式にも。 

  4. こういうところでprotocプラグインが活きてくる