Kafka

Kafka Schema Evolution の実務: 後方互換性を守る書き換え

2026-04-19
NicheeLab編集部

後方互換性を守るとは、過去に書かれたメッセージ(旧Writerスキーマ)を、新しいReaderスキーマで正しく読める状態を保つこと。Confluent Schema Registry の BACKWARD(特に TRANSITIVE)設定が軸になります。

本稿はCCDAKの出題範囲に合わせ、用語の正確さと、現場で手が動く具体策(安全に追加・改名・段階的廃止)に重点を置いています。

用語と互換性レベルの要点(CCDAK頻出)

後方互換性は、新しいReaderスキーマ(主に新コンシューマ側)が、古いWriterスキーマで書かれたデータを読む保証です。Confluent Schema Registry の BACKWARD はこの性質を指し、TRANSITIVE を付けると全履歴の全バージョンに対して保証します。

一方で、既存のコンシューマを壊さずに新しいProducerを出したい場合は前方互換性(FORWARD)が関係します。FULL は BACKWARD と FORWARD の両方を同時に満たす必要があり、保守性は高い一方で許される変更の幅は狭くなります。実務ではロールアウト順序(先にコンシューマ更新か、先にプロデューサ更新か)に応じて使い分けます。

Avro のリーダー/ライター解決則に基づき、追加フィールドのデフォルト、数値の型拡張、別名(aliases)などが互換性維持に役立ちます。Protobuf や JSON Schema でも原理は似ていますが、細部は異なります(例えば Protobuf はフィールド番号が本質)。

  • BACKWARD(TRANSITIVE推奨): 新Readerが旧Writerのデータを読める
  • FORWARD(TRANSITIVE推奨): 旧Readerが新Writerのデータを読める
  • FULL: 両方を同時に満たす(変更は最も制約される)
  • ロールアウト順序で使い分けるのが実務の定石
互換性レベル新旧の読み書き保証代表的に安全な変更例
BACKWARD_TRANSITIVE新Readerが全履歴の旧Writerを読めるフィールド追加(デフォルト必須)、enumシンボル追加、数値型の拡張(int→long/float/double等)、フィールド並び替え、フィールド名変更+aliases
FORWARD_TRANSITIVE旧Readerが全履歴の新Writerを読めるフィールド追加(旧Readerが無視できる形)、デフォルト維持、既存必須フィールドの削除は不可
FULL_TRANSITIVE双方が全履歴で相互読取可能追加は厳格(デフォルト必須)、破壊的変更は段階移行で吸収

後方互換性(BACKWARD_TRANSITIVE)の流れ

Avro Sv0register Sv1reads old Sv0 w/ defaultsProducer(Sv0)Kafka TopicSchema RegistrySubject: t-value / Compat: BACKWARD_TRANSITIVEConsumer(Sr1)

互換性レベルの設定(例: BACKWARD_TRANSITIVE)

curl -s -X PUT http://localhost:8081/config/t-value \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"compatibility": "BACKWARD_TRANSITIVE"}'

# グローバル既定を変える場合(推奨はSubject単位での設定)
# curl -s -X PUT http://localhost:8081/config -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"compatibility": "BACKWARD_TRANSITIVE"}'

後方互換性を守る基本パターン

最も安全で頻出なのは、フィールドの追加です。Avro では新たなフィールドにデフォルト値を必ず付けます。旧Writerがそのフィールドを持たない場合でも、新Readerはデフォルトで補完して読み取れます。

数値型の拡張(int→long/float/double、long→float/double、float→double)はAvroの解決則上、Reader側の後方互換で有効です。enum も新しいシンボルを追加するのは後方互換ですが、削除や並び替えは注意が必要です。

フィールド名変更は直接は破壊的ですが、Avro の aliases を使えば旧名で書かれたデータを新名のフィールドにマッピングできます。Protobuf ではフィールド名よりも番号が本質なので、番号を変えない限り改名は安全です。

  • フィールド追加はデフォルト必須(Avro)。デフォルト未指定は互換性違反になりやすい
  • 型拡張はReader側でのみ安全に働く(後方互換の文脈)
  • enum は追加は可、削除・置換は不可に近い
  • 改名は aliases(Avro)または番号維持(Protobuf)で吸収

Avro スキーマの追加・改名(aliases)例

// 旧スキーマ(Sv0)
{
  "type": "record",
  "name": "Order",
  "namespace": "com.example",
  "fields": [
    {"name": "id", "type": "string"},
    {"name": "amount", "type": "int"},
    {"name": "status", "type": {"type":"enum","name":"Status","symbols":["NEW","PAID"]}}
  ]
}

