kevy
このページ

手書きインデックスからテーブルへ移行する

この章が存在するのは、ある本番の利用者が完全な移行——メールシステムが、アプリケーションコードで保守してきた手書きのセカンダリインデックス(sorted set とカウンタキー)を TABLE.DECLARE に載せ替える——をやり切り、その教訓を持ち帰ってきたからです。教訓はその人のノートではなく、エンジンのドキュメントに属します。以下のルールはどれも対価を払って得たもので、並び順は、あなたが必要とする順です。

そのすべてに先立つ最初の一マイル:kevy-cli sql plan schema.sql は、あなたが既に持っているスキーマを読み、各クエリがどうなるかを報告します——それぞれどの宣言経路が供給するのか、供給できないものには、ちょうど追加すべき CREATE INDEX を。*そもそも移せるのか*への 10 分の答えであり、サーバを起こす必要もありません。tables.md を参照。

まず「なぜ」から:エンジンのインデックスだけが検証できる

やり方の前に、論拠を。アプリケーションコードがインデックスを保守するとき、すべての writer が、永遠に、そのインデックスの保守を覚えていなければなりません。何もチェックしません。上の移行は、よく保守された実コードベースでそれが何を意味するかを、手書きの構造をエンジンが新しく構築したインデックスと突き合わせて実測しました:

  • ある手書きインデックスでは行の 89 % が欠けていた——後から追加された writer は、そのインデックスの存在をそもそも知らなかった(*書かれなかった*ドリフト)。
  • 別のインデックスではエントリの 76 % が陳腐化していた——削除パスは行を消したが、インデックスのエントリを消さなかった(*消されなかった*ドリフト)。
  • 3 つ目はメンバーシップは一致するのに順序が食い違った——スコアの式が、片方の writer だけで変更されていた(*スコア*のドリフト)。

これらはどれも、アプリケーションの内側からは検出できませんでした。インデックスが「インデックスに何が入っているべきか」の唯一の記録だったからです。エンジン保守のインデックスは種類として違います:導出は書き込みパスそのもので走るので、忘れられる writer が存在しえません——そして TABLE.VERIFY は要求に応じて双方向を再計算します(tables.md):index→row(drift)と row→index(missing——まさに上の「忘れられた writer」クラスが、数字として見えます)。この移行は性能プロジェクトではありません。*検証不能*から*検証可能*への移動です。

8 つの教訓、必要になる順に

1. 最初の問い:クエリの各次元は、行の上で単値か?

インデックスに答えさせたい各クエリを見て、その次元が行の上で単一の値かを問うてください。答えはたいてい行ではなく、id 導出やキー構築のコードにあります——このメールシステムの「メールボックスごとのスレッド」は単値に見えましたが、id のコードを読むと、ひとつのスレッドが複数のメールボックスに住めることが分かりました。次元が多値なら、どのカラムもそれを運べません:(owner, item) ごとのメンバーシップ行——member:{owner}:{item} に owner と item とソート属性をカラムとして持つ——をモデル化し、ORDERPATH にそれをソートさせてください。これを最初に決めることが、テーブル全体の再宣言を防ぎます。

kevy-cli lint overlap が見つけるのは症状であって、原因ではありません。 原因はコードの中にあり、そこに留まります——しかし多値の次元は、機械が読める痕跡をデータに残します:同じ名前が二つ以上の owner の下に現れる。今あなたが持っている、owner ごとのコレクション群に向けてください:

$ kevy-cli lint overlap -p 6004 --prefix mailbox:
2 owner(s) under mailbox:, 3 distinct name(s)
1 name(s) appear under more than one owner:
  t2  →  mailbox:1, mailbox:2
this dimension is multi-valued, so no column can hold it — model a membership row per (owner, item) and let an ORDERPATH sort it

交わりがあれば非ゼロで終了します。それはヒントではなく答えだからです:二つの owner を名指しする次元を保持できる列は存在しないので、宣言スクリプトは止まるべきです。そしてやらないことにも注意を:候補の列をサンプリングして単値かを確かめる、ということはしません。ハッシュのフィールドは構造上ひとつの値しか持たないので、その検査は永遠に通ってしまいます。

2. 読み取りが供され始めた瞬間、すべての writer が荷重を担う

導出された行は、それを書く者によって埋まります。読み取りを切り替える前に、writer を枚挙してください——基礎エンティティを作成・変更・削除するすべてのコードパスが、テーブルの宣言対象である行を書いていることを確認するのです。忘れる writer は、テーブルが存在する前に書かれた writer です。(これはまさに TABLE.VERIFYmissing カウンタが事後に捕まえるクラスです。監査は、それに本番で出会わないための手段です。)

