- Published on
バイナリシリアライゼーション完全ガイド 2025: Protobuf, Thrift, Avro, MessagePack, FlatBuffers, Cap'n Proto — 性能 vs 柔軟性の選択
- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに: JSONは本当に最適か?
よくあるシナリオ
REST APIを作るとき、ほとんどの開発者が自然に選ぶもの:
{
"id": 12345,
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
JSONは人間が読めて、言語中立で、どこでもサポートされる。完璧に見える。
しかし、1日10億件のリクエストを処理するサービスだったら? 各リクエストが1 KBのJSONなら1日に1 TBのデータ。パース コスト、ネットワーク帯域幅、ストレージ — すべて高価になる。
同じデータをProtobufで表現すると約100バイト。パース速度は10倍以上速い。1日100 GBの代わりに10 GB。1年なら数十TBの差。
なぜバイナリシリアライゼーションなのか?
JSONの弱点:
- サイズ: フィールド名を毎回繰り返す。
"email"が何十回も。 - パースコスト: テキストを数値/オブジェクトに変換。
- 型情報なし: 実行時の型チェックが必要。
- スキーマなし: ドキュメントと検証が別。
バイナリフォーマットの強み:
- 小さい: フィールド名の代わりに数値tag。
- 速い: 直接メモリマッピングまたは単純なデコード。
- 型安全: スキーマベース。
- 進化可能: schema evolutionサポート。
この記事で比較するもの
- Protocol Buffers (Google): 最も広く使われる標準。
- Thrift (Facebook/Apache): RPC統合。
- Avro (Apache Hadoop): ビッグデータの事実上の標準。
- MessagePack: JSONのバイナリ置換。
- FlatBuffers (Google): Zero-copy読み込み。
- Cap'n Proto (Sandstorm): Zero-copy + RPC。
各フォーマットの内部構造、エンコーディング方式、いつ使うかを720行で掘り下げる。
1. Protocol Buffers: 標準の王
歴史
- 2001: Google内部開発開始。
- 2008: オープンソースで公開 (proto2)。
- 2016: proto3リリース。より単純な文法。
- 2023: Edition 2023リリース。段階的機能追加。
gRPCのデフォルトフォーマット。Google全体で使われ、外部でも最も人気のあるバイナリシリアライゼーション。
IDL (Interface Definition Language)
Protobufは.protoファイルでスキーマを定義する:
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
string email = 3;
optional int32 age = 4;
repeated string phone_numbers = 5;
}
- 各フィールドはtag number (1, 2, 3...)を持つ。
- Tag numberがワイヤフォーマットのキー。フィールド名はコード生成のみに使用。
コード生成
protocコンパイラがターゲット言語のコードを生成:
protoc --java_out=. --python_out=. --go_out=. person.proto
結果: Java、Python、Goクラス。それぞれにシリアライズ/デシリアライズメソッドを含む。
Wire Format: 核心アイデア
Protobufのエンコーディングはkey-valueストリーム。各フィールドは:
[tag + wire_type][value]
Wire types:
0: Varint (整数、boolean、enum)。1: 64-bit (fixed64, double)。2: Length-delimited (string, bytes, message, packed repeated)。5: 32-bit (fixed32, float)。
Varint: 可変長整数
Protobufの核心最適化: varintエンコーディング。小さい数値は少ないバイトで:
150をvarintに:
150 = 0b10010110
Step 1: 7ビットずつ分割
→ [0b0000001, 0b0010110]
Step 2: 各バイトのMSBを「継続フラグ」として使用
→ [0b10010110, 0b00000001]
Step 3: Little-endian順
→ [0x96, 0x01]
結果:
0~127: 1バイト。128~16383: 2バイト。16384~2097151: 3バイト。- など。
小さい数値は1バイト、大きい数値は複数バイト。ほとんどのフィールドは小さいので、平均的に効率的。
エンコーディング例
message Person {
int32 id = 1;
string name = 2;
}
id = 150, name = "Bob"
Wire:
Field 1 (id):
tag = (1 << 3) | 0 = 0x08 (field 1, wire type 0 = varint)
value = 150 varint = 0x96 0x01
Field 2 (name):
tag = (2 << 3) | 2 = 0x12 (field 2, wire type 2 = length-delimited)
length = 3
value = "Bob" = 0x42 0x6F 0x62
Total: 08 96 01 12 03 42 6F 62 (8 bytes)
8バイト! JSONは:
{"id":150,"name":"Bob"}
23バイト。2.9倍小さい。
ZigZag Encoding: 負数の最適化
Varintは正数に最適化されている。負数は非常に大きなvarintになる (2の補数で変換されるため):
-1 → 0xFFFFFFFF = 10バイト!
解決: sint32、sint64型はZigZagエンコーディングを使う:
0 → 0
-1 → 1
1 → 2
-2 → 3
2 → 4
...
公式: (n << 1) ^ (n >> 31)
負数と正数が交互にマッピングされ、絶対値が小さいほどエンコーディングが短くなる。
Schema Evolution
Protobufはforward/backward compatibilityをサポート:
ルール:
- Tag numberを絶対に変えない: tagはID。
- フィールド名変更OK: wire formatと無関係。
- 新フィールド追加OK: 古いクライアントは無視。
- フィールド削除OK、ただしtagをreservedに: 再利用防止。
- requiredは使わない: proto3で削除。
例:
// v1
message Person {
int32 id = 1;
string name = 2;
}
// v2 (互換性維持)
message Person {
int32 id = 1;
string name = 2;
string email = 3; // 新フィールド
reserved 4, 5; // 将来のための予約
}
v1サーバがv2クライアントのメッセージを受け取ってもemailフィールドを知らず無視する。逆も同様。これが分散システムでデプロイ時に非常に重要。
Proto3のDefault Values
Proto3はすべてのフィールドがoptional。なければdefault値:
- 数値: 0
- 文字列: ""
- bool: false
- message: null
落とし穴: 「値が0なのか?」と「値がないのか?」を区別できない。これを解決するにはoptionalキーワード (proto3 2.6+):
message Person {
optional int32 age = 1; // 明示的な存在追跡
}
性能
Protobufは非常に速い:
- JSONに対し3~10倍小さいサイズ。
- パース速度: 5~100倍 (言語実装による)。
- メモリ割り当てが少ない (再利用可能)。
2. Apache Thrift: RPC統合
誕生
- 2007: Facebook公開。
- 2008: Apache Incubator。
- 現在: 多くの大企業の内部RPC標準。
Protobufと同時期に登場。Facebookの規模で検証されている。
Protobufとの違い
Thriftはシリアライゼーション + RPC + サーバフレームワークをすべて提供する:
service UserService {
Person getUser(1: i32 id) throws (1: NotFoundException e),
list<Person> listUsers(1: i32 limit, 2: i32 offset)
}
struct Person {
1: i32 id,
2: string name,
3: optional string email
}
一つのファイルにデータ構造とサービス定義が一緒。ProtobufはgRPCと分離されていたが、Thriftは統合。
Transport層
Thriftの特異な点: transport、protocol、serverを分離。
Transport: データがどう移動するか。
- TSocket, TFramedTransport, TMemoryBuffer, ...
Protocol: データがどうエンコードされるか。
- TBinaryProtocol: 単純、固定サイズ。
- TCompactProtocol: 可変長 (varint類似)。
- TJSONProtocol: JSON形式。
Server: リクエストをどう処理するか。
- TSimpleServer, TThreadPoolServer, TNonblockingServer, ...
この組み合わせ可能性で状況に合わせて選択可能。
TCompactProtocol
Thriftの最も効率的なエンコーディング。Protobufと似たアイデア:
- Varint: 整数。
- ZigZag: 負数。
- Field ID delta: 前のフィールドIDとの差だけ保存 (より小さい)。
- Type + ID統合: 1バイトにwire typeとfield idを一緒に。
結果: Protobufとほぼ同じサイズ。場合によっては少し小さい。
長所と短所
長所:
- 統合RPC: 別途gRPC設定不要で完結。
- 言語サポート: 27以上。
- 成熟: Facebook、Uber、Twitter内部検証済み。
- 柔軟なtransport/protocol。
短所:
- 最近の更新減少: Thrift 4議論中だが遅い。
- ドキュメント分散: Protobufほど整理されていない。
- gRPCに押される: Googleのマーケティング力。
選択指針:
- 新プロジェクト: gRPC (Protobuf)。
- 既存Thriftシステム: 維持。
3. Apache Avro: ビッグデータの友
背景
Hadoop生態系から生まれたフォーマット。現在Kafka、Spark、Hiveの事実上の標準。
核心特徴: Schemaをデータと共に
Protobuf/Thriftはスキーマがコード生成時点で使われる。Avroは違う:
Avroの哲学: スキーマがデータと共に伝達されるか、中央で管理される。
2つのモード:
- Container File Mode: ファイルヘッダにschemaを含む。
- Schema Registry Mode: 中央registryからschema IDで参照。
Schema例
AvroスキーマはJSONで記述:
{
"type": "record",
"name": "Person",
"fields": [
{ "name": "id", "type": "int" },
{ "name": "name", "type": "string" },
{ "name": "email", "type": ["null", "string"], "default": null },
{ "name": "age", "type": "int", "default": 0 }
]
}
Wire Format
Avroのエンコーディングは非常に単純:
- 固定サイズ型: そのまま保存 (int, long, float, double)。
- ZigZag varint: intとlong。
- 可変サイズ: 長さ + データ (string, bytes)。
- Record: フィールドをスキーマ順で列挙。
重要: Tag numberなし! フィールド順序が構造。
結果: 非常に圧縮されたバイナリ。スキーマなしでは解釈不可。
例
同じPerson:
id=150, name="Bob", email=null, age=0
Avro wire:
150 (zigzag varint) = 0xAC 0x02 (2 bytes)
length 3 + "Bob" = 0x06 0x42 0x6F 0x62 (4 bytes)
null union tag = 0x00 (1 byte)
0 (zigzag varint) = 0x00 (1 byte)
Total: 8 bytes
Schema Evolution: Writer vs Reader Schema
Avroのキラー機能: Writer schema ≠ Reader schema。
- Writer schema: データを書くときに使われたスキーマ。
- Reader schema: データを読むときに期待するスキーマ。
Avroは両スキーマ間の差を自動解決する:
Writer: {id, name, age}
Reader: {id, name, email}
ageはreaderにない → 無視。emailはwriterにない → readerのdefaultを使用。
これにより互換性チェックとマイグレーションが容易。
Schema Registry
Confluent Schema RegistryはAvroスキーマを中央管理:
Producer:
schema_id = registry.register(schema)
message = [magic byte][schema_id (4B)][avro payload]
Consumer:
schema_id = extract_from_message
schema = registry.get(schema_id)
data = decode(payload, schema)
各メッセージは4バイトのschema IDのみ含む。Payloadは純粋なAvro。数十~数百GBのKafkaトピックでこの節約が累積すると莫大。
Kafka + Avro + Schema Registry
この3つはビッグデータストリーミングのゴールデンコンビネーション:
- Kafka: メッセージブローカ。
- Avro: 効率的エンコーディング + schema evolution。
- Schema Registry: 中央スキーマ管理 + 互換性検証。
Confluent Platformのデフォルトアーキテクチャ。Netflix、LinkedIn、Uberなど全員採用。
Protobuf vs Avro
| 項目 | Protobuf | Avro |
|---|---|---|
| スキーマ必要時点 | コード生成時 | 書き/読み時 |
| Field identification | Tag number | 順序 |
| Wire size | 類似 | 類似 (Avroやや小さい) |
| Schema evolution | 良好 | 最強 |
| ビッグデータ親和性 | 中 | 最強 |
| RPCサポート | gRPC | なし (別途) |
| 言語サポート | 広範 | 中 |
選択基準:
- RPC: Protobuf (gRPC)。
- Kafka / ビッグデータストリーミング: Avro。
- ファイル保存: Avro (自己記述ファイル)。
4. MessagePack: JSONの直接的代替
哲学
MessagePackのスローガン: "It's like JSON. but fast and small."
- JSONと1:1マッピング可能。
- スキーマ不要。
- バイナリでより小さく速い。
例
JSON:
{"id": 150, "name": "Bob"}
MessagePack (hex):
82 A2 69 64 CC 96 A4 6E 61 6D 65 A3 42 6F 62
82: 2項目のmap。A2: 長さ2のfixstr ("id")。CC 96: uint8値150。A4: 長さ4のfixstr ("name")。A3 42 6F 62: 長さ3のfixstr "Bob"。
計15バイト。JSONの25バイトから40%削減。
特徴
- スキーマなし: 即時使用可能。
- 型保存: int, string, array, map, binary, など。
- Extension: ユーザ定義型サポート (timestamp, UUIDなど)。
- 言語サポート: 50以上。
使用先
- Redis: Luaスクリプト結果、一部内部通信。
- Fluentd: ログ収集プロトコル。
- ZeroMQ: メッセージバインディング。
- ゲーム: リアルタイムネットワークプロトコル。
Protobufとの違い
| 項目 | MessagePack | Protobuf |
|---|---|---|
| スキーマ | なし | 必須 |
| サイズ | 中 | 小 |
| パース速度 | 速い | 非常に速い |
| 自己記述 | はい | いいえ |
| Schema evolution | 弱い | 強い |
| IDEサポート | 少ない | 多い |
MessagePackはJSON代替として最適。スキーマシステムが負担だがJSONより速く小さくしたいとき。
5. FlatBuffers: Zero-Copyの答え
動機
Googleがゲームとモバイル用に開発。目標: デシリアライズ時間0。
既存フォーマットの問題:
- Protobuf/Avro/MessagePack: 読むには全体パースが必要。新オブジェクト割り当て。
- 小さなメッセージは問題ないが大きなメッセージや頻繁にアクセスする場合のオーバーヘッド。
アイデア: メモリから直接使う
FlatBuffersはメモリレイアウト自体をシリアライゼーションフォーマットとして使う:
Byte stream:
[root offset][vtable 1][table 1][vtable 2][table 2]...
読むとき:
- バイトバッファを取得 (memcpyまたはmmap)。
- ルートoffsetに従う。
- オブジェクト割り当てなしに直接フィールドアクセス。
auto person = GetPerson(buffer);
int id = person->id(); // 実際のパースなしに直接メモリを読む
結果: デシリアライズ時間ほぼ0。メモリ割り当てなし。キャッシュフレンドリ。
IDL
Protobufに似たスキーマ:
namespace Game;
table Person {
id: int;
name: string;
email: string;
}
root_type Person;
性能
- デシリアライズ: Protobufに対し1000倍速い (ほぼ0)。
- メモリ: 割り当てなし。
- シリアライズ: Protobufよりやや遅い (vtable生成)。
- サイズ: Protobufより20~50%大きい。
使用先
- ゲームエンジン: Cocos2d、各種ゲームデータ。
- モバイルアプリ: データファイルフォーマット。
- Apache Arrow: 一部フォーマット共有。
- Facebook: 主要ユーザ。
- TensorFlow Lite: モデルファイル。
いつ使うか
- 頻繁にアクセスするが書き込みは稀。
- 大きなデータ構造。
- メモリ制約。
- 高速ロードが重要。
欠点:
- サイズ: Protobufより大きい。
- 柔軟性低: 書き込みが複雑。
- デバッグ困難: バイナリを直接見るのが難しい。
RandomAccessの価値
FlatBuffersの大きな強み: ランダムアクセス。1 GBファイルから特定フィールドだけ読みたい場合:
auto file = mmap(filename); // 実際のディスクロードはlazy
auto root = GetRoot(file);
auto item_100 = root->items()->Get(100);
int value = item_100->value();
// ここで実際に必要なページだけディスクからロード
全体パース不要。大きな設定ファイル、ゲームアセット、MLモデルに理想的。
6. Cap'n Proto: FlatBuffersのいとこ
誕生
Kenton Varda (Protobuf v2の主要開発者)がGoogleを離れて作った次世代フォーマット。哲学:
"Protocol Buffers, infinity times faster."
Zero-Copy + RPC
FlatBuffersのzero-copyの利点 + 内蔵RPCシステム。Sandstormプロジェクトで使用。
スキーマ
struct Person {
id @0 :Int32;
name @1 :Text;
email @2 :Text;
}
Protobufに似ているが@N文法で順序を示す。
エンコーディング特性
- 整列されたメモリ: 8バイト境界。
- Zero-copy可能: FlatBuffersのように。
- Packed圧縮: 選択的に適用してサイズ削減。
- ランダムアクセス: 大きなメッセージの一部のみアクセス。
Canonical form
Cap'n Protoは同じデータは常に同じバイトを生成しようとする (hashベース比較に有用)。
RPC特徴
Time Traveling RPC: サーバが応答する前にその結果を別のサービスに事前伝達できる。Promise pipelining。
Client → A: getUser(1)
Client → A: (promise of user) → B.emailSummary(user)
AとBの間で直接データが流れる。Clientが間に入らない。レイテンシ削減。
使用先
FlatBuffersほど使われていないが、Cloudflareのような大企業が内部で使用。
FlatBuffers vs Cap'n Proto
| 項目 | FlatBuffers | Cap'n Proto |
|---|---|---|
| Zero-copy | はい | はい |
| サイズ | 中 | 類似 |
| RPC | なし | あり |
| 成熟度 | より成熟 | 若い |
| 言語サポート | 多い | 少ない |
Cap'n Protoが技術的に優れた点があるが、FlatBuffersが実戦でより広く使われる。
7. その他注目に値するフォーマット
CBOR (Concise Binary Object Representation)
IETF標準 (RFC 8949)。JSONの「本物の」バイナリ版。IoTとCoAPで使用。
- スキーマなし。
- JSONと1:1マッピング。
- MessagePackに類似だが標準化されている。
BSON (Binary JSON)
MongoDBの保存フォーマット。JSONスタイルだが追加型 (ObjectId, Date, Binary)。
- スキーマなし。
- JSONよりやや大きいがパース速い。
- 主にMongoDB内部用途。
Apache Arrow
Columnarメモリフォーマット。前述。シリアライゼーションも可能だが主用途はプロセス間zero-copy交換。
Parquet
Columnarディスクフォーマット。前述。シリアライゼーションというより保存フォーマットだが言語間互換性を提供。
Bencode
BitTorrentのプロトコル。単純だが効率は劣る。
Smile
JSON-likeバイナリ。Jacksonライブラリの一部。
8. 性能比較: 数字で見る
ベンチマークシナリオ
典型的メッセージ (Personオブジェクト、100フィールド、ネスト含む):
| フォーマット | サイズ | シリアライズ | デシリアライズ |
|---|---|---|---|
| JSON | 5.2 KB | 85 us | 120 us |
| BSON | 4.8 KB | 75 us | 105 us |
| MessagePack | 3.8 KB | 45 us | 60 us |
| Protobuf | 2.8 KB | 15 us | 25 us |
| Avro | 2.6 KB | 20 us | 35 us |
| Thrift (Compact) | 2.7 KB | 18 us | 30 us |
| Cap'n Proto | 3.2 KB | 10 us | 1 us |
| FlatBuffers | 3.5 KB | 12 us | 1 us |
主要観察:
- サイズ: Avro、Thrift、Protobuf、FlatBuffers、MessagePack、JSONの順に大きくなる。
- シリアライズ速度: すべてJSONより速い。
- デシリアライズ: Cap'n Proto/FlatBuffersが圧倒的 (zero-copy)。
実戦スループット
1 GBデータ処理 (100 GBシステムメモリ):
| フォーマット | 時間 |
|---|---|
| JSON | 120 s |
| MessagePack | 60 s |
| Protobuf | 25 s |
| Avro (columnar compression) | 30 s |
| FlatBuffers | 2 s (主にmmap時間) |
FlatBuffersの利点は大量データ処理で極めて明確。
9. Schema Evolution比較
時間が経つにつれてスキーマが変わる。どうやって異なるバージョンを処理するか?
Protobuf
- Tag numberベース: 変更禁止。
- フィールド追加: OK (新tag)。
- フィールド削除: OK (tagをreservedに)。
- 型変更: 互換型同士のみ (int32とint64の間)。
- Forward/Backward互換性: 両方可能。
安全な変更 (v1からv2へ):
- 新フィールド追加。
- フィールド名変更。
- requiredからoptionalへ。
危険な変更:
- Tag number変更。
- 型の急変 (stringからintへ)。
- フィールド削除後のtag再利用。
Thrift
Protobufに類似。Tagベースで互換性をよくサポート。
Avro
Writer + Reader schemaで処理。Avroのschema resolutionルール:
- フィールドマッチング: 名前基準 (aliasesサポート)。
- ないフィールド: default値使用。
- 型promotion: intからlongへ、floatからdoubleへ。
- Union進化: null追加可能。
Avroは最も強力なevolutionシステム。複雑な変更も可能。
JSON
スキーマなし → 評価が難しいが事実上何でも可能。
- 新フィールド追加: 簡単 (クライアントが無視)。
- フィールド削除: 危険 (クライアントが依存する可能性)。
- 型変更: 危険。
JSON Schemaを使えば検証可能だが、コードレベルの自動互換性はない。
FlatBuffers / Cap'n Proto
Tagベース。Protobufに類似のルール。ただし、zero-copyのためフィールド再配置禁止。
実戦ルール
- Tag/フィールド番号は永遠: 絶対に再利用しない。
- 追加のみ: 削除より追加が安全。
- Optionalをデフォルト: 新フィールドはoptionalで。
- Default値: 明示的に設定。
- Deprecated表示: コードコメントで廃止予定を通知。
- CIで互換性検証: buf breaking (Protobuf)、Schema Registry (Avro)。
10. 選択ガイド
シナリオ別推奨
REST API (公開API):
- JSON: 依然としてデフォルト。誰でもパース可能。
- 内部最適化: MessagePackまたはProtobuf over HTTP。
gRPCサービス:
- Protobuf: デフォルト。gRPCがこれを前提に設計。
Kafka / ストリーミング:
- Avro + Schema Registry: 標準組み合わせ。
- 代替: Protobuf with Schema Registry。
ゲーム / リアルタイムネットワーキング:
- FlatBuffers: ゲームデータ。
- Cap'n Proto: 追加RPCが必要なとき。
- MessagePack: 柔軟性が必要なとき。
設定ファイル / CLI:
- YAML/JSON/TOML: 人間が読めなければならない。
MLモデル:
- FlatBuffers: TensorFlow Lite方式。
- Protobuf: ONNX方式。
- SafeTensors: 最新トレンド。
ログ / イベント:
- Avro: Big dataパイプライン。
- Protobuf: gRPCログ。
- JSON: 人間がパース。
IoT / 組み込み:
- CBOR: 標準IoT。
- Protobuf: 性能重視。
- MessagePack: 柔軟性。
MongoDB保存:
- BSON: 固定 (MongoDB専用)。
決定木
スキーマを望むか?
|-- はい
| |-- RPC統合が必要? -> gRPC + Protobuf or Thrift
| |-- ビッグデータ進化が重要? -> Avro
| |-- Zero-copyが必要? -> FlatBuffers or Cap'n Proto
| +-- 一般 -> Protobuf
+-- いいえ
|-- JSON互換? -> MessagePack or CBOR
|-- MongoDB? -> BSON
+-- 人間が読むべき? -> JSON/YAML
11. 落とし穴と実戦教訓
落とし穴 1: "JSONで十分なのにProtobufを使う"
問題: 小規模APIに複雑なコード生成パイプライン。
教訓: ツールが問題を正当化しなければならない。トラフィックが低ければJSONの開発利便性がProtobufの性能優位を上回る。
落とし穴 2: "Protobufで古いフィールドを削除しtagを再利用"
問題: v1データをv2コードが誤解釈。
教訓: Tag numberは永遠。削除が必要ならreservedに表示。
落とし穴 3: "Avroでwriter schemaを失う"
問題: Kafkaトピックの古いメッセージを読めない。
教訓: Schema Registry使用。Writer schemaは必ず保管。
落とし穴 4: "FlatBuffersのファイルサイズ増加"
問題: ProtobufからFlatBuffersへ転換後ファイルサイズ増加。
教訓: FlatBuffersは速度優先。サイズが重要ならProtobufがよい。またはFlatBuffersに圧縮層を追加。
落とし穴 5: "数十のターゲット言語コード生成維持"
問題: protocバージョン管理、各言語別ビルド。
教訓: モノレポ + bufのようなツール使用。CIで自動化。
落とし穴 6: "Schema evolutionルール違反"
問題: フィールド追加なのにrequiredで、または型変更。
教訓: 自動検証。buf breaking、Schema Registry compatibility check。
12. 実戦ツール
buf: Protobufのモダンツール
Uberとコミュニティが作ったbufはProtobuf開発を画期的に改善:
# Lint
buf lint
# Breaking change detection
buf breaking --against .git#branch=main
# Generate code
buf generate
# Remote plugins (protoc installation not needed)
buf generate --template buf.gen.yaml
すべてのプロジェクトに推奨。
Confluent Schema Registry
Avro/Protobuf/JSON Schemaを中央管理:
- REST APIでスキーマ登録/照会。
- Compatibility検証。
- Kafka統合。
// Kafka producer
properties.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
properties.put("schema.registry.url", "http://localhost:8081");
プロダクションKafka + Avro環境の必須。
protobuf-validator
Protobufメッセージにvalidationルール追加:
message Person {
int32 age = 1 [(validate.rules).int32 = { gte: 0, lte: 150 }];
string email = 2 [(validate.rules).string.email = true];
}
生成されたコードにvalidateメソッド追加。
grpcurl / grpc_cli
gRPCサービスをcurlのようにテスト:
grpcurl -d '{"id": 1}' localhost:50051 myservice.UserService/GetUser
Server reflectionを通じてスキーマを自動取得。
クイズで復習
Q1. Protobufのvarintが小さい数値に効果的な理由は?
A. Varintは7ビットずつ値をエンコードし、各バイトのMSBを「次のバイトがあるか?」フラグとして使う。
例:
0~127: 1バイト (MSB=0)。128~16383: 2バイト。- 大きい数値: より多くのバイト。
通常のint32の場合: 常に4バイト。 Varint:
0: 1バイト100: 1バイト10000: 2バイト1000000: 3バイト2^31-1: 5バイト (むしろ大きい!)
実戦観察: ほとんどのID、カウント、ビットマスク、インデックスは小さい値。数百万回のメッセージでほとんどの整数が1~2バイトでエンコードされれば全体サイズが大きく減る。
例外: 負数
-1をint32 varintにすると? 2の補数のため0xFFFFFFFFになり、5バイトが必要。これは災難。
解決: ZigZag
sint32型はZigZagエンコーディングを使う:
0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...- 絶対値が小さいほど短くエンコード。
したがって負数がよく出るフィールドはsint32/sint64を使うべき。Protobufチュートリアルの重要な教訓だがしばしば見逃される。
教訓: データの分布を知ってフォーマットを選べば大きな違いを生む。Protobufのvarintは小さい数値の一般的分布を活用した賢い設計。
Q2. AvroがProtobufと違って「スキーマをデータと共に伝達」する理由と利点は?
A. ProtobufとAvroは根本的に異なる哲学を持つ。
Protobuf:
- スキーマはコード生成時点にのみ必要。
- Wire formatにtag numberのみ。
- フィールド名がなくてもtagで識別。
- 実行時にはスキーマが「すでに知られた」状態を仮定。
Avro:
- スキーマが書き/読み時点に必要。
- Wire formatにフィールド名やtagなし (順序のみ)。
- スキーマなしでは解釈不可。
- 2つの運用モード:
- Container file: ファイルヘッダにスキーマ含む。
- Schema Registry: メッセージにschema IDのみ、実際のスキーマは中央で。
なぜこの違い?
Avroはビッグデータとストリーミングのために設計された。この環境の特性:
- データが長く生きスキーマが進化。
- 消費者が未来で誰なのか分からない。
- Writer schema ≠ Reader schemaが日常。
Avroの利点:
-
Writer vs Reader schema resolution:
- Avroは両スキーマを受け取り自動変換。
- Protobufはtagベースで片側だけ分かればOK。
- 複雑なマイグレーションでAvroがより柔軟。
-
自己記述ファイル:
- Avro container fileはスキーマを含むので「どんなツールでも」読める。
- Protobufは
.protoファイルがなければ読めない。
-
Schema Registryパターン:
- 中央でスキーマを管理しcompatibility検証。
- Kafka + Avroがビッグデータの標準になった理由。
Protobufの反論:
- Tagベースがより効率的 (フィールド名なし)。
- コード生成が型安全性を提供。
- gRPCと統合。
結論: Avroは**「データが長く生きスキーマが変わる」という仮定に最適化され、Protobufは「スキーマがコードと共にデプロイされる」という仮定に最適化された。どちらが正しいかは運用環境**次第:
- RPCシステム (Protobufが有利)
- ビッグデータストリーム (Avroが有利)
- 保存ファイル (Avroが有利)
両方とも検証された技術で、それぞれの領域で最善の選択。
Q3. FlatBuffersが「デシリアライズ時間0」を達成する方法は?
A. メモリレイアウト自体をシリアライゼーションフォーマットとして使う。
従来のシリアライゼーションの問題: Protobuf、Avro、MessagePackなどはシリアライズされたバイトを順次パースする:
// Protobufスタイル
Person person;
person.ParseFromArray(buffer, size); // 全体データをパース
int id = person.id(); // すでにパースされたフィールドにアクセス
- 全体バッファを読まなければならない。
- 新オブジェクトを割り当ててフィールドを埋める。
- サイズに比例する時間とメモリ消費。
FlatBuffersのアプローチ: シリアライズされたバイトはすでにメモリから直接使える構造:
// FlatBuffers
auto person = GetPerson(buffer); // ポインタだけキャスト、パースなし
int id = person->id(); // 内部でoffset計算、直接メモリ読み取り
動作原理:
- バッファ開始にroot offset保存。
- 各オブジェクトの前にvtable offset (可変構造処理)。
- Vtable: 「フィールドXはoffset Yにある」という情報。
- フィールドアクセスはoffset加算 + メモリ読み取り。O(1)。
利点:
- デシリアライズ時間: 0 (バッファマッピング時間のみ)。
- メモリ割り当てなし: 既存バッファ再利用。
- ランダムアクセス: 1 GBファイルでフィールド1つだけ必要ならその部分のみロード。
- mmapフレンドリ: 大きなファイルも怠惰にロード。
- Cache効率: 一度読めばCPUキャッシュに残り再利用。
コスト:
- サイズ: vtableオーバーヘッドでProtobufより20~50%大きい。
- シリアライズ複雑: builderで段階的に構成。
- 柔軟性低: 一度作ったバッファを修正しにくい。
- デバッグ: バイナリ構造を直接見にくい。
いつFlatBuffersを使うか:
- ゲームデータ: キャラクター情報、レベルデータ。よく読むがほぼ変わらない。
- MLモデルファイル: TensorFlow Liteが代表。
- モバイルアプリデータ: 高速ロードが重要。
- 組み込み: メモリ制約厳しい、割り当て高価。
いつ使わないか:
- RPC: 毎リクエストごとにメッセージが違う → zero-copy利点を享受できない。
- 小さなメッセージ: 数KB以下ならパース時間は無視できる。
- 頻繁な修正: 書き込みが面倒。
- ネットワークサイズ重要: Protobufがより小さい。
FlatBuffers vs 通常パーサ:
- 1 MBメッセージを1000回パース:
- Protobuf: 1000 × 20 ms = 20秒。
- FlatBuffers: 1000 × 0.001 ms = 1 ms。
1000万倍ではないが、大量処理では極めて大きな違い。これがFlatBuffersがゲームエンジンとMLランタイムの標準になった理由。
教訓: 「シリアライゼーションフォーマット」が必ずしも「パース可能なバイトストリーム」である必要はない。FlatBuffersは「メモリレイアウト = ワイヤフォーマット」という大胆な選択で新しい性能領域を開いた。エンジニアリングは時に基本仮定を覆すとき飛躍する。
Q4. Schema evolutionで「フィールド削除」がなぜ危険か?
A. 3つのレベルの危険がある。
1. Tag number再利用の危険
Protobuf/Thriftでtagは永遠のID:
// v1
message Person {
int32 id = 1;
string name = 2;
string deprecated_field = 3; // これを削除すると...
}
// v2 (危険!)
message Person {
int32 id = 1;
string name = 2;
int32 new_field = 3; // tag 3を再利用
}
問題シナリオ:
- v1データが保存されたKafkaトピックやDBに
deprecated_field = "hello"が残っている。 - v2コードがこのデータを読む → tag 3が
int32として解釈される。 "hello"をint32として読もうとしてエラーまたはゴミデータ。
解決: reservedキーワードでtag再利用防止:
message Person {
int32 id = 1;
string name = 2;
reserved 3;
reserved "deprecated_field";
}
これでv2でtag 3を再利用しようとするとコンパイルエラー。
2. Readerがフィールドを期待する場合
Avroでフィールド削除はreader schemaに該当フィールドがなければOK:
- Writer v1:
{id, name, age} - Reader v2:
{id, name}(ageなし) - → age無視。安全。
問題: Readerでフィールド削除後、再びwriterがそのフィールドを使わなければ?
- Reader v2はv1データを読める (age無視)。
- Reader v2はv2データも読める (ageなし)。
- しかしReader v1はv2データを読むとき... v1はageを期待したがv2にはない → defaultまたはnull。ビジネスロジックが壊れる可能性。
教訓: 削除はすべてのreaderが消えた後にのみ。
3. データ依存性
クライアントコードが該当フィールドを使用していれば削除は災難:
# Old client code
age = person.age # KeyError / AttributeError / 0 (default)
send_birthday_wishes(person.age) # おかしな結果
フィールド削除はAPI契約変更。すべての使用箇所を追跡しなければならない。
安全なフィールド除去手順:
- 表示 (deprecated): コードにコメント、ドキュメントに明記。
- 使用除去: すべてのwriterが該当フィールドを設定しないようにコード修正。
- 収集期間: 数週間~数ヶ月間、実際に使われていないか確認。
- Reader更新: readerコードでこのフィールド除去。
- Writer更新: writer schemaから除去。
- Reserved: Tagをreservedとして表示 (再利用防止)。
- モニタリング: このtag関連エラー追跡。
実戦助言:
- 追加は簡単で削除は難しい: 「後で消せばいい」と書いたフィールドは永遠に残る。
- 新バージョンよりフィールド追加: 壊れる変更より段階的追加。
- CI検証:
buf breaking、Schema Registry compatibility check。 - Graceful deprecation: 古いフィールドは「DEPRECATED」表示後数四半期間維持。
結論: 「削除」ボタンは簡単だがその結果は分散システムの複数層に及ぶ。スキーマ設計時に最初から拡張可能にするのが最善。削除が必要なら慎重なプロセスで進めるべき。
これが「スキーマ設計は一度決めると修正が難しい」という言葉の意味。いい加減に設計したスキーマは永遠の技術的負債になる。
Q5. いつJSONを使い続けるべきでいつバイナリに転換すべきか?
A. 単純な答えはない。複数の要因を考慮しなければならない。
JSONを使い続ける理由:
- 公開API: 外部開発者が使用。標準ツールでテスト可能でなければならない。
- 低トラフィック: スループットが少なければ最適化利点 < 複雑性コスト。
- 高速開発: スキーマ生成パイプライン設定の負担。
- デバッグ利便性: ログからそのまま読める。
- 多様なクライアント: JavaScriptブラウザ、シェルスクリプトなど。
- 小さなメッセージ: パースオーバーヘッドがネットワークに比べて微小。
バイナリに転換すべき信号:
- トラフィック増加: 日10億+リクエスト、帯域幅コスト無視できず。
- CPUボトルネック: プロファイリングでJSONパースが10%以上。
- 内部サービス: 公開APIではなくマイクロサービス間通信。
- スキーマ進化必要: バージョン互換性が重要に。
- 型安全性不足によるバグ: 「フィールド名のタイプミス」による障害。
- モバイル/IoT: 帯域幅とバッテリーが制限される。
具体的閾値:
JSON維持:
- 1日 < 100万リクエスト。
- 平均メッセージ < 10 KB。
- APIが外部公開。
- 開発チームが小規模。
MessagePack転換 (JSONの柔らかいアップグレード):
- JSON構造維持しながらサイズ/速度少し改善。
- スキーマなしでもOK。
- 少数の内部通信。
Protobuf転換 (本格バイナリ):
- マイクロサービスアーキテクチャ。
- gRPC使用。
- 型安全性とスキーマ進化必要。
- チームがtoolingを運用可能。
Avro転換 (ビッグデータ):
- Kafka中心。
- 長期保存データ。
- スキーマ進化が核心。
FlatBuffers (特殊ケース):
- ゲーム、モバイル、MLモデル。
- よく読むが稀にしか書かれない。
混合戦略 (最も一般的):
ほとんどの実戦システムは混合を使う:
- 外部API: JSON (REST)。
- 内部サービス間: Protobuf (gRPC)。
- メッセージキュー: Avro (Kafka)。
- DB保存: テキストまたはフォーマット別。
- 設定ファイル: YAML/JSON。
1つに統一しようとせず、各境界に合うものを選べ。
意思決定フレームワーク:
- 測定せよ: 現在のシステムでJSONが実際にボトルネックか?
- 比較せよ: ベンチマークを自分のワークロードで。
- 段階的転換: 1つのサービスから始める。
- 複雑性代価考慮: ツーリング、デバッグ、採用。
- 戻せるように: JSON互換性維持またはgatewayレイヤー。
よくある間違い:
- 「Protobufが速いという記事を読んで無条件導入」: 問題がなかったのに複雑性だけ追加。
- 「サイズが重要だからFlatBuffers」: サイズはFlatBuffersがむしろ大きい。Protobufがより小さい。
- 「JSONパースが遅い」: 実際のボトルネックでないことが多い。ネットワークやDBが本当の原因かもしれない。
- 「すべての所で同じフォーマット」: 公開APIにProtobuf強要 → 開発者離脱。
結論: 問題があるときに解決策があらねばならない。JSONで始め、実際の問題を測定した後、適切な境界でバイナリに転換せよ。「JSONが遅い」は神話で事実ではない。何百万もの成功したシステムがJSONの上で動いている。
これはエンジニアリングの一般原則と同じ: 単純さをデフォルトに、複雑性は証明された必要性によってのみ。バイナリシリアライゼーションは強力なツールだが、ツールを選ぶ判断がツール自体よりもっと重要。
おわりに: バイトの選択
核心整理
- Protobuf: 標準。RPC + 型安全性。
- Thrift: Protobufのいとこ。統合RPC。
- Avro: ビッグデータの標準。Schema evolution最強。
- MessagePack: JSONの柔らかい置換。
- FlatBuffers: Zero-copy。ゲーム/ML。
- Cap'n Proto: FlatBuffers + RPC。
- JSON: 依然として公開APIのデフォルト。
選択の核心
「どのフォーマットが最高か?」は間違った質問。「どのフォーマットがこの状況に最適か?」が正しい質問。
- RPC: Protobuf
- Kafka: Avro
- ゲーム: FlatBuffers
- 公開API: JSON
- ファイル保存: Avro (自己記述)
- IoT: CBOR/MessagePack
最後の教訓
シリアライゼーションフォーマットはデータの服。状況に合わせて着替えなければならない。間違って選べば一生苦労し、うまく選べば見えないところで数々の問題を予防する。
あなたが次に新しいシステムを設計するとき、ただ「JSON使おう」と言わないでおこう。質問しよう:
- どれだけのデータが流れるか?
- スキーマが変わるか?
- 誰が消費者か?
- 性能がボトルネックか?
- 型安全性が重要か?
この答えに従ってフォーマットを選べ。それがエンジニアリングの技術。
参考資料
- Protocol Buffers Language Guide (proto3)
- Protobuf Encoding Reference
- Apache Thrift Documentation
- Apache Avro Specification
- FlatBuffers Documentation
- Cap'n Proto
- MessagePack Specification
- buf: Modern Protobuf Tooling
- Confluent Schema Registry
- Designing Data-Intensive Applications, Ch.4 (Encoding)
- Martin Kleppmann: Thinking in Events
- Comparing Binary Formats: MessagePack, BSON, CBOR