TT Lab
はじめる
学ぶ 学習パス コース

データパイプライン

スキーマは変わる — どちらが先に変わっても壊れないように

TT Labで続きを見る

一言でいうと

スキーマ変更の本当の問いは、「何を変えるか」ではなく、書き込む側と読み取る側のどちらが先に変わっても壊れないかであり、その答えは、デフォルト値・エイリアス・型のプロモーションの3つで決まります。

なぜ必要なのか

パイプラインが出力するイベントに、フィールドを1つ加える必要があります。スキーマファイルを直して、生産する側をデプロイします。その日の夜に、ダウンストリーム3か所が止まります。そのうちの1つは、私たちのチームが存在することも知らなかったチームです。

次は慎重に、読み取る側を先に直して、書き込む側をあとでデプロイします。今度は、読み取る側が新しいフィールドを見つけられずに止まります。まだ誰もそのフィールドを使っていないからです。

2つの事故は、同じ原因です。デプロイは一瞬では起こりません。書き込む側と読み取る側の間には、必ず片方だけが新しいコードである期間があり、その期間にもデータは流れ続けます。そのため、変更を設計するときに問うべきことは、「この変更は正しいか」ではなく、この変更を、どの順序でデプロイしても生き残るかです。

このコースの前のモジュールと分ける場所が、ここです。他人が渡してくるファイルが、通知なしに変わったことを検知するのは、受け取る側の防御です。ここは、変える側が私たちで、私たち自身がバージョン番号を付けて変えながら、古いデータと古い読み取りコードを同時に生かしておくことです。

どう動くのか

2つの方向を、名前で分けて呼びます。Confluentの互換性ドキュメントが使っている名前を、そのまま使います。

判定ルールは、Avro仕様のスキーマ解決にすでに書かれています。3行がすべてです。

この3行から、実務のルールがそのまま出ます。デフォルト値のあるフィールドを加えることは、両方向に安全です。読み取る側が古いデータに出会えば、デフォルト値で埋め、古い読み取りコードは、新しいフィールドを捨てるだけでよいからです。逆に、デフォルト値のない必須フィールドを加えることは、後方互換を壊します。古いデータには、その値がそもそもなく、読み取る側には、埋める方法がないからです。

名前の変更は、もっと微妙です。スキーマだけを見ると、名前の変更は追加1つと削除1つです。それを1つの出来事に戻す唯一の仕組みが、エイリアス(alias)です。ところが、Avroは、エイリアスを読み取る側のスキーマのものだけ使います。書き込む側のスキーマを、読み取る側の名前に直して読む方式だからです。そのため、名前の変更は片方向にだけ生き残ります。新しい読み取りコードは、エイリアスを持っているので古いデータを読みますが、古い読み取りコードには、新しい名前を指すエイリアスがありません。プロトコルバッファが、名前の代わりにフィールド番号を使い、その番号は一度使われたら変えられないと明記している理由も、同じです。

型の変更も、方向が分かれます。intをlongに広げると、後方互換は成り立ち、前方互換は壊れます。longをintに狭めると、ちょうど反対です。そのため、「型を変えた」という言葉だけでは何も決められず、どの方向に変えたかを見る必要があります。

現場での姿

1つ目は、1つのバージョンだけを読むコードを書くことです。移行期間には、複数のバージョンが混ざって流れます。読み取る側が最新バージョンしか知らなければ、その期間ずっと古いデータを丸ごと捨て、捨てたという事実は、件数が減ったこととしてだけ現れます。移行期間を越える方法は1つです。読み取りスキーマにデフォルト値を十分に付けて、古いバージョンまで読めるようにすることです。

2つ目は、バージョン番号をデータに書かないことです。行ごとに、どのバージョンで書かれたかの表示がなければ、読み取る側は推測するしかなく、推測は外れます。バージョン番号は、データに付いて回る必要があります。

3つ目は、フィールドを消すときに、デフォルト値を見ないことです。フィールドを消すと、古い読み取りコードは、そのフィールドを見つけられません。そのフィールドが古いスキーマでデフォルト値を持っていたなら、埋められ、なければ壊れます。そのため、消せるフィールドは、最初からデフォルト値があったフィールドだけで、この事実は、フィールドを加えるときに、あらかじめ決まっています。

4つ目は、互換性をドキュメントだけで管理することです。ドキュメントはデプロイを止めません。判定をコードにしておき、新しいバージョンを上げるときに、そのコードが先に動くようにする必要があります。理由を、人が読む文章ではなく、固定されたコードで出せば、自動化が簡単になります。

実務で本当に大切なこと

次のラボですること

注文イベントの5つのバージョンを作って出力したあと、契約ツールcontract.pyを1ステップずつ育てます。必須とオプションをデフォルト値で分け、バージョン間の変化を、追加・削除・名前変更・型変更に分類し、後方・前方互換をAvroのルールどおりに判定して、理由を固定されたコードで出力します。最後に、5つのバージョンが混ざって流れる行を、厳格な読み取りスキーマと寛容な読み取りスキーマでそれぞれ読み、デフォルト値1つが何件を生かすかを、数字で見ます。採点ツールは、毎回異なるフィールド名と型で、自分のスキーマを作って、自分で作ったツールを実際に動かし、答えを突き合わせます。