大規模データ転送のボトルネック:なぜ「JSON」で消耗しているのか
Webシステムがスケールし、マイクロサービス間の連携やRedis等のインメモリキャッシュへの巨大なペイロード(数MB〜数十MBの配列やオブジェクト)の保存が日常茶飯事となった現代。開発現場で最も思考停止で選ばれがちなのが `json_encode()` と `json_decode()` だ。
「人間が読める」「言語非依存である」「デバッグが容易である」——これらの美徳は、こと高スループット・低レイテンシが要求されるバックエンドのデータパイプラインにおいては、単なる免罪符に過ぎない。
コードレビューでこう進言するジュニアエンジニアが後を絶たない。
「APIのレスポンスやRedisのキャッシュには、標準のJSONを使っています」
私はこう返す。
「そのJSON、PHPのZend VMとメモリ帯域をどれだけ無駄にしているか計算したことがあるか?」と。
今回は、PHPのシリアライズ形式の双璧である「JSON」と「igbinary」を低レイヤ(Zendエンジン、メモリ空間、CPUサイクル)から解剖し、大規模データ転送における真の勝者を決定する。
—
1. 内部構造の比較:JSON vs igbinary の生存戦略
まずは、PHPのメモリ空間上で、ひとつの巨大な連想配列がどのように扱われ、各フォーマットに変換されるのかのメカニズムを覗いてみよう。
JSON:テキスト化の代償とパースの重労働
JSONへのシリアライズは、PHP内部の複雑な `zval`(Zend Value)構造体を、一度人間が読めるUTF-8の文字列ストリームにエンコードする作業だ。
1. CPUバウンドな処理: すべてのキー名(文字列)や値が、文字列表現へと逐次変換される。たとえば整数型の `12345678` すら、ASCII文字の `”12345678″`(8バイト)へと変換される。
2. 無駄なメモリ重複: キャッシュから `json_decode($data, true)` を呼ぶたび、Zendメモリマネージャー(ZMM)は文字列をパースし、再びメモリ上に新しい `HashTable` と `zval` のツリーを構築し直す。キー名(例: `”user_id”`)のハッシュ計算とメモリ割り当てが、リクエストのたびに発生する。
igbinary:バイナリ形式とZendハッシュの直結
一方、PECL拡張である `igbinary` は、PHPのデータ構造(`zval`)をそのままコンパクトなバイナリ形式にシリアライズする。
1. キー名の重複排除(Dictionary Compression): igbinaryの真骨頂は、同一のリクエスト(あるいはシリアライズデータ内)で頻出するキー名(例: `”user_id”`, `”created_at”`)を一度だけ辞書登録し、以降はインデックス(小さな整数)で参照する点にある。これにより、連想配列特有の「キー名の文字列がメモリと転送量を圧迫する問題」が劇的に解決される。
2. 型情報の保持: 整数は整数のまま、浮動小数点数はそのままバイナリとしてパックされるため、デシリアライズ時の型推論やパースのオーバーヘッドがJSONに比べて圧倒的に少ない。
—
2. ベンチマークと実測:メモリ消費とCPU負荷の圧倒的乖離
百聞は一見に如かず。数万件のレコードを持つ巨大な多次元配列を想定し、JSONとigbinaryの挙動をシミュレートする実務的コードを見てみよう。
以下のコードは、実務の現場でRedisキャッシュや内部RPCを模したデータ転送における、メモリ消費量と実行時間を計測するためのリファレンス実装である。
/
function create_large_dataset(int $rows): array {
$data = [];
for ($i = 0; $i < $rows; $i++) {
$data[] = [
'id' => $i,
‘uuid’ => ‘a1b2c3d4-e5f6-7890-abcd-ef0123456789’,
‘username’ => ‘architect_user_’ . $i,
‘email’ => ‘user_’ . $i . ‘@example.com’,
‘metadata’ => [
‘login_count’ => $i 2,
‘is_active’ => ($i % 2 === 0),
‘roles’ => [‘ROLE_USER’, ‘ROLE_API’]
],
‘payload_blob’ => str_repeat(‘X’, 128) // ペイロードの肥大化を再現
];
}
return $data;
}
// 1. データの生成(約1万件)
$dataset = create_large_dataset(10000);
// — JSON方式 —
$startMemoryJson = memory_get_usage(true);
$startTimeJson = microtime(true);
$jsonString = json_encode($dataset, JSON_UNESCAPED_UNICODE);
$jsonDecoded = json_decode($jsonString, true);
$jsonTime = microtime(true) – $startTimeJson;
$jsonMemoryPeak = memory_get_peak_usage(true) – $startMemoryJson;
$jsonPayloadSize = strlen($jsonString);
// — IGBinary方式 —
// ※要 extension=igbinary
if (!extension_loaded(‘igbinary’)) {
throw new \RuntimeException(‘igbinary extension is not loaded.’);
}
// ガルベージコレクタを一度発動させ、公平なベースラインを作る
gc_collect_cycles();
$startMemoryIg = memory_get_usage(true);
$startTimeIg = microtime(true);
$igBinaryData = igbinary_serialize($dataset);
$igDecoded = igbinary_unserialize($igBinaryData);
$igTime = microtime(true) – $startTimeIg;
$igMemoryPeak = memory_get_peak_usage(true) – $startMemoryIg;
$igPayloadSize = strlen($igBinaryData);
// — 結果出力 —
echo “=== 性能比較結果 (10,000レコード) ===\n”;
echo sprintf(“JSON -> 処理時間: %.4f 秒 | ペイロードサイズ: %.2f MB | ピークメモリ差分: %.2f MB\n”,
$jsonTime, $jsonPayloadSize / 1024 / 1024, $jsonMemoryPeak / 1024 / 1024);
echo sprintf(“IGBinary-> 処理時間: %.4f 秒 | ペイロードサイズ: %.2f MB | ピークメモリ差分: %.2f MB\n”,
$igTime, $igPayloadSize / 1024 / 1024, $igMemoryPeak / 1024 / 1024);
echo sprintf(“\n[圧縮率 (vs JSON)] ペイロードは %.1f%% 削減されました。\n”,
(1 – ($igPayloadSize / $jsonPayloadSize)) 100);
このコードから読み解くべきアーキテクチャの真実
手元でこのスクリプトを実行すれば一目瞭然だが、データ構造が複雑化し、レコード数が数万件規模に達するほど、igbinaryの圧勝となる。
1. ペイロードサイズ(ネットワーク・ストレージ帯域):
JSONはキー名がレコードごとに繰り返しシリアライズされるため、ファイルサイズが肥大化する。一方、igbinaryは辞書圧縮の効果により、サイズがJSONの30%〜50%程度にまで圧縮されるケースが多い。これはRedisなどのネットワーク転送において、I/Oボトルネックを劇的に緩和する。
2. CPU負荷とシリアライズ速度:
C言語で実装されたigbinaryのシリアライザは、PHPのネイティブな内部構造(`zval`)をダイレクトにバイナリへマッピングするため、文字列をパース・構築するJSON(`ext/json` もC実装だがテキスト処理のコストが高い)に比べて、CPUサイクルを大幅に節約できる。
—
3. 実務で踏み抜く「致命的な地雷」と設計ルール
では、明日からすべてのデータをigbinaryに置き換えれば良いか? ——答えは「NO」である。
テックリードとして、以下のトレードオフとセキュリティ・互換性のリスクをチームに徹底しなければならない。
地雷1: 「外部公開API」でのigbinary利用
igbinaryはあくまでPHP内部(Internal)のシリアライズ形式である。外部のクライアント(iOS/Androidアプリ、フロントエンドのReact/Vue、サードパーティWebhook)はPHPの `zval` 構造など理解できないため、外部APIのレスポンスにigbinaryを使ってはならない。
- 適用境界のルール: 外部境界(HTTP Response, Public API)ではJSON。内部境界(Microservices間のgRPC/HTTP代替、Redisキャッシュ、PHPプロセス間のIPC)ではigbinaryと厳格に分離する。
地雷2: キャッシュの永続性とスキーマ変更
Redis等にigbinary形式で長期間データを保存する場合、PHPのバージョンアップやオブジェクトのクラス定義(クラスプロパティの変更など)によって、デシリアライズ時に予期せぬエラーや `__unserialize()` の挙動不整合を引き起こすリスクがある。
- 設計ルール: キャッシュストレージに格納するデータは、極力オブジェクトではなく、シリアライズ・デシリアライズの親和性が高い「純粋な連想配列(Array)」の形で扱うこと。
—
4. コピペで使える堅牢なラッパー実装
実務において、環境によってigbinaryが入っていないフォールバックを考慮したり、シリアライズ処理を散在させたりするのは悪しき設計(Anti-Pattern)である。
以下の `SerializerInterface` に準拠した安全かつ洗練されたコンポーネントクラスをプロジェクトに導入せよ。
/
final class OptimizedPayloadSerializer implements SerializerInterface {
private bool $useIgbinary;
public function __construct(bool $preferIgbinary = true) {
$this->useIgbinary = $preferIgbinary && extension_loaded(‘igbinary’);
}
public function serialize(mixed $data): string {
try {
if ($this->useIgbinary) {
// igbinary_serialize は失敗時に false を返すことがある
$result = igbinary_serialize($data);
if ($result !== false) {
return $result;
}
}
} catch (\Throwable $e) {
// ログ基盤への転送などをここに記述(例: Log::warning(…))
}
// フォールバック: 標準JSON(フラグは厳格に設定)
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
// JSONであることを識別するためのヘッダプレフィックスを付与する場合もある
return $json;
}
public function unserialize(string $payload): mixed {
if ($payload === ”) {
return null;
}
// ペイロードがigbinary形式か、JSON形式かを安全に判定する、
// あるいはシステム全体でフォーマットを統一している場合は直接分岐する。
if ($this->useIgbinary && $this->isIgbinary($payload)) {
$unserialized = igbinary_unserialize($payload);
if ($unserialized !== null || $payload === igbinary_serialize(null)) {
return $unserialized;
}
}
// JSONとしてのパースを試みる
try {
return json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException(‘Failed to unserialize payload: ‘ . $e->getMessage(), 0, $e);
}
}
/
- 簡易的なigbinaryバイナリ判定(先頭バイトのシグネチャ確認)
- igbinary v2/v3 のヘッダ構造に依存
/
private function isIgbinary(string $payload): bool {
// igbinaryのペイロードは特定のマジックバイトで始まるか、
// json_decodeで例外になるかで判定するのが実務的。
// ここでは簡略化のため、json_decodeが失敗するかどうかをハンドリングする。
json_decode($payload);
return json_last_error() !== JSON_ERROR_NONE;
}
}
—
5. アーキテクトからの最終提言
パフォーマンスチューニングの本質は、「無駄なCPUサイクルとメモリ割り当てを削ぎ落とすこと」に他ならない。
大規模なデータを扱うWebシステムにおいて、すべての通信やキャッシュを「人間都合のJSON」で行うことは、エンジニアの怠慢であり、インフラコストの無駄遣いである。
システムの内側で流れるデータ、特にRedisやMemcached、内部API間での巨大なペイロードには `igbinary` を採用し、PHPのZendエンジンが持つポテンシャルを極限まで引き出せ。
コードレビューでこの設計思想を示せたとき、あなたのチームのプロダクトは、次のステージへとスケールする耐性を手に入れているはずだ。