Skip to content

✍️ 필사 모드: バイナリシリアライゼーション完全ガイド 2025: Protobuf, Thrift, Avro, MessagePack, FlatBuffers, Cap'n Proto — 性能 vs 柔軟性の選択

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに: 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サポート。

この記事で比較するもの

  1. Protocol Buffers (Google): 最も広く使われる標準。
  2. Thrift (Facebook/Apache): RPC統合。
  3. Avro (Apache Hadoop): ビッグデータの事実上の標準。
  4. MessagePack: JSONのバイナリ置換。
  5. FlatBuffers (Google): Zero-copy読み込み。
  6. 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の補数で変換されるため):

-10xFFFFFFFF = 10バイト!

解決: sint32、sint64型ZigZagエンコーディングを使う:

 00
-11
 12
-23
 24
...

公式: (n << 1) ^ (n >> 31)

負数と正数が交互にマッピングされ、絶対値が小さいほどエンコーディングが短くなる

Schema Evolution

Protobufはforward/backward compatibilityをサポート:

ルール:

  1. Tag numberを絶対に変えない: tagはID。
  2. フィールド名変更OK: wire formatと無関係。
  3. 新フィールド追加OK: 古いクライアントは無視。
  4. フィールド削除OK、ただしtagをreservedに: 再利用防止。
  5. 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つのモード:

  1. Container File Mode: ファイルヘッダにschemaを含む。
  2. 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つはビッグデータストリーミングのゴールデンコンビネーション:

  1. Kafka: メッセージブローカ。
  2. Avro: 効率的エンコーディング + schema evolution。
  3. Schema Registry: 中央スキーマ管理 + 互換性検証。

Confluent Platformのデフォルトアーキテクチャ。Netflix、LinkedIn、Uberなど全員採用。

Protobuf vs Avro

項目ProtobufAvro
スキーマ必要時点コード生成時書き/読み時
Field identificationTag 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以上。

使用先

  1. Redis: Luaスクリプト結果、一部内部通信。
  2. Fluentd: ログ収集プロトコル。
  3. ZeroMQ: メッセージバインディング。
  4. ゲーム: リアルタイムネットワークプロトコル。

Protobufとの違い

項目MessagePackProtobuf
スキーマなし必須
サイズ
パース速度速い非常に速い
自己記述はいいいえ
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]...

読むとき:

  1. バイトバッファを取得 (memcpyまたはmmap)。
  2. ルートoffsetに従う。
  3. オブジェクト割り当てなしに直接フィールドアクセス
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%大きい。

使用先

  1. ゲームエンジン: Cocos2d、各種ゲームデータ。
  2. モバイルアプリ: データファイルフォーマット。
  3. Apache Arrow: 一部フォーマット共有。
  4. Facebook: 主要ユーザ。
  5. 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。

ClientA: getUser(1)
ClientA: (promise of user)B.emailSummary(user)

AとBの間で直接データが流れる。Clientが間に入らない。レイテンシ削減

使用先

FlatBuffersほど使われていないが、Cloudflareのような大企業が内部で使用。

FlatBuffers vs Cap'n Proto

項目FlatBuffersCap'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フィールド、ネスト含む):

フォーマットサイズシリアライズデシリアライズ
JSON5.2 KB85 us120 us
BSON4.8 KB75 us105 us
MessagePack3.8 KB45 us60 us
Protobuf2.8 KB15 us25 us
Avro2.6 KB20 us35 us
Thrift (Compact)2.7 KB18 us30 us
Cap'n Proto3.2 KB10 us1 us
FlatBuffers3.5 KB12 us1 us

主要観察:

  1. サイズ: Avro、Thrift、Protobuf、FlatBuffers、MessagePack、JSONの順に大きくなる。
  2. シリアライズ速度: すべてJSONより速い。
  3. デシリアライズ: Cap'n Proto/FlatBuffersが圧倒的 (zero-copy)。

実戦スループット

1 GBデータ処理 (100 GBシステムメモリ):

フォーマット時間
JSON120 s
MessagePack60 s
Protobuf25 s
Avro (columnar compression)30 s
FlatBuffers2 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ルール:

  1. フィールドマッチング: 名前基準 (aliasesサポート)。
  2. ないフィールド: default値使用。
  3. 型promotion: intからlongへ、floatからdoubleへ。
  4. Union進化: null追加可能。