これには、意図的にツールがありません。 あなたが必要とする事実をストアは持っていないからです——「このテーブルに書くコードパスはどれか」はデータのどこにも記録されていません。writer はコードであり、エンジンに見えるのは書き込みだけです。ツールに*できる*のは、その結果を捕まえることです:kevy-cli shadow は、忘れられた writer を「新経路に欠けている行」として、切り替えの後ではなく前に報告します。驚きを避けるために監査を使い、避けられたことを証明するためにシャドー実行を使ってください。

3. バックフィルは、item を名指しできる全ソースの和集合から

レガシーのインデックスは互いに食い違います——それが上の実測 89 % / 76 % です。どれか*ひとつ*からバックフィルすれば、その穴を相続します。そして VERIFY は、一度も書かれなかった行を見ることができません。バックフィルのキー集合は、item を名指しできるすべての構造(旧インデックス、プライマリキースペースのスキャン、アーカイブ)の和集合から作り、行は権威ある記録から書いてください。

その和集合を作るのが kevy-cli backfill-keys です——そしてそれだけです。この教訓はそれ自身で二つに割れており、後半はあなたのものです。何が権威ある記録なのか、行がどんな形なのかは、アプリケーションの側に住む知識です。推測する道具は、間違った行を自信たっぷりに書きます。

$ kevy-cli backfill-keys --from-index idx:threads --from-prefix mail: \
      --from-file archive.txt > keys.txt
601 name(s) in the union
  index idx:threads                3 name(s), 0 only here
  prefix mail:                     600 name(s), 596 only here
  file archive.txt                 2 name(s), 1 only here
597 name(s) appear in only one source — backfilling from any single one would have missed them

名前は stdout へ一行ずつ、行を書く工程にそのまま流せます。会計は stderr へ出るので、リストをリダイレクトしても失われません。最後の数字が肝心です:ただ一つのソースにしか現れない名前は、どれか一つからバックフィルしていたら取りこぼしていた行です——上の 89 % / 76 % のドリフトを、他人の数字の引用ではなく、あなた自身のデータで測ったものです。

プレフィックスのソースは既定でプレフィックスを剥がします(mail:123123)。そうするとインデックスのメンバと名前が揃うからです。キーそのものが名前である場合は --keep-prefix を。読めないソース——存在しないキー、型の違うキー——はエラーであって、空の貢献ではありません:黙って空になるソースこそ、このコマンドが塞ぐために存在する穴です。

4. 切り替えの前にシャドーリード——内容と順序を比較する

旧経路から読みを供しつつ、新しい答えを横で計算して比較してください。メンバーシップだけでなく順序も比較すること:スコアのドリフトは、同一の集合を異なる順序で生みます。ページネーションされた UI はそれをユーザーに見える揺れに変えます。最初の食い違いを両方のソートキーつきでログに残してください——その 1 行が、ドリフトしている writer を即座に名指しします。

kevy-cli shadow がこれを代わりにやります。 両方のコマンドを渡すと、それぞれが返す行キーの順序を比較し、食い違いがあれば非ゼロで終了します——カットオーバーのスクリプトはそれをそのままゲートにできます:

$ kevy-cli shadow -p 6004 \
    --old "ZRANGE old:act 0 -1 WITHSCORES" --old-pairs \
    --new "IDX.QUERY u.act RANGE 0 999 LIMIT 20" --samples 50
shadow: 50 samples, 50 diverged (first at sample 0)
  ORDER differs at position 0:
    old: u:5 (sort 5)
    new: u:1 (sort 10)

このソート値の対こそ、この教訓が言っている行です。もう一つ報告される形は MISSING——旧経路にあって新経路にない行、つまり先回りして現れた第 2 の教訓です:誰も更新しなかった writer が、カットオーバーのに見えます。TABLE.VERIFY が後から報告するのを待つ必要はありません。

推測しないことが二つあります。kevy のページ応答([カーソル, [キー, ソート値, …]])は形から判別できますが、member/score の対はプレーンなリストと見分けがつきません——WITHSCORES の類には --old-pairs を付けてください。さもないとスコアがすべて行キーとして読まれ、毎サンプルが食い違いになります。そして一度の食い違いは手がかりであって判決ではありません:両側は同じ接続で連続して読まれるので、その間に書かれた行はここに現れます。--samples n を回して、その比率を読んでください。