// 新スキーマ(Sr1, BACKWARD互換):フィールド追加+改名
{
  "type": "record",
  "name": "Order",
  "namespace": "com.example",
  "fields": [
    {"name": "id", "type": "string"},
    {"name": "amount", "type": "long"},               // Reader側の型拡張
    {"name": "state", "type": "string", "aliases": ["status"], "default": "NEW"},
    {"name": "coupon", "type": ["null","string"], "default": null} // 新規追加
  ]
}

# 互換性チェック(最新と比較)
curl -s -X POST http://localhost:8081/compatibility/subjects/t-value/versions/latest \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"schema": "{ \"type\": \"record\", \"name\": \"Order\", ... }"}'

破壊的変更を安全に進める段階的移行

どうしても破壊的に見える変更(必須フィールド削除、型の縮小、enum削除、Protobufのフィールド番号変更など)は、一度にやらず段階的に分解します。まずは後方互換な形で併存させ、消費者・生産者の順に切替え、最後に不要部分を除去します。

移行中は Registry の互換性を BACKWARD_TRANSITIVE で固定し、Producer 先行のフェーズが必要な場合のみ短期に FORWARD 系を使います。FULL は両方向を同時に満たす必要があるため、変更幅がさらに狭まる点に留意します。

  • ステップ1: 新フィールドを追加(デフォルト付与)し、旧フィールドは残す(非推奨フラグなど)
  • ステップ2: コンシューマを新フィールド参照に切替(旧フィールドはフォールバック)
  • ステップ3: プロデューサを新フィールドのみ書き込みへ移行(必要なら二重書き込み期間を設ける)
  • ステップ4: 全てのコンシューマ更新完了後、旧フィールドを削除

互換性ガードを効かせた登録フロー(擬似)

# 1) 互換性は原則 BACKWARD_TRANSITIVE
curl -s -X PUT http://localhost:8081/config/t-value -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"compatibility":"BACKWARD_TRANSITIVE"}'

# 2) 登録前に必ずドライラン確認
curl -s -X POST http://localhost:8081/compatibility/subjects/t-value/versions/latest \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"schema": "...new schema..."}'

# 3) 問題なければ登録
curl -s -X POST http://localhost:8081/subjects/t-value/versions \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"schema": "...new schema..."}'

Subject 命名戦略の選択と影響

互換性は Subject 単位で評価されます。TopicNameStrategy(例: topic-name-value)が一般的で、トピックごとに互換性を独立管理できます。レコードを複数トピックで共有したい場合は RecordNameStrategy を使うと、同一レコード名の互換性履歴を共有できますが、想定外の衝突には注意が必要です。

マルチトピック配信やKafka Connect/ksqlDBを交える場合、どのSubjectに登録されるか(valueかkeyか、トピック駆動かレコード駆動か)を事前に確認し、互換性レベルと移行計画を合わせます。

  • TopicNameStrategy: 運用直感的、トピック単位で分離
  • RecordNameStrategy: スキーマ再利用に有利、衝突時の影響範囲が広い
  • TopicRecordNameStrategy: 折衷案。衝突リスクを下げつつ再利用性を確保

Kafka Avro Serializer の Subject 名戦略指定(例)

props.put("value.subject.name.strategy", "io.confluent.kafka.serializers.subject.TopicNameStrategy");
// 例: RecordNameStrategy を使う場合
// props.put("value.subject.name.strategy", "io.confluent.kafka.serializers.subject.RecordNameStrategy");

実務フロー: チェックリストとCI/CDゲート

スキーマの変更はコードレビューと同じ強度でゲートするのが安全です。事前に Registry 互換性APIでドライランチェックをかけ、テストデータで旧バージョンのメッセージを読み出す回帰テストを自動化します。スキーマの正規化(フィールド順やスペース差異の吸収)を前提に比較することで、ノイズの少ないレビューができます。

本番では互換性レベルをSubject単位で明示し、例外的に緩める必要がある場合は期間・影響・ロールバック手順をチケット化します。監査用にスキーマIDとGitコミットの対応表を残すのも有効です。

  • 互換性APIのドライランをCIで必須化
  • 旧データ再生テスト(読み取り側の回帰)
  • Subjectごとに互換性レベルを明示管理
  • スキーマIDとリリースタグの対応表を残す

CIでの簡易ドライラン例(Bash)