Avroは最も強力なevolutionシステム。複雑な変更も可能。

JSON

スキーマなし → 評価が難しいが事実上何でも可能。

  • 新フィールド追加: 簡単 (クライアントが無視)。
  • フィールド削除: 危険 (クライアントが依存する可能性)。
  • 型変更: 危険。

JSON Schemaを使えば検証可能だが、コードレベルの自動互換性はない。

FlatBuffers / Cap'n Proto

Tagベース。Protobufに類似のルール。ただし、zero-copyのためフィールド再配置禁止

実戦ルール

  1. Tag/フィールド番号は永遠: 絶対に再利用しない。
  2. 追加のみ: 削除より追加が安全。
  3. Optionalをデフォルト: 新フィールドはoptionalで。
  4. Default値: 明示的に設定。
  5. Deprecated表示: コードコメントで廃止予定を通知。
  6. 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つの運用モード:
    1. Container file: ファイルヘッダにスキーマ含む。
    2. Schema Registry: メッセージにschema IDのみ、実際のスキーマは中央で。

なぜこの違い?

Avroはビッグデータとストリーミングのために設計された。この環境の特性:

  • データが長く生きスキーマが進化。
  • 消費者が未来で誰なのか分からない
  • Writer schema ≠ Reader schemaが日常。

Avroの利点:

  1. Writer vs Reader schema resolution:

    • Avroは両スキーマを受け取り自動変換。
    • Protobufはtagベースで片側だけ分かればOK。
    • 複雑なマイグレーションでAvroがより柔軟。
  2. 自己記述ファイル:

    • Avro container fileはスキーマを含むので「どんなツールでも」読める。
    • Protobufは.protoファイルがなければ読めない。
  3. 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計算、直接メモリ読み取り

動作原理:

  1. バッファ開始にroot offset保存。
  2. 各オブジェクトの前にvtable offset (可変構造処理)。
  3. Vtable: 「フィールドXはoffset Yにある」という情報。
  4. フィールドアクセスはoffset加算 + メモリ読み取り。O(1)。

利点:

  1. デシリアライズ時間: 0 (バッファマッピング時間のみ)。
  2. メモリ割り当てなし: 既存バッファ再利用。
  3. ランダムアクセス: 1 GBファイルでフィールド1つだけ必要ならその部分のみロード。
  4. mmapフレンドリ: 大きなファイルも怠惰にロード。
  5. Cache効率: 一度読めばCPUキャッシュに残り再利用。

コスト:

  1. サイズ: vtableオーバーヘッドでProtobufより20~50%大きい。
  2. シリアライズ複雑: builderで段階的に構成。
  3. 柔軟性低: 一度作ったバッファを修正しにくい。
  4. デバッグ: バイナリ構造を直接見にくい。

いつ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契約変更。すべての使用箇所を追跡しなければならない。

安全なフィールド除去手順:

  1. 表示 (deprecated): コードにコメント、ドキュメントに明記。
  2. 使用除去: すべてのwriterが該当フィールドを設定しないようにコード修正。
  3. 収集期間: 数週間~数ヶ月間、実際に使われていないか確認。
  4. Reader更新: readerコードでこのフィールド除去。
  5. Writer更新: writer schemaから除去。
  6. Reserved: Tagをreservedとして表示 (再利用防止)。
  7. モニタリング: このtag関連エラー追跡。

実戦助言:

  • 追加は簡単で削除は難しい: 「後で消せばいい」と書いたフィールドは永遠に残る。
  • 新バージョンよりフィールド追加: 壊れる変更より段階的追加。
  • CI検証: buf breaking、Schema Registry compatibility check。
  • Graceful deprecation: 古いフィールドは「DEPRECATED」表示後数四半期間維持

結論: 「削除」ボタンは簡単だがその結果は分散システムの複数層に及ぶ。スキーマ設計時に最初から拡張可能にするのが最善。削除が必要なら慎重なプロセスで進めるべき。

これが「スキーマ設計は一度決めると修正が難しい」という言葉の意味。いい加減に設計したスキーマは永遠の技術的負債になる。

