読み取りと書き込みのあいだには敷居が一つある
一言でいうと
調査は間違えてもやり直せますが、UPDATEはやり直せません。そのため、書き込みの前には、調査にはなかった4つのものが加わります。範囲の算定、元に戻す手段、ドライラン、中止基準です。
なぜ必要なのか
FDEが顧客環境で行う仕事の大半は、読み取りです。ログを読み、テーブルを照会し、コードをたどり、その結果をドキュメントにまとめます。この段階で判断を誤ったときのコストは、時間です。仮説が外れれば消して立て直せばよく、数え間違えた数字は数え直せば済みます。
ところが、ある時点で必ずこの一言が来ます。「では、それを少し直してもらえますか。」
ここから性質が完全に変わります。間違った照会はやり直せば済みますが、間違ったUPDATEはやり直せません。元に戻す手段をあらかじめ用意していなかったなら、その瞬間に私たちがしたことは、調査ではなく事故です。
もう1つ問題があります。依頼は条件ではなく文章で届きます。「長く止まっている決済案件を整理してください」という文章をWHERE status='pending'に置き換えた瞬間、依頼した人が頭の中に描いた範囲と、実際に変わる範囲がずれます。ずれた幅の分だけが事故であり、その幅は数えてみるまで、依頼した側も受けた側もわかりません。
どう動くのか
書き込みの前に付く4つの仕組みは、すべてこのずれを小さくするためのものか、小さくできなかったときに備えるものです。
1つ目、範囲の算定。2つの数字を並べます。依頼の文章をそのまま置き換えたときに変わる行数と、条件を絞ったときに変わる行数です。この2つの数字の差が、この変更のリスクです。45件と20件なら25件がリスクであり、その25件がなぜ除外されるのかを1件ずつ書けて初めて、条件が完成したことになります。
2つ目、元に戻す手段。バックアップは2種類残しますが、用途が違います。
파일 사본 전부 잘못됐을 때 통째로 되돌린다.
단점 — 그 사이에 들어온 남의 변경까지 함께 되돌아간다.
행 스냅샷 바꿀 행의 변경 전 값을 id 와 함께 남긴다.
id 를 열쇠로 그 행만 되돌릴 수 있고, 무엇이 어떻게 바뀌었는지 설명할 수 있다.
ファイルコピーだけでは、元に戻せても説明ができず、行スナップショットだけでは、スキーマが変わる変更を元に戻せません。そのため、両方を残します。
3つ目、ドライラン。トランザクションを開き、実際のUPDATEを実行したあと、コミットせずに元に戻します。SELECT COUNT(*)で事前に数えるのと結果は同じに見えますが、性質が違います。数える条件と変更する条件が文字どおり同じだからです。2つの文を別々に書くとその間に誤字が潜み、誤字は必ず2つの文のうち実行される側にあります。
4つ目、中止基準。どんな結果を見たら止めるかを、実行前に決めておきます。これは次のモジュールで別に扱います。
そして、適用が終わったあとの検証には、もう半分あります。対照群、つまり変わってはいけないものがそのままかどうかを数えることです。変わるべきものだけを数える検証は、条件が広くて隣まで一緒に触ってしまった事故でも、問題なく通ってしまいます。
現場での姿
1つ目に、確認できないものは変更しません。顧客テーブルにないcustomer_idを持つ注文のように、条件は満たすものの誰のものかわからない行が、実際のデータにはほとんどいつも混ざっています。古いというだけの理由でまとめて取り消すと、あとでその1件を説明できません。除外し、除外した事実と理由を計画書に書き、顧客企業に問い合わせるのが正解です。除外は仕事を減らしたのではなく、判断を記録に残したことです。
2つ目に、なかった値を新しく作る変更は、静かに広がります。statusにこれまでなかったcancelledを入れると、その値を知らない集計クエリやダッシュボードは、その行を黙って除いて数えます。エラーが出ないので誰も気づかず、1か月後に精算が合わないことで発覚します。そのため、新しい値を導入する変更は、データの変更であると同時に、通知が必要な変更です。
3つ目に、実行記録は実行した人のためではなく、2時間後にこのテーブルを不審に思う別の人のためのものです。いつ、何を、何件、何で元に戻すのかが1か所にまとまっていないと、その人は私たちを探し出すのに時間を使います。
元に戻せない変更を元に戻せる形に分ける
変更計画で最も重要な項目は「失敗したらどう元に戻すか」ですが、変更によっては、その欄に書ける内容がありません。そのときは、計画を直すのではなく、変更そのものを分けます。
列の削除は3回に分けて行います。一度に行うと、古いコードが動いている間に壊れます。
- コードからその列を読まないようにしてデプロイします(書き込みは続けます)。
- 数日置き、元に戻す必要がないことを確認してから、書かないようにしてデプロイします。
- そのあとで列を削除します。
各段階は前後どちらのバージョンとも一緒に動かせるので、いつでもデプロイを元に戻せます。
列の追加も同じです。not nullを最初から付けると、古いコードのinsertがすべて失敗します。NULLを許容して入れ、既定値を埋め、コードが必ず値を入れるようにしてから、制約を付けます。大きなテーブルでは、制約をまずnot validで付けて、あとから検証すると、ロック時間が短くなります。
長いロックは、デプロイの失敗には見えません。マイグレーションがテーブルをロックしたまま5分かかると、その間すべてのリクエストが溜まります。デプロイは「成功」で終わり、障害だけが残ります。そのため、マイグレーションには必ずロックの上限を設定します。
set local lock_timeout = '3s';
set local statement_timeout = '30s';
alter table orders add column region text;
ロックを取れなければ失敗して再試行するほうが、取れるまで待ってサービスを止めるよりましです。
元に戻す操作を、実際にやってみます。計画書の「ロールバック: 前のバージョンにデプロイ」は、たいてい検証されたことがありません。開発環境で一度戻してみると、設定ファイルが合わない、キャッシュに新しい形式が残っている、キューに新しいメッセージが溜まっている、といった問題が見つかります。
変更ウィンドウは、人のためのものです。午前3時にデプロイすると、問題を誰も見られません。人が起きていて、トラフィックが少なく、翌日が勤務日である時間帯が、最も安全です。
次のラボですること
顧客企業から「pendingの注文をすべてcancelledに変更してください」と依頼された状況を、最初から最後まで通して行います。依頼どおりに置き換えると45件が変わりますが、実際に変更すべきなのは20件です。その25件の差を見つけ、元に戻す手段を用意し、コピー上で元に戻す操作を実演したあと、本適用と対照群の検証まで終えます。