アソビュー株式会社でバックエンドエンジニアをしている進藤です。今回は、私がアソビューにジョインしてから初めて開発に使用するようになったRPCフレームワーク”gRPC”の要素技術のひとつであり、データフォーマットとシリアライズを行うためのツールProtocol Buffers(以降、protobufと表記)のおおまかな歴史とバージョン変遷について、ふりかえりの目的も兼ねて執筆してみようと思います。
私自身、前職での経験としていくつかのAPI開発に携わったことはありましたが、エンドポイントごとにクライアントコードを手動で記述し管理する煩雑さや、ビジネスサイドの要請により変化していくシステムの中でAPIの互換性を保つ難易度の高さに苦手意識を感じていました。
それがprotobufでは、複数の言語に対応したクライアントとサーバーのコードを自動生成することで異なるプログラミング言語間での通信が容易である点、あるいはデータの長期的利用を目的としてバージョン間での互換性を意識した設計ができるよう予め様々なルールが備わっている点など、巨大で複雑なアーキテクチャには大きなメリットをもたらす技術であると感じています。
なおアソビューでは、アーキテクチャにモジュラモノリスを採用しており、WebフロントエンドのSPA化に伴って、gRPC経由でAPI GatewayでJSONに変換し、集約処理をフロントエンドやAPI Gatewayに委ねています。また、マイクロサービス化への移行と互換性を確保するために、公開インターフェースとしてprotobufを活用し、プロトコルや言語に依存しないインターフェース定義を実現しています。これにより、将来的なマイクロサービス化にも柔軟に対応できる設計となっています。
はじめに
この記事は以下の方々を対象にしています。
- まだprotobufに詳しくない、これから使おうとしている初心者
- プロトコルバッファやgRPCを使ったシステム開発者
protobufの誕生背景と概要
protobufはGoogleが開発したシリアライゼーション(データ変換)フォーマットおよびツールで、主に異なるシステム間でデータを効率的に送受信するために使用されます。2000年代初頭、Googleは膨大な量のデータを内部システム間で交換する必要がありました。
XMLやJSONのようなデータフォーマットは可読性が高いものの、パフォーマンスと効率性が低いという課題があったそうです。特に大量のデータ通信が行われるGoogle内部のサービスでは、パフォーマンスの最適化が強く求められていました。結果として、Googleは軽量かつバイナリ形式で高速にデータをエンコード・デコードできる仕組みを開発します。これがprotobufです。
(やや話が脱線しますが、以上のように当時世界規模で最も巨大かつ複雑な内部システムを抱えていた企業の悩みから生み出された技術が、その誕生背景までをも一通り理解しつつ巡り巡っていままさに手元で動いているというストーリー性に個人的には面白さを感じます。出身が文系のエンジニアだからでしょうか、四半世紀前の出来事とはいえ歴史の流れに自分が接続されているようなロマンを感じます。閑話休題。)
protobufはXMLやJSONに比べ、以下のようなメリットがあります。
- バイナリ形式であるため、データサイズが小さく、高速に通信可能
- スキーマ定義(.protoファイル)を使って、異なる言語でもデータ構造を共有
- フォワード互換性、後方互換性をサポートし、柔軟なプロトコルの進化が可能
protobufはXMLやJSONに比べてサイズが小さく、パース速度も速いという特徴を持っています。2008年に最初のバージョンが公開され、その後、gRPCと共に第二版(proto3)で大きな進化を遂げました。データ定義を簡潔に記述でき、型安全性を保ちながら他言語間のデータ交換を効率化します。protobufは、スキーマ言語を使ってデータ構造(メッセージ)を定義します。
例えば、次のようなスキーマを定義します。このスキーマから、protobufのコンパイラを用いて、C++、Java、Pythonなどの様々なプログラミング言語用のコードが生成されます。生成されたコードを使うことで、異なるプラットフォームや言語間で効率的にデータのやり取りができるようになります。
syntax = "proto3"; message Person { string name = 1; int32 id = 2; string email = 3; }
protobufのバージョンアップのポイント
現在のprotobuf最新バージョンは2024年9月にリリースされたv28.1です。このバージョンでは、Java、C++、Python、PHPなどの複数の言語におけるパフォーマンスや互換性の向上、いくつかのバグ修正が行われています。最新バージョンについては、公式のprotobufリリースページから確認できます。ここからは、proto1からのリリースの変遷とproto2とproto3の主な違いについて記載します。 ※https://github.com/protocolbuffers/protobuf/releases
probufのバージョン変遷
proto1(正式な公開は無し。2001年頃にGoogle内部で最初のバージョンが開発された)
- 最初のバージョンで、現在は非推奨
- 限られた機能セットを持っていた
proto2(2008年に初めて外部向けにリリース)
- proto1からの主な改善点:
- 拡張機能(extensions)のサポート
- 未知のフィールドの保持
- ネストされたメッセージ
- デフォルト値の設定
proto3(2016年にリリース)
- 2015年に導入された最新バージョン
- proto2からの簡素化と機能追加が特徴
proto2とproto3の主な違い
- 構文宣言:
- proto2: 省略可能(省略時はproto2として扱われる)
- proto3:
syntax = "proto3";
が必須
- フィールドの必須/オプション指定:
- proto2:
required
,optional
,repeated
が使用可能 - proto3: すべてのフィールドが暗黙的にオプショナル。
required
は廃止
- proto2:
- デフォルト値:
- proto2: カスタムデフォルト値の指定が可能
- proto3: 各型に応じたゼロ値が自動的に使用される
- 未知のフィールド:
- proto2: パース時に保持される
- proto3: v3.5以前は破棄、v3.5以降は保持
- 列挙型(enum):
- proto2: 最初の要素がデフォルト値
- proto3: 最初の要素は必ず0で、デフォルト値となる
- 繰り返しフィールドのエンコーディング:
- proto2:
[packed=true]
オプションで指定 - proto3: 数値型とenum型の繰り返しフィールドは自動的にパックされる
- proto2:
- マップ型:
- proto2: サポートなし
- proto3:
map<key_type, value_type>
構文をサポート
- JSONマッピング:
- proto3: 標準的なJSONエンコーディングをサポート
- 言語サポート:
- proto3: より多くのプログラミング言語をサポート
- APIの簡素化:
- proto3: より簡単で一貫性のあるAPIを提供
required、optional、repeatedは、protobufの文法においてメッセージ内のフィールドの出現回数や必須性を示すキーワードです。バージョンごとにその扱いは異なっており、以下はそれぞれのバージョンでの使い方の変遷を説明しています。proto2ではフィールドの必須性を細かく制御できるため、より厳密なデータモデルを定義できましたが、proto3では簡素化されているため使いやすさと柔軟性が向上しています。
proto2
proto2では、required、optional、repeatedの3つの修飾子が使用され、フィールドが必須か、任意か、または複数回出現するかを明示的に指定
required
requiredフィールドは、必ずメッセージ内で指定されなければならない。シリアライズ時にこのフィールドが存在しないとエラーが発生する。これにより常に必要なデータを確実に含むことが保証されるが、バージョンアップ時に互換性の問題が生じやすいという欠点がある。
message Person { required string name = 1; }
optional
optionalフィールドは、存在するかしないかが任意。デシリアライズ時にこのフィールドが存在しなければデフォルト値が使われる。このフィールドは将来的な拡張がしやすく、後方互換性の保持に役立つ。
message Person { optional string email = 2; }
repeated
repeatedフィールドは、リストや配列のように複数回出現できるフィールドを定義する。このフィールドは0個から複数個のデータを含むことができ、可変長のデータを扱う際に有効。
message Person { repeated string phone_numbers = 3; }
proto3
proto3では、構文が大幅に簡素化されrequiredが廃止された。これによりすべてのフィールドが暗黙的にoptionalとなり、optionalキーワードは不要になった。repeatedは引き続き使用可能。
requiredの廃止
proto3では、requiredフィールドは削除され、全フィールドがデフォルトでオプション扱い。これにより、メッセージ設計が簡素化される一方で、厳密に必須フィールドを指定する必要がある場合には制約がある。
暗黙的なoptional
proto3では、optionalキーワードはなくなったが、すべてのフィールドは暗黙的にオプショナルとして扱われる。フィールドが設定されていない場合には、そのフィールドの型に応じたデフォルト値(例えば、int32なら0、stringなら空文字列)が使用される。
repeatedの継続
repeatedフィールドは引き続きproto3でもサポートされている。
これらの変更により、proto3はより使いやすく、多くの言語やプラットフォームで利用しやすくなっています。ただし、一部の詳細な制御機能は失われているため、プロジェクトの要件に応じて適切なバージョンを選択することが重要です。現時点での最新バーションで定義可能な型は基本型とカスタム型に分類されます。proto3で定義可能な型を以下に示します。
基本データ型
整数型
- int32: 32ビットの符号付き整数
- int64: 64ビットの符号付き整数
- uint32: 32ビットの符号なし整数
- uint64: 64ビットの符号なし整数
- sint32: 32ビットの符号付き整数(符号に応じたエンコーディングを行う)
- sint64: 64ビットの符号付き整数(同様に符号に応じたエンコーディング)
- fixed32: 32ビット固定サイズの符号なし整数
- fixed64: 64ビット固定サイズの符号なし整数
- sfixed32: 32ビット固定サイズの符号付き整数
- sfixed64: 64ビット固定サイズの符号付き整数
浮動小数点型
- float: 32ビットの浮動小数点数
- double: 64ビットの浮動小数点数
ブール型
- bool: 真偽値 (true または false)
文字列型
- string: UTF-8エンコードされた文字列
バイト列型
- bytes: 任意のバイト列
カスタム型
列挙型(enum)
- 列挙型は、定義された定数のセットを持つ
enum Status { UNKNOWN = 0; OK = 1; ERROR = 2; }
メッセージ型
- 複数のフィールドを持つカスタム型で、フィールドは基本型や他のメッセージ型、列挙型を持てる
message Person { string name = 1; int32 id = 2; string email = 3; }
マップ型
- キーと値のペアを表すデータ型。キーにはスカラ型、値には基本型またはメッセージ型が使用可能
map<string, int32> scores = 1;
oneof
- 一度に1つのフィールドだけがセットされる
message Sample { oneof result { int32 id = 1; string name = 2; } }
スカラ型で使用できる型一覧
int32, int64, uint32, uint64, sint32, sint64 fixed32, fixed64, sfixed32, sfixed64, float, double, bool, string, bytes
バージョン選択のポイントと注意点
プロジェクトにおいて、proto2とproto3のどちらを選択すべきかは、プロジェクトの要件や目的によって異なります。以下に、選び方のポイントをいくつか挙げます。proto2はより細かい制御が必要な場合や既存のシステムとの互換性が重視されるプロジェクトに適しています。一方、proto3はシンプルで拡張性の高いAPIを提供し現代的な開発環境に適しているため、新規プロジェクトやスケーラブルなアプリケーションに最適です。
1. APIのシンプルさと開発のスピード
proto3は、よりシンプルで一貫性のあるAPIを提供しています。特に必須フィールド(required)が廃止され、すべてのフィールドが暗黙的にオプショナルになったため、開発が簡素化されています。もし新しいプロジェクトで開発スピードやメンテナンスの容易さを優先するならproto3が最適です。
2. 細かなデータ制御が必要かどうか
proto2は、フィールドをrequiredやoptionalで明示的に指定でき、未使用フィールドも保持するため、細かなデータ制御が可能です。一方、proto3はフィールドが全てオプショナルであり、特定のフィールドが必須である場合の制約を表現できません。より厳密なデータ定義が必要な場合はproto2の方が適しています。
3. プロジェクトの互換性と拡張性
proto3はJSONとの標準的なマッピングをサポートし、多くのプログラミング言語との互換性を高めています。特に、Webサービスやモバイルアプリケーションのように、異なるプラットフォーム間でデータを効率的にやり取りする必要がある場合proto3の方が有利です。逆に既存のproto2ベースのシステムと互換性を保つ必要がある場合はproto2の使用を続ける方がスムーズでしょう。
4. デフォルト値の柔軟性
proto2では、カスタムデフォルト値の指定が可能で、proto3は型に応じたゼロ値を自動的に使用します。特定のフィールドでカスタムデフォルト値を保持したい場合はproto2が適していますが、ゼロ値が適切であればproto3の方がシンプルに管理できます。
5. 将来のメンテナンスやアップグレード
proto3は今後の開発やサポートが強化されており、新機能やバグフィックスもproto3向けに提供されることが多くなっています。長期的にメンテナンスを視野に入れる場合は、proto3を選択することで将来のバージョンアップや新機能への対応が容易になるでしょう。
最後に
この記事を通じて、protobufのおおまかな誕生背景とバージョンごとの特徴について学んでいただけたと思います。protobufは、効率的なデータ通信と柔軟なプロトコル定義を可能にする強力なツールです。特に、gRPCと組み合わせることで、スケーラブルで高速な分散システムを構築する際の重要な要素となります。バージョンごとの違いや、各バージョンで提供される機能を理解することでプロジェクトに最適な技術選定が可能になります。これからgRPCやprotobufを使い始めようと考えている方にとって、この知識が役立つことを願っています。
アソビューでは「生きるに、遊びを。」をミッションに、一緒に働くメンバーを募集しています! ご興味がありましたらお気軽にご応募ください。 www.asoview.com