Q5. いつJSONを使い続けるべきでいつバイナリに転換すべきか?

A. 単純な答えはない。複数の要因を考慮しなければならない。

JSONを使い続ける理由:

  1. 公開API: 外部開発者が使用。標準ツールでテスト可能でなければならない。
  2. 低トラフィック: スループットが少なければ最適化利点 < 複雑性コスト。
  3. 高速開発: スキーマ生成パイプライン設定の負担。
  4. デバッグ利便性: ログからそのまま読める。
  5. 多様なクライアント: JavaScriptブラウザ、シェルスクリプトなど。
  6. 小さなメッセージ: パースオーバーヘッドがネットワークに比べて微小。

バイナリに転換すべき信号:

  1. トラフィック増加: 日10億+リクエスト、帯域幅コスト無視できず。
  2. CPUボトルネック: プロファイリングでJSONパースが10%以上。
  3. 内部サービス: 公開APIではなくマイクロサービス間通信。
  4. スキーマ進化必要: バージョン互換性が重要に。
  5. 型安全性不足によるバグ: 「フィールド名のタイプミス」による障害。
  6. モバイル/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つに統一しようとせず、各境界に合うものを選べ。

意思決定フレームワーク:

  1. 測定せよ: 現在のシステムでJSONが実際にボトルネックか?
  2. 比較せよ: ベンチマークを自分のワークロードで。
  3. 段階的転換: 1つのサービスから始める。
  4. 複雑性代価考慮: ツーリング、デバッグ、採用。
  5. 戻せるように: JSON互換性維持またはgatewayレイヤー。

よくある間違い:

  • 「Protobufが速いという記事を読んで無条件導入」: 問題がなかったのに複雑性だけ追加。
  • 「サイズが重要だからFlatBuffers」: サイズはFlatBuffersがむしろ大きい。Protobufがより小さい。
  • 「JSONパースが遅い」: 実際のボトルネックでないことが多い。ネットワークやDBが本当の原因かもしれない。
  • 「すべての所で同じフォーマット」: 公開APIにProtobuf強要 → 開発者離脱。

結論: 問題があるときに解決策があらねばならない。JSONで始め、実際の問題を測定した後、適切な境界でバイナリに転換せよ。「JSONが遅い」は神話で事実ではない。何百万もの成功したシステムがJSONの上で動いている。

これはエンジニアリングの一般原則と同じ: 単純さをデフォルトに、複雑性は証明された必要性によってのみ。バイナリシリアライゼーションは強力なツールだが、ツールを選ぶ判断がツール自体よりもっと重要。


おわりに: バイトの選択

核心整理

  1. Protobuf: 標準。RPC + 型安全性。
  2. Thrift: Protobufのいとこ。統合RPC。
  3. Avro: ビッグデータの標準。Schema evolution最強。
  4. MessagePack: JSONの柔らかい置換。
  5. FlatBuffers: Zero-copy。ゲーム/ML。
  6. Cap'n Proto: FlatBuffers + RPC。
  7. JSON: 依然として公開APIのデフォルト。

選択の核心

「どのフォーマットが最高か?」は間違った質問。「どのフォーマットがこの状況に最適か?」が正しい質問。

  • RPC: Protobuf
  • Kafka: Avro
  • ゲーム: FlatBuffers
  • 公開API: JSON
  • ファイル保存: Avro (自己記述)
  • IoT: CBOR/MessagePack

最後の教訓

シリアライゼーションフォーマットはデータの服。状況に合わせて着替えなければならない。間違って選べば一生苦労し、うまく選べば見えないところで数々の問題を予防する。

あなたが次に新しいシステムを設計するとき、ただ「JSON使おう」と言わないでおこう。質問しよう:

  • どれだけのデータが流れるか?
  • スキーマが変わるか?
  • 誰が消費者か?
  • 性能がボトルネックか?
  • 型安全性が重要か?

この答えに従ってフォーマットを選べ。それがエンジニアリングの技術


参考資料

현재 단락 (1/756)

REST APIを作るとき、ほとんどの開発者が自然に選ぶもの:

작성 글자: 0원문 글자: 23,160작성 단락: 0/756