Schema Registry - 怖くないSchema Evolution

# はじめに
[Confluent Schema Registry](https://github.com/confluentinc/schema-registry)はイベントの型 (Schema) を集中管理する仕組みです。本来イベントはスキーマと合わせてApache Kafkaに送られますが、Schema Registryを利用すると：

1. Schemaをメッセージに含めずIDで指定出来る為サイズを大幅に小さく出来る。
- Schemaをバージョン管理できる。
- 互換性の無いSchema変更を遮断する制御ができる。

といった利点があります。本エントリでは特に#3、Schemaの変更互換性とバージョンアップ (Schema Evolution) と上手く付き合う方法について説明します。

### 実際に試すには
Confluent CloudではマネージドサービスにSchema Registryが含まれているのでそのまま[お試しいただけます](https://docs.confluent.io/ja-jp/cloud/current/sr/schemas-manage.html)。ローカルで試したいという方はConfluentが提供しているデモシナリオ (Docker Compose) をお勧めします。手順は[こちら](https://docs.confluent.io/platform/current/quickstart/ce-docker-quickstart.html#step-1-download-and-start-cp)をどうぞ。

# Schema Registry
Schema RegistryはREST APIを備えたシンプルなメタデータストアです。
Kafka自体はイベントをそのままバイトで扱うことによって転送/保全負荷を下げていますが、その分イベントのProducerとConsumerがそれなりに多くの責務を担っています。イベントのSchema自体もKafka Brokerは意識しない為、何かしらの形でイベントのスキーマをProducerからConsumerに伝えないといけません。最も簡単な方法はメッセージにSchema自体を埋め込んで送信する方法です。Schema Registryを利用するとメッセージ内ではSchemaをIDで扱えます。
![schema-registry-and-kafka.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213933/64e4d87e-8621-cb56-da3c-e5d797e460a3.png)

1. Producer/Consumer側双方でSchema Registryの登録（PropertyにSchema RegistryへのURLを指定）
1. ProducerのSerializerがSchemaをSchema Registryに登録。代わりにIDが採番がされ返される。
1. ProducerがそのIDをメッセージに含み送信。
1. ConsumerのDeserializerがIDを元にSchema RegistryからSchemaを取得しデシリアライズ。

SchemaとそのIDはProducer/Consumer側にキャッシュされるので、以降のコミュニケーションではIDのみで通信されます。またProducer/ConsumerのProperty設定以外は全て自動で実施されるので、開発者として意識する必要はありません。

# Schema Evolution
DBのテーブル定義やAPI同様、Schemaも時間を経過して変わっていくものです。イベント駆動の様な非同期なアプローチでは、通信時に厳密な型が不要という柔軟性はありますが、このアプローチでもConsumer側が扱えない型変更をProducer側で行うとシステム/業務的に影響を及ぼします。このSchema変更を明示的に管理する事をSchema Evolutionと呼びます。

具体的には、Producer側から新たなSchemaバージョンが送られてきた際に、指定された互換性設定に準拠しているかをSchema Registryがチェックすることにより管理します。チェックに合格しない変更はSchema Registryが拒否することによりメッセージの送信自体を止めて事前に障害を防ぎます。

互換性パターンには3つ存在し、それぞれによってProducer/Consumerのいずれの変更デプロイを先に行うかが決まります。Topicに対して複数のProducer/Consumerがある想定です。

__Backward Compatibility (デフォルト)__ - 旧Consumerが新Schemaを処理できない - Consumerを先に更新
__Forward Compatibility__  - 新Consumerが旧Schemaを処理できない - Producerを先に変更
__Full Compatibility__  - 新旧いずれのConsumerも新旧いずれのSchemaを処理できる - どちらを先に変更してもOK

指定したCompatibilityに則しているかの確認はSchema Registryの役目で、そのCompatibilityに倣った順序でデプロイするのが開発者の役目です。

# 理屈から考えるBackward/Forward/Full Compatibility
Schema互換性はややとっつきにくいのですが、その互換性を担保しないとどういう問題が起こるのかという観点で見ると：

__Backward Compatibility__ - Schemaに新しい必須フィールドがある or 既存必須フィールドが変更されている
__Forward Compatibility__ - Schemaにあるはずの必須フィールドがない or 変更されている
__Full Compatibility__ - Schemaの必須フィールドに変更がない

つまり重要なのは必須フィールド (Optionalではないフィールド) の扱いで互換性が決まるという点です。

また、必須フィールドの定義変更はいずれの互換性も失います。 Producer/Consumerの一斉デプロイ (ダウンタイムが発生する) か、アプリ内での分岐処理 (このSchema Evolutionの為だけに必要なコードロジック) が必要となる影響の大きい変更です。
__Backward Compatibility__ - 新規必須フィールド追加 or 既存必須フィールドを変更するとアウト
__Forward Compatibility__ - 既存必須フィールドの削除 or 変更するとアウト
__Full Compatibility__ - 必須フィールドに変更が加わるとアウト

これら互換性は現在とその次のバージョンに着目したモードですが、それぞれにはTransitiveという全バージョンを通して互換性を指定するモード (`BACKWARD_TRANSITIVE`, `FORWARD_TRANSITIVE`, `FULL_TRANSITIVE`) もありますが考え方は同じです。

# おわりに
制約とは互換性を担保する1つの方法であり、Schema Registryを利用する事も1つのアプローチに過ぎません。何か魔法の様な仕組みがある訳ではなく、極めて明確な存在意義があり、また作法というか考え方を理解しなければいけません。

しかしながら、この手の型の厳格な定義が必要となるケースは多くあり、特に組織内での利用が部門横断的なものへと広がる際には必要となります。スケールに伴う課題を、運用でカバーするのではなく体系的に取り組む際にはSchema Evolutionは強力な武器となります。イベントのSchema管理だけで全てが解決する訳ではありませんが、まずはSchema Evolutionと馴染み、ルール整備の基礎の一つ選択肢と捉えていただければと思います。