5. 旧構造の削除:先に reader、次に writer

シャドーの窓を閉じるとき、旧インデックスの reader を先に、writer を後に取り除いてください。逆順には静かな故障モードがあります:存在しないキーの読み取りはエラーではなく 0 や空として返るので、writer が消えた後に残った reader は、クラッシュする代わりに静かに間違った答えを供します。それからキーを消してください。

ここにもツールはなく、そしてここはツールがあれば積極的に害になる場所です。 kevy はキーを*読んだ*のが誰かを追跡しないので、「もう誰もこれを読んでいない」という探査は、観測ではなく推論になります。この教訓の故障モードはそもそも*静かに空を読んで、問題なしと見なすこと*です——「確認済み、削除して安全」と言う探査は、その故障に確認の衣を着せることになります。削除の順序は手で決めてください:先に reader、次に writer、最後にキーです。

6. インデックスにない述語 ⇒ もうひとつの ORDERPATH。カラムの複製は決してしない

新しいクエリが現在の形では出せない述語を必要とするとき、手書き時代の反射は「その値をもう 1 か所にも書く」ことです——それは、この移行が消したばかりの「writer 2 つ、真実 1 つ」問題の再生産です。代わりに同じカラムの上にもうひとつの ORDERPATH(またはインデックス)を宣言してください。エンジンは同じ書き込みで、同じ行から両方を導出します。

kevy-cli lint columns <table> が見つけるのは形です。 ほぼすべての行で同じ値を持つ二つの列は、第二のソート順を得るために複製された一つの列です:

$ kevy-cli lint columns -p 6004 ev
ev: 43 row(s) sampled under ev:
  created_at and sort_ts agree on 93% (40/43)
a column copied to get a second sort order is the shape lesson 6 warns about — the answer is another ORDERPATH; ask IDX.ADVISE which one

lint overlap と違い、何を見つけても 0 で終了します:二つの列が正当に一致することはあり得るので、これは判決ではなく疑いです。また第 1 の教訓の検査と違い、これはテーブルの宣言のに走ります——行を読むからです。--sample N が読む量を、--threshold PCT が線の位置を決めます。

7. 起動は ensure

定常状態は起動パターンです:プロセスの開始ごとに TABLE.ENSURE——最初の起動で Created、以後は Unchanged、そしてコードの spec がストアのそれと合わなくなったときは差分を名指しする拒否。それが、意図した TABLE.REPLACE 移行を自分で走らせるための合図です——移行に走られるのではなく。

8. VERIFY は移行の一部ではなく、運用の一部に

カウンタは呼び出しごとに新鮮で、cron や doctor コマンドから回せるほど安価です:driftmissing は永遠にゼロであるべきで、absent / excluded / coerce_failures は各除外原因が奪った行を名指しします(正確な意味論は tables.md。ORDERPATH の duplicates が非ゼロならページネーションに有界のタイブレークが要る、という話も含めて)。この移行全体の眼目は、これらの数字が*存在する*ことです。読んでください。

kevy-cli doctor がその cron です。 宣言済みのすべてのテーブルに VERIFY を回し、終了コードで答えます:

$ kevy-cli doctor -p 6004
  OK       user  (rows 59999 · entries 59999 · absent 0 · excluded 0 · coerce_failures 0)
  WARN     ev    duplicates 1 — paging this path needs a bounded tie-break or pages repeat rows
  BUILDING new   — an index is still backfilling, not a verdict
doctor: 3 table(s) — 0 drifted, 1 warned, 1 still building

対応付けは新しい主張ではなく、この教訓自身の言葉です:driftmissing が非ゼロなら失敗duplicates警告absent / excluded / coerce_failures報告するだけで決して失敗させません——どれも正当な状態であり、NULL の列が一つあるだけで赤くなる doctor は永遠に赤いままだからです。

意図的な選択が二つ。警告は既定では失敗させません——情報で落ちる cron は読まれなくなります——ので、より厳しい契約が欲しい人のために --warn-is-failure があります。そしてインデックスが backfill 中のテーブルは -INDEXBUILDING を返しますが、それは失敗ではなく、それ自体が一つの結末です:失敗として扱えば、インデックスを宣言するたびに誰かを呼び出すことになります。

参照

  • tables.md——宣言の面、VERIFY の意味論、複合 ORDERPATH の規則。
  • cookbook.md——シーケンス、制約、複合順序のレシピ。
  • tiering.md——行がコールドでもインデックスはホット。index-only クエリは行にゼロ回しか触れません。