隔離した行はどこへ行くのか
一言でいうと
隔離は行を捨てない仕組みであって、それ自体が解決ではありません。直して入れ直すループと、古さを見る目がなければ、隔離は誰も開けない引き出しになります。
なぜ必要なのか
品質検査を付けました。使えない行は捨てずに、隔離テーブルに理由と一緒に残します。ここまでは、たいていやります。
半年後にそのテーブルを開いてみると、4万行が入っています。理由は6種類で、そのうち1つが3万行で、その3万行は、すべて同じ日に、上流がフィールドを1つ移し替えたことで生じたものです。その日以降、誰もそのテーブルを見ませんでした。売上集計は、その間3万件が抜けたまま出ていて、ダッシュボードはずっと緑ランプでした。
ここで明らかになるのが、実行の状態とデータの状態が別の軸だという事実です。パイプラインは毎日成功しました。成功したというのは、「エラーなしで終わった」という意味であって、「入れるべきものをすべて入れた」という意味ではありません。2つを1つの欄にまとめて報告すると、どちらも見えなくなります。
このコースとfde-dataが分かれるところ
fde-dataのCSVラボは、顧客が渡したファイル1つの塊から、使えない行を切り離して、リジェクトファイルとして残すところまで行きます。そのファイルは一度来るだけで、リジェクトの一覧は、顧客に送れば終わりです。このコースは、毎日回るパイプラインなので、そこで終わりません。隔離は、実行のたびにまた積み重なり、昨日隔離した行が今日もまた来て、誰かが直して入れると、その行が2回数えられることがあります。1回解きほぐす作業と、回り続ける作業は、設計が違います。
期待値をコードではなくファイルで
期待値(expectation)は、「このデータが正しいなら、こうなっているはず」を書いたものです。条件文でコードの中に散らばらせておくと、何を見ているのか誰もわからなくなります。1つのファイルに一覧として書いておくと、3つのことが変わります。上流と、そのファイルを見ながら話せて、ルールが増えてもコードが変わらず、破られたルールの名前が、隔離の理由になります。
{"rules": [
{"name": "order_id_format", "column": "order_id", "type": "regex",
"pattern": "^ORD-[0-9]{6}$"},
{"name": "qty_range", "column": "qty", "type": "range", "min": 1, "max": 50},
{"name": "status_enum", "column": "status", "type": "enum",
"values": ["NEW", "PAID", "CANCELLED"]}
]}
理由をルール名にすると、「なぜ隔離されたか」が、自由な文章ではなく、数えられる値になります。数えられて初めて、理由ごとに何件かを報告し、どの理由が増えているかを見られます。
しきい値を何で決めるか
行1つが間違っていることと、ファイル全体が間違っていることは、別の出来事です。前者は行単位の隔離で扱い、後者は実行単位の停止(circuit break)で扱います。上流がカラムの順序を変えて送ってくると、ほぼすべての行がずれますが、そのときに行単位で隔離すると、隔離テーブルに1万行が入り、集計は空のまま出ていきます。そのようなファイルは、1行も入れないほうがよいです。
分ける基準は、比率です。そして、その比率は普段の値から決めます。「5%」のようなきりのいい数を先に決めると、どちらかになります。普段が7%のデータでは毎日止まり、普段が0.1%のデータでは、20倍壊れても止まりません。直近の実行の失敗率を測っておき、その上に余裕を持たせてとります。しきい値は、データごとに違い、そのため、データごとに書いておく必要があります。
停止するときは、何も入れません。半分だけ入れて止まると、次の人がどこまで入ったかを判断する必要があり、その判断は、いつか間違えます。
入れ直すループと二重計上
隔離した行は、直して入れ直して、初めて終わりです。ここで、2つが壊れやすいです。
1つ目は、ファクトテーブルに2回入ることです。直した行をそのままINSERTすると、元の行と直した行が両方残ります。自然キーに一意制約をかけて、更新として入れれば、この問題はなくなります。
2つ目は、隔離を2回閉じることです。同じ修正を2回実行すると、すでに閉じた隔離をまた閉じてしまい、「今回3件を解決した」が2回報告されます。閉じるときに、まだ開いているものだけを閉じれば、2回目の実行は0件になります。再投入は、必ず何回も実行されます。人が手で回す作業だからです。
そして、直せない隔離もあります。キーそのものが壊れた行です。伝票番号が規格を守らずに隔離されたなら、番号を直した瞬間に、それは別の行になります。このようなものは、再投入ではなく、上流に差し戻す必要があります。
古くなった隔離にアラートをかける
隔離は、積み重なるのが正常です。問題は、減らないことです。そのため、「何件が隔離されたか」ではなく、「最も古い隔離が、何回前の実行のものか」を見ます。
古さ(age)は、時刻よりも実行回数で測るほうが扱いやすいです。1日1回回るパイプラインで、「3回以上前の隔離」は3日を意味し、デプロイが止まってパイプラインが回らなければ、古さも増えません。そのほうが、人の感覚に合っています。
現場での姿
- 隔離テーブルが1万行なのに、誰も知りません。開いている隔離の最大の古さを指標として出力すれば、その日にすぐ見えます。
- 再処理を回したら、売上が2倍。直した行を更新ではなく挿入で入れた場合です。
- 毎日止まるパイプライン。しきい値を、普段の値ではなくきりのいい数で決めた場合です。人々は、すぐに検査をオフにしてしまいます。
- 「昨日は成功しましたけど」。実行の状態だけを見て、データの状態を見なかった場合です。報告を2つの欄に分ければ、この会話はなくなります。
実務で本当に大切なこと
- 期待値は、コードではなくファイルに書き、破られたルールの名前を、隔離の理由として使います。
- 行単位の隔離と実行単位の停止を分け、停止のしきい値は、普段の失敗率から決めます。
- 再投入は更新として入れ、開いている隔離だけを閉じます。2回回しても、数字が変わらない必要があります。
- 隔離の古さを指標として出力します。件数だけでは、減らないものを見られません。
- 実行が成功したかとデータが正しいかを、2つの欄で報告します。
次のラボですること
期待値をファイルに書いておき、ゲートキーパーqgate.pyを1ステップずつ育てます。数えるだけのcheckから作って、sqliteにファクトと隔離を分けて積み、間違いが多すぎる実行は丸ごと止め、直した行を入れ直して、2回入れても数字が変わらないようにし、古くなった隔離を見つけ出し、実行の状態とデータの状態を分けて報告します。採点ツールは、毎回異なるしきい値と異なる店舗で、自分のドロップと期待値を用意して、自分で作ったゲートキーパーを実際に動かし、データベースを直接開いて、ファクトと隔離が正しく入っているかを数えます。