SUBJECT="t-value"
NEWSCHEMA_FILE="schema_new.avsc"
BODY=$(jq -Rs '{schema: .}' < "$NEWSCHEMA_FILE")

curl -s -X POST "http://localhost:8081/compatibility/subjects/${SUBJECT}/versions/latest" \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d "$BODY" | jq .

コンシューマ実装の注意点と運用

後方互換の鍵はコンシューマ側の堅牢さです。新しいReaderスキーマにデフォルトを定義し、未知フィールドは無視、null許容の扱いは明示的に分岐します。ログ圧縮トピックでは tombstone(value=null)を受ける可能性があるため、Avro/JSON Schema/Protobuf いずれでも null安全を確保してください。

GenericRecord なら存在チェックとデフォルト適用、SpecificRecord なら生成コードのデフォルトに依存します。エラー時にはスキップよりも死信キューやリトライキューに退避して、スキーマやデータの不一致を可観測化するのが有効です。

  • nullと未設定の区別を曖昧にしない
  • デフォルトをスキーマ側に必ず持たせる
  • 例外時はDLQで隔離し原因を可視化
  • 再処理(replay)計画を用意する

Avro GenericRecord の慎重な読み取り例(Java, 概略)

GenericRecord r = consumerRecord.value();
Long amount = null;
if (r.getSchema().getField("amount") != null) {
  Object v = r.get("amount");
  if (v instanceof Integer) amount = ((Integer) v).longValue();
  else if (v instanceof Long) amount = (Long) v;
}
String state = (String) (r.get("state") != null ? r.get("state") : "NEW");
// coupon は optional
String coupon = r.get("coupon") == null ? null : r.get("coupon").toString();

問題で確認

CCDAK

問題 1

Avro の value スキーマに新しいオプションフィールドをデフォルト付きで追加します。まずコンシューマを新Readerスキーマに更新し、旧データも継続して読みたい。Schema Registry の Subject に設定すべき互換性レベルとして最も適切なのはどれか(履歴全体を対象)?

  1. BACKWARD_TRANSITIVE
  2. FORWARD
  3. NONE
  4. FULL

正解: A

新しいReaderが過去のWriterで書かれた全履歴を読める保証が必要なため BACKWARD_TRANSITIVE が最適です。FORWARD は旧Readerが新Writerを読める保証であり順序が逆、NONE は無保証、FULL は双方向で制約が厳しく本件には不要です。

よくある質問

後方互換(BACKWARD)と前方互換(FORWARD)の使い分けは?

先にコンシューマを更新する計画ならBACKWARD(新Readerが旧Writerを読む)を選び、先にプロデューサを更新する計画ならFORWARD(旧Readerが新Writerを読む)を選びます。両方向を同時に満たしたい特殊なケースでのみFULLを検討します。

フィールドの改名はどう行えば安全ですか?

Avroでは新フィールド名にaliasesで旧名を登録します。段階的にコンシューマを新名へ切替え、十分な期間の後に旧名を持つフィールドを削除します。Protobufはフィールド番号が本質なので番号を変えずに名前だけを変えるのが安全です。

enum の変更で気をつける点は?

新しいシンボルの追加は後方互換ですが、既存シンボルの削除・置換は多くの互換性モードで失敗します。意味変更も実質的に破壊的です。追加のみで進め、不要になった値はアプリ側で非推奨扱いに留めるのが無難です。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
Kafka

Kafka Topic と Partition の基礎: 分散とスケーラビリティの要

CCDAK 対策と実務の両立を意識し、Topic/Partition/Replica/Consumer Group の役...

Kafka

CCDAK 試験ガイド:出題範囲・配点・申込み・対策

Confluent Certified Developer for Apache Kafka (CCDAK) の出題範囲...

Kafka

Confluent Certified Administrator (CCAAK) 対策: 出題範囲・配点の考え方・運用観点の要点

CCAAKに向けて、試験領域の押さえどころを運用目線で整理。プロダクションで通用する設定・監視・セキュリティの実践知を、...

Kafka

Kafka の Replica と In-Sync Replicas を正しく設計する: 耐障害性と一貫性

レプリカとISRの仕組みを起点に、acks と min.insync.replicas、クリーン/アンクリーンリーダー選...

Kafka

Kafka の Offset とコミット: ポジション管理と at-least-once の基礎

CCDAK 対策と実務の両立を意識して、Kafka コンシューマのオフセット管理とコミット戦略を整理。at-least-...

Kafkaの記事一覧 (101件)
© 2026 NicheeLab All rights reserved.