kevy
このページ

6.4 から 7.0 へのアップグレード

要点:プロトコル経由のクライアントはコードの変更不要、データディレクトリはそのまま開けます。 バイナリを差し替える前に確認が必要なことが四つあります。

  • 同じデータでも used_memory が大きく報告されます。文字列とハッシュが混ざったデータで約 1.5 倍です。アロケーターが実際に持っている量を数えるようになったためです。6.4 の報告値ぎりぎりに maxmemory を設定していると、以前より早く追い出しが始まります(§4)。
  • レプリカがある場合、プライマリが 6.4 の間はレプリカにインデックス・ビュー・テーブルがなく、プライマリが 7.0 になるとプライマリのカタログを受け取ります(§2)。
  • 6.4 に戻すと、アップグレード前のサイドファイルを取っておかない限り、インデックス・ビュー・テーブルのカタログは失われます。7.0 で強制終了された組み込みストアは、先に 7.0 で一度きちんと開いて閉じる必要があります。スナップショットのあとにログを書き直したのが最後の操作だったディレクトリは、7.0 でもう一度保存しないと、6.4 が書き込みの一部を二回適用します(§1)。
  • Rust から呼び出しているコードは修正が必要で、直す場所はコンパイラが一つずつ示します(§13)。

メジャーバージョンを上げたのは、あとの三種類の変更のためです。ワークスペースの公開 Rust API をすべて Rust API Guidelines に揃え、ほとんどのクレートでシグネチャが変わりました。kevy-config の構造体に公開フィールドが増え、構造体リテラルが壊れます(§10)。Go モジュールは github.com/goliajp/kevy-go/v7 に移りました(§14)。そして 7.0 が書いたディレクトリには、6.4 が理解しないファイルとログフレームが入ります(§1)。

7.0.0 の中心は四つです。強制終了されたプロセスでも、返った書き込みをすべて保ち、開くのも閉じるのも速くなった組み込みストア。値でシャードに分散するグローバルインデックス。以前の 4 分の 1 以下のコストになったインデックスの行。そして暗号化したリンクです。ノード間のリンクと二つ目のクライアントポートがあり、どれも設定しなければ無効です(encrypted-links.md)。あわせて、データを失ったり変えたりしていた欠陥を直しました。1.x の頃からのものもあります。一覧は §16 にあります。

kevy-embedded = "7.0.0"

早見表:やること

こういう場合変わること§
サーバーを動かしている、またはプロトコル経由で kevy を使っているバイナリを差し替え、maxmemory を確認する4
maxmemory を設定している、または階層化したサーバーを動かしている同じデータで used_memory が約 1.5 倍になる。階層化したサーバーは、プロセスが予算を超えて保持している間、データを増やす書き込みも拒否する4
レプリカを動かしているどちらを先に上げてもよい。プライマリが 7.0 になるとレプリカはそのカタログを受け取り、レプリカ自身のインデックス宣言は破棄される2
BLPOP、BRPOP、RENAME、RENAMENX やカタログコマンドをレプリカに送っているREADONLY で拒否される3
6.4 に戻す可能性がある7.0 を SHUTDOWN SAVE で止める(組み込み:save_snapshot のあと閉じる)。アップグレード前のカタログのサイドファイルを取っておく1
インデックスに MAXMEM を設定している、または階層ストアの容量がインデックスの下限に近いインデックスのサイズの読みが変わる。大きなインデックスでは小さく、最低でもシャードあたり約 1.8 KB5
IDX.LIST や IDX.DESCRIBE を位置で解析しているそれぞれ partitioning の対が一つ増える6
変更フィードや AOF を読んでいる組み込みの MSET がシャードごとに一つのフレームで届き、ストリームの書き込みは再生に必要な形で届く7
うっかり同じポートでサーバーを二つ起動する二つ目は起動を拒否するようになる8
io_uring のリアクターを使っている受信バッファがシャードあたり 64 MiB から 16 MiB になる。転送された処理のあと、シャードは 200 µs ポーリングする9
kevy_config の構造体を構造体リテラルで作っている新しいフィールドを書くか、..Default::default() を使う10
kevy_rt::Commands を実装しているメソッドが増える。どれも既定実装つき11
スクリプトで kevy-cli doctor、export、sql compile などを裸の単語で呼んでいるツールを --kevy の後ろに移す12
kevy のクレートを Rust のライブラリとして使っているほとんどのシグネチャが変わる。変わった箇所はコンパイラが一つずつ示す13
Go モジュールを使っている、またはバインディングの読み取り専用エラーの文面で照合している/v7 を import する。文面の末尾に句点(ピリオド)が付いた14
ストリームのコマンド、5- のような ID、期限切れ間近のキーへの EXPIRE を使っているvalkey や Redis と同じ応答になる。タイムアウトしたブロッキング XREAD は *-1 を返す15
アロケータをプリロードしている、MALLOC_* 変数を設定している、または INFO modules を読んでいるサーバーは kevy-alloc で動く。システムアロケータにするには --no-default-features でビルドする17
インデックスの読み取りが触れるシャードを減らしたいグローバルインデックスとして宣言するインデックス

そのまま引き継がれるもの

6.4.0 のリリース版バイナリ(v6.4.0 タグからビルド)と 7.0.0 を、それぞれ二シャードで、io_uring リアクターを使う Linux 上で実測しました。組み込みストアは macOS でも測っています。データセットは、文字列、キーの TTL、フィールド TTL つきのハッシュ、リスト、セット、ソート済みセット、コンシューマーグループつきのストリーム(ENTRIESREAD で読み取りカウンターを設定したグループと NOACK で読んだグループを含み、コンシューマーは読み取り、クレーム、XGROUP CREATECONSUMER で作成。保留中に削除・トリムされたエントリーあり)、インデックス二つ、テーブル、マテリアライズドビューです。途中で BGSAVE を挟み、一部の書き込みがログにだけあるようにしました。

ケースキー、TTL、フィールド TTL、ストリームインデックス、ビュー、テーブル
6.4.0 のディレクトリを 7.0 で開くすべて揃うすべて揃う。サイドファイルはログに移され、削除される
7.0 のディレクトリを 6.4.0 でログから開くすべて揃う。ただし 7.0 がログを書き直していた場合、保留中に削除・トリムされたエントリーの保留行はないなし
7.0 のディレクトリを 6.4.0 でスナップショットとログから開くすべて揃うなし
6.4.0 が書き込んだあと 7.0 で開き直すすべて揃う。6.4.0 が書いたものも含む戻る。ただし 6.4.0 がログを書き直した場合は除く(§1)
6.4.0 プライマリ → 7.0 レプリカフル同期もライブストリームも収束するなし。§2 を参照
7.0 プライマリ → 6.4.0 レプリカフル同期もライブストリームも収束するなし

6.4.0 は新しいフレームを、ログに何も出さずに読み飛ばします。バックアップはコピーです。 コピーしたディレクトリは元と同じデータを返します。プロセスが強制終了されたあとにコピーしたものも同じです(§1)。

1. 6.4 に戻すとき、ディレクトリにありうるもの

クラッシュや強制終了のあと

以前は、プロセスが強制終了されると、ユーザー空間のバッファで待っていた書き込みが失われました。7.0 では、追加が返った瞬間に、その内容はカーネルが持つメモリにあります。

  • Apple のプラットフォームでは AOF そのものをマップし、末尾に事前確保した領域(4 MiB から倍々で 64 MiB まで)に追加をコピーします。ストアを開いている間はファイルがレコードより長く、残りはゼロです。きちんと閉じると切り詰められます。強制終了されたプロセスはゼロを残し、次に開いたときに取り除かれます。
  • それ以外では 追加はまずステージングリングに入ります。小さなマップファイル aof-<i>.aof.stage(既定 4 MiB)で、tick ごとに AOF へ移されます。強制終了されたプロセスがそこに残したものは、次に開いたときに再生されます。元のディレクトリでも、強制終了のあとに取ったコピー(バックアップがそうです)でも同じです。

6.4 はどちらも知りません。書き込みを続けている組み込みの書き手を SIGKILL で強制終了して測りました。

強制終了直後のディレクトリ6.4.0 で開く7.0 で開いて閉じてから 6.4.0 で開く
macOS、マップした AOF(macOS での既定)返った書き込みはすべてある。6.4.0 は壊れた末尾を報告し、ゼロ(この実行では 20 MB)を aof-<i>.aof.corrupt-quarantine.<ts> ファイルに移す返った書き込みはすべてある。警告も隔離ファイルもない
Linux、ステージングリング(Linux での既定)リングに残っていた書き込みが失われる(この実行では 1,303,496 件中 9,992 件)。6.4.0 は .stage ファイルをそのまま残す返った書き込みはすべてある

そのため、クラッシュや強制終了のあとは、6.4 に戻す前に 7.0 でディレクトリを一度開き、きちんと閉じてください。 リングが空になり末尾が切り詰められて、6.4 がこれまでどおり読めるファイルが残ります。Linux と Android では、空になった aof-<i>.aof.stage はディレクトリに残りますが、6.4 はこれを無視します。強制終了されたディレクトリを 6.4 がすでに開いて書き込んでいた場合、7.0 は古いリングをその新しい書き込みの上に再生しません。6.4 が書いたものを保ち、リングにあった書き込みは失われたままです。

Config::with_stage_ring(0) と Config::with_mapped_aof(false) でこの二つを無効にでき、AppendFsync::Always はどちらも使いません。両方を無効にすると、強制終了されたプロセスは 6.4 と同じくバッファにあったものを失います(これらの実行での実測は 44〜80 件)。

スナップショットと書き直したログ

7.0 はすべての AOF の先頭に内部レコードを置き、そのログがどのスナップショットの続きか、または書き直しで得た完全なイメージであることを記録します。スナップショットファイルの末尾にはそのスナップショットの id を書きます。再起動では、スナップショットはその続きであるログの下でだけ読み込みます。6.4 はどちらも知りません。ディレクトリにあるスナップショットを読み込み、その上にログ全体を再生します。スナップショットの読み込みは id の手前で止まり、再生ではそのレコードを一つのコマンドとして数えて読み飛ばします。7.0 が二つのシャードで書いたディレクトリ(サーバーと組み込みストアのそれぞれ)を 6.4.0 で開き、リスト、追記した文字列、増やしたハッシュのフィールド、ストリームを比べて測りました。

7.0 が止まる前に最後にしたこと6.4.0 は 7.0 と同じ値を返すか
書き込みだけ(スナップショットなし)同じ
スナップショットのあと書き込み同じ
ログの書き直し。ディレクトリにスナップショットはない同じ
スナップショット、ログの書き直し、書き込みの順違う。書き直しより前にリストへ入れた要素がすべて二回現れる。文字列・ハッシュ・ストリームは正しい
上の行のあと、もう一度スナップショット同じ

違う結果になる行は 6.4 自身の規則によるものです。6.4 のサーバーで BGSAVE のあとに BGREWRITEAOF を実行しても、次の起動で同じリストが二倍になります(7.0 で修正。§16)。そのため、戻す前に 7.0 を SHUTDOWN SAVE で止めてください(組み込みストアは save_snapshot() を呼んでから閉じる)。最後の操作がスナップショットになり、ログにはそのあとの書き込みだけが残ります。スナップショットと書き直しの直後に SHUTDOWN SAVE で止めたサーバーを 6.4.0 で開くと、すべての値が 7.0 の返したとおりでした。

カタログとコンシューマーの接触

ログにはほかにも新しいものが四つあり、サーバーでも組み込みストアでも同じです。

  • コンシューマーの最後の接触時刻と最後にエントリーを受け取った時刻は、内部の XINTERNAL.CONSUMERSEEN フレームとして記録されます。コマンドがコンシューマーを作った場合は、その前に XGROUP CREATECONSUMER のフレームがあります。6.4 は後者を実行して前者を読み飛ばすので、コンシューマーはすべて揃い、アイドル時間は 6.4 がログを再生した時点から数えられます。これは 6.4 の従来の数え方と同じです。
  • グループの読み取りカウンター(XINFO GROUPS の entries-read)は、XGROUP SETID … ENTRIESREAD n という独立したフレームに記録され、その前にカウンターなしでグループの位置だけを動かすフレームがあります。6.4 はこのオプションを受け付けずにそのフレームを読み飛ばしますが、グループの位置は 7.0 と同じです。
  • 書き直したログは、保留中にストリームから消えた(削除またはトリムされた)エントリーの保留行を、内部の XINTERNAL.PENDING フレームとして残します。6.4 の書き直しはその行を落としていました。6.4 はこのフレームを読み飛ばすので、その行はありません。6.4 自身が書き直したあとと同じです。
  • インデックス・ビュー・テーブルのカタログは、index-catalog.meta、view-catalog.meta、table-catalog.meta に置かれなくなりました。7.0 は変更のたびに、カタログ全体を運ぶ内部の XINTERNAL.CATALOG フレームを一つログに記録し、現在のカタログをすべてのスナップショットに保存します。6.4 のディレクトリで初めて起動したときは、カタログを三つのファイルから移し、ファイルを削除します。

戻すときにこれが何を意味するか、実測した結果です。

  • 7.0 が書いたディレクトリを 6.4.0 で開くと、ログからでもスナップショットからでも、キーはすべて揃い、インデックス・ビュー・テーブルは一つもありません。
  • そのあと 6.4 が何を書いても 7.0 は読め、カタログも一緒に戻ります。ただし 6.4 がログを書き直した(BGREWRITEAOF)場合はフレームが消え、7.0 は最新のスナップショットのカタログで開きます。スナップショットがなければカタログもありません。
  • 6.4 が初めて開く前に、アップグレード前の三つのサイドファイルをディレクトリにコピーして戻しておけば、6.4 はカタログを取り戻します。

そのため、戻す前に、アップグレード前の三つのファイルのコピーを取っておいて先に戻すか、6.4 でカタログを宣言し直してください。

2. レプリケーション構成:プライマリを先にアップグレードする

レプリカは、バージョンがどちらの向きに違っても、プライマリのキーに追従します。カタログは渡りません。6.4 のプライマリはもともとカタログをレプリカに送らず、7.0 のプライマリが送るフレームを 6.4 は読み飛ばします。インデックス二つ、テーブル、ビューを持つ 6.4.0 のプライマリと、自分でもインデックスを一つ宣言した 6.4.0 のレプリカで測りました。

  • プライマリが先。 レプリカがまだ 6.4.0 で動いている間は、自分で宣言したインデックスを保ち、プライマリのものは一つも受け取りません。7.0 として初めて起動したときにフル同期でプライマリのカタログ全体を受け取り、自分で宣言したインデックスは消えます。その index-catalog.meta はディレクトリに残ります。
  • レプリカが先。 6.4.0 プライマリの 7.0 レプリカには、インデックス・ビュー・テーブルがありません。自分で宣言することも拒否する(§3)ので、プライマリが 7.0 になるまで、そのレプリカへのインデックスのクエリはすべて no such index で失敗します。そのあとプライマリが 7.0 として再起動すると、サイドファイルからカタログを読み、ストリームで送ります。接続したままのレプリカは、追いついた時点でプライマリのカタログを持ちます(接続したままの 7.0 レプリカの下で 6.4.0 のプライマリを 7.0 に入れ替えて測定)。

どちらの順でも、最後にはすべてのレプリカがプライマリのカタログを持ちます。プライマリを先に上げると、レプリカにインデックスがない時間が最も短くなります。6.4 でレプリカ自身がインデックスを宣言していた場合は、代わりにプライマリで宣言する必要があります(レプリケーション)。

3. レプリカが拒否する書き込みが増える

読み取り専用のレプリカは、次のコマンドに -READONLY You can't write against a read only replica. で応答します。6.4 はこれらを自分のキー空間に対して実行し、プライマリとずれていくのを許していました。

  • BLPOP、BRPOP、RENAME、RENAMENX(6.4.0 での実測:レプリカに送った BLPOP は、レプリカが持つリストのコピーから要素を取り出していました)
  • すべてのカタログコマンド:IDX.CREATE / DROP / REBUILD、VIEW.CREATE / DROP / REBUILD、TABLE.DECLARE / ENSURE / REPLACE / DROP

EVAL_RO のスクリプトも、最初の四つを呼べなくなりました。組み込みのレプリカは、カタログのメソッドを KevyError::ReadOnly で拒否します。インデックス・ビュー・テーブルはプライマリで宣言してください。すべてのレプリカに届きます。

4. 同じデータでも used_memory が大きく報告される

6.4 は、キー空間のテーブルでの場所代として、どのキーにも一律 96 バイトを計上し、ハッシュは少なく計上していました(ハッシュのテーブルを包む箱は数えられず、各スロットは実際には 49 バイト使うところを 32 バイトで計上していました)。7.0 は、キー空間のテーブルと各ハッシュについて、アロケーターが実際に持っているバイト数を計上します。そのため used_memory、MEMORY USAGE、maxmemory による追い出し、階層ストアの降格は、どれも大きい、実際の数字を見ます。データを入れ終えたあとのプロセスのメモリが増えたわけではなく、数える範囲が広がっただけです。

同じ 250,000 キー(32 バイトの文字列 200,000 個と、フィールド四つのうち一つが 200 バイトのハッシュ 50,000 個)を二シャードに一本のパイプライン接続で投入し、投入の五秒後に測った結果です。

6.4.07.0
used_memory77,600,000103,543,040
プロセスの RSS163.1 MiB134.1 MiB
投入中の RSS のピーク171.2 MiB196.4 MiB
文字列一つの MEMORY USAGE128201
ハッシュ一つの MEMORY USAGE1,0401,272

そのため、6.4 の used_memory から決めた maxmemory では、以前保持していたデータの約 4 分の 3 で追い出しが始まります。RSS を基準に決めるか、自分のデータで測った比率の分だけ引き上げてください。

階層化したサーバー(--tiering-budget)は、常駐メモリも監視するようになりました。生きているメモリが二回続けて予算 × 1.05 を超えていると、メモリが下がるまで、すべてのシャードがデータを増やす書き込みを -OOM command not allowed when the process holds more memory than the tiering budget allows で拒否します。INFO # Tiering に tier_rss_line_bytes、tier_refusing_writes、tier_live_bytes、tier_overhead_bytes が加わります。

5. インデックスのサイズが実際どおりに報告される

IDX.LIST、IDX.VERIFY、TABLE.VERIFY の bytes は行あたり value + key + 48 という見積もりでした。今はインデックスの葉が実際に持つバイト数で、インデックス自体も以前よりずっと小さくなっています。キーが row:<n> のような i64 インデックスは 1 行 16〜25 バイトと報告され(6.4 では 67 バイト)、実際の使用量は 6.4 の 4 分の 1 以下です。同じ数字がインデックスの MAXMEM と、階層ストアがインデックスのために取っておく領域に使われるため、次のようになります。

  • 6.4 に合わせて決めた MAXMEM のインデックスは、-INDEXOVERBUDGET になるまでに数倍の行を持てます。
  • 同じインデックスのもとで、階層ストアはより多くのデータをホットに保ちます。

6.4 の見積もりより大きく報告されるケースが二つあります。

  • 小さなインデックス:その行を一つでも持つシャードは、少なくとも 1,784 バイトの葉を一つ持ちます。実測では、1 行のとき 6.4.0 で 59 バイト、7.0 で 1,816 バイト、二シャードに 3 行のとき 186 バイトと 3,632 バイトでした。数キロバイトの MAXMEM だと、構築を拒否することがあります。
  • 値が長い文字列で、キーが数字でないインデックス:行がランダムな順に届くと、バックグラウンドの詰め直しが届くまで葉が 60〜70% しか埋まりません。

上限ぎりぎりに決めた予算は、実データを入れたサンプルで IDX.LIST と照らして確かめてください。行あたりの式は indexes.md にあります。

6. IDX.LIST と IDX.DESCRIBE に対が一つずつ増える

  • IDX.LIST の各行の末尾に partitioning local|global が付きます。グローバルインデックスには partitions、max_entries、mean_entries も加わります。
  • IDX.DESCRIBE は declaration の直前に partitioning を示します。declaration は引き続き最後の対です。

キーで応答を読むクライアントには、無視できる新しいキーが見えるだけです。位置で数えるクライアントはそうはいきません。

7. 組み込みの MSET はシャードごとに一フレーム

組み込みストアはこれまで、MSET の各ペアをそれぞれのロックの下で設定し、それぞれ SET として記録していました。今は一つのシャードのペアを一つのロックの下で設定し、一つの MSET フレームとして記録するので、クラッシュしても各シャードの分は全部残るか全部残らないかのどちらかです。AOF、レプリカ、変更フィードには、キーごとの SET の代わりに MSET フレームが届きます。SET だけを扱うフィードの利用者は MSET も扱ってください。サーバーに対してはもともとそうする必要がありました。

ストリームの書き込みの記録のしかたも変わりました。サーバーでも組み込みストアでも同じで、再生したときにクライアントが受け取った応答と同じ結果になるようにするためです。生成 ID の XADD は、返した ID で記録されます。クレーム(XCLAIM、XAUTOCLAIM)とグループ読み取り(XREADGROUP)は、触れたエントリーについての XCLAIM … FORCE JUSTID フレームとして記録され、読み取りではグループがどこまで進んだかを示す XGROUP SETID が加わります。応答した待機中のブロッキング pop は、実際に行った LPOP / RPOP として記録されます。これらのコマンドを解釈する変更フィードの利用者には、記録された形が届きます。

8. 使用中のポートは拒否される

各シャードは SO_REUSEPORT で待ち受けます。そのため、同じユーザーが同じポートで二つ目の kevy を起動すると、一つ目の待ち受けに加わっていました。両方が動き、それぞれが接続の一部を受け取るので、クライアントが読み返す側によっては書き込みが消えたように見えました。今はポートが使用中なら、起動は Address already in use で止まります。Redis と同じです。

9. io_uring:受信リングが小さくなり、転送された処理のあとはポーリングする

  • io_uring リアクターの各シャードは、16 KiB の受信バッファを 4,096 個持っていました。シャードあたり 64 MiB で、トラフィックが一巡するとすべて常駐します。個数は [advanced] recv_buffers で設定でき、既定は 1,024、シャードあたり 16 MiB です。リングが尽きてもエラーにはならず、受信を仕掛け直します。必要だと測って確かめたなら 4096 に戻してください(tuning.md)。
  • 他のシャードから転送された処理をひとまとまり終えると、シャードは 200 µs スリープし、その間は新しい入力を受け付けませんでした。パイプラインのバッチの間に間を置くクライアントは、そのたびにこれを待たされていました。今はその 200 µs の間ポーリングを続けます。その代わり、転送された処理がまとまって来るたびに、シャードが休止するまで最大 200 µs 分、コア一つを使います。他のプロセスと共有するマシンでは、これが CPU 時間として見えます。

10. kevy-config:設定セクションの構造体にフィールドが増える

暗号化したリンクと、プロキシの後ろに置けるクラスタのために設定項目が増えました。設定項目はどれも公開フィールドです。

構造体新しいフィールド
Configsecure(型は SecureSection:private_key_file、listen_port、client_keys、cluster_port_base、announce_cluster_port_base)
ClusterSectionannounce_ip、announce_port_base、secure、peer_keys
PeerEntryrepl_port_base
ReplicationSectionsecure、upstream_key、replica_keys
AdvancedSectionrecv_buffers

すべてのフィールドを書いた構造体リテラルはコンパイルできなくなります。

// 6.4
let repl = ReplicationSection { role, upstream, listen_port_base, /* … every field */ };
// 7.0
let repl = ReplicationSection { role, upstream, listen_port_base, ..Default::default() };

上の表のうち既定値がないのは PeerEntry だけで、その新しいフィールドは repl_port_base: None と書けば以前と同じ動作です。Config::load や Config::from_toml_str で作った設定は変更不要です。新しい設定項目の既定値はどれも無効で、6.4 向けに書いた kevy.toml はそのまま読み込めます。

11. ランタイムを組み込む Rust 利用者へ

kevy_rt::Commands に take_ext_out、apply_ext、extension_targets が加わりました。いずれも 6.4 の動作を保つ既定実装つきです。シャード間でメッセージを運び(書き込みはそれらが適用されてから応答します)、拡張の読み取りが必要なシャードを指定できるようにします。グローバルインデックスはこの上に作られています。さらに snapshot_aux、load_snapshot_aux、on_restored が加わりました。既定実装はキー空間のほかに何も保存しません。コマンドセットはこれらで自分の状態をすべてのスナップショットと書き直したログに残し、全シャードの復元が終わったところで一度だけ整えられます。インデックス・ビュー・テーブルのカタログはこの仕組みで保存されています(§1)。さらに xreadgroup_refusal が加わりました。既定実装は何も拒否しません。XREADGROUP を扱うコマンドセットは、そのコマンドが拒否する内容をここで返します。これで複数のシャードにまたがるストリームの読み取りを、どれかが読む前に各シャードで確かめられます。

12. kevy-cli:ツールは --kevy の後ろでだけ動く

kevy-cli 6.4 はツールを裸の単語で動かしていました(kevy-cli doctor -p 6004、kevy-cli export …、kevy-cli sql compile f.sql --apply --url h:p)。6.x の間は、--kevy での書き方を示す行を 1 行表示していました。7.0 では裸の単語はサーバーのコマンドで、redis-cli と同じです。そのため古い書き方は DOCTOR をサーバーに送り、サーバーのエラーを表示します。ツールを --kevy の後ろに、接続オプションをその前に移してください。

# 6.4
kevy-cli doctor -p 6004
kevy-cli sql compile schema.sql --apply --url h:p
# 7.0
kevy-cli -p 6004 --kevy doctor
kevy-cli -h h -p p --kevy sql compile schema.sql --apply
kevy-cli -p 6379 --kevy diff 127.0.0.1:6380 user:        # the first server is the session's

digest <prefix> と、ファイルを扱う形の backup/restore は --kevy digest|backup|restore です。古い書き方のためにあったライブラリ関数(route_tool、run_doctor_cli、run_shadow_cli、run_lint_cli、run_backfill_keys_cli)も一緒に削除しました。

13. Rust API

ワークスペースの公開 Rust API はすべて Rust API Guidelines に揃えたので、kevy のクレートをライブラリとして使うコードはそのままではコンパイルできません。変更はいくつかの規則に従っていて、直す場所はコンパイラが一つずつ示します。

  • bool の引数は意味を名前にした列挙型になりました。store.copy(a, b, true) は store.copy(a, b, CopyMode::Replace) です。
  • ライブラリが今後拡張しうる構造体と列挙型は #[non_exhaustive] です。構造体リテラルではなく Default か with_* ビルダーで作り、それに対する match には _ の腕を足してください。
  • 不変条件を持つ型はフィールドを非公開にし、同じ名前のメソッドを用意しています。
  • ある型を第一引数に取っていた自由関数は、その型のメソッドになりました。
  • エラーは String ではなく、std::error::Error を実装した型です。その文面がプロトコルに出ていた場合は、as_wire() か to_wire() が同じ文面を返します。
  • 変更フィードとレプリケーションの(generation, offset)の対は、一つの FeedPosition になりました。

組み込みストアでよくある書き換えは次のとおりです。

// 6.4
store.copy(b"src", b"dst", true)?;
store.linsert(b"l", true, b"c", b"b")?;
let (generation, offset) = store.changes_tail()?;
let batch = store.changes_since(generation, offset, 100, &[])?;
let dropped: bool = store.idx_drop(b"by_age");
// 7.0
store.copy(b"src", b"dst", CopyMode::Replace)?;
store.linsert(b"l", InsertPosition::Before, b"c", b"b")?;
let tail: FeedPosition = store.changes_tail()?;
let batch = store.changes_since(tail, 100, &[])?;  // batch.next is the next position
let dropped: bool = store.idx_drop(b"by_age")?;     // a closed store or a replica is an error now

keys_iter は、キー空間のコピーではなく一ページ分だけを持つ KeysIter を返します。すべての変更は、クレートごとに旧 → 新の形で rust-api-7.0.md に一覧があります(英語)。

14. バインディング:Go モジュールのパスと読み取り専用エラーの文面

  • Go。 モジュールは github.com/goliajp/kevy-go/v7 です。import パスを変えて go get してください。Go API のほかの部分は変わっていません。

``go // 6.4 import kevy "github.com/goliajp/kevy-go/v6" // 7.0 import kevy "github.com/goliajp/kevy-go/v7" ``

  • 読み取り専用エラーの文面。 C++、C#、Go、Python、Tauri、TypeScript の各バインディングが自前で組み立てる読み取り専用エラーは、サーバーの応答と完全に同じ READONLY You can't write against a read only replica. になりました。五つは末尾のピリオドがなく、Tauri プラグインは READONLY the store is a read-only replica と返していました。文字列全体で照合するコードは新しい文面が必要です。READONLY の接頭辞やエラーの型で照合するコードは変更不要です。
  • Android。 KevyDB.mget はコマンドの経路で MGET を実行するのではなく、ネイティブ呼び出し一回で済むようになり、KevyDB.mset(vararg pairs) が加わりました。結果は同じです。
  • Tauri。 プラグインの Rust の Error に Other バリアント(kind() は "Other")が加わり、pub/sub イベントの count は i64 になりました。プラグインが知らない種類のイベントは webview に転送しなくなりました。JavaScript API は変わっていません。
  • ほかのすべての入口(Node、TypeScript、Electron、Expo、Nitro、Flutter、Python、C#、Java、Swift、C++、wasm パッケージ)は API を保っています。組み込みの入口から観測できるエンジンの変更は、§1、§7、§15 のものと、ストリームと geo のコマンドです。これらのコマンドは、組み込みエンジンが汎用のコマンド経路で扱うようになりました(その経路では BLOCK つきの読み取りは拒否されます)。ダウングレードは §1 に従います。

15. 変わった応答

以下はどれも Redis と同じ応答になったか、誤りだった文面を取り除いたものです。

  • XRANGE、XREVRANGE などストリーム ID を受け取るコマンドは、5- のようにハイフンの後ろが空の ID を ERR Invalid stream ID specified as stream command argument で拒否します。6.4 は 5-<最大のシーケンス> として読んでいました。
  • XAUTOCLAIM のカーソルは次の保留エントリの ID で、最後まで見終わると 0-0 です。6.4 は最後に見た ID に 1 を足したものを返していたので、末尾に達した呼び出しが 0-0 ではなくカーソルを返していました。一回の呼び出しで見るのは Redis と同じく最大 COUNT × 10 件で、6.4 は一覧をすべて見ていました。カーソルが 0-0 になるまで呼び続けるループはどちらでも動き、7.0 では呼び出しが一回少なくなります。
  • ほかのストリームのコマンドも valkey 9.1 と同じ応答になりました。変更の一覧は CHANGELOG にあります。クライアントがまず出会うのは次のとおりです。タイムアウトしたブロッキングの XREAD と XREADGROUP は RESP2 で *-1 を返します(6.4 は $-1)。RESP3 ではどちらもストリームからエントリーへのマップを返します。ID を指定した XREADGROUP はコンシューマーの保留エントリーを渡し直して配送回数を増やし、削除されたエントリーはフィールドなしで返します。近似トリム(MAXLEN ~、MINID ~)は 100 エントリーのノード単位でしか削らないので、6.4 が正確に削っていた場面で何も削らないことがよくあります。XCLAIM はキーやグループがないと NOGROUP を返し(6.4 は ERR no such key)、LASTID を受け付けます。キーがないときの XGROUP DESTROY、CREATECONSUMER、DELCONSUMER は拒否されます(6.4 は 0 を返していました)。
  • EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT に 0 以下の TTL を渡し、コマンドの実行中にキーの期限が過ぎた場合、0 を返して何も記録しません。6.4 は 1 を返し、削除を記録していました。
  • INFO replication が repl_port_base を報告します。
  • 組み込みストアの table_declare、table_replace、table_verify_report の拒否は -ERR ERR … ではなく -ERR … になりました。
  • 提供できない TABLE.DECLARE … WINDOW を拒否する文面から、途中の余分な空白 17 個を取り除きました。
  • すべての言語バインディングが通る Store::dispatch_argv の経路では、INCRBYFLOAT がサーバーと同じく保存された値そのものの桁で応答します。閉じたストアやレプリカへの不正な書き込みは、引数を検査する前に、閉じている旨か READONLY で拒否されます。レプリカの READONLY 応答は、サーバーと同じくピリオドで終わります。

16. データを失ったり変えたりしていた欠陥の修正

以下はいずれも、エラーを出さずに書き込み・期限・キーを失ったり、値を変えたりすることがありました。括弧内のバージョンは、その欠陥を含む最初のリリースです。

  • スナップショットのあとに AOF を書き直すと、書き直し前にリストへ入れた要素が再起動で二回適用されていた。書き直したログの下でスナップショットも読み込んでいたため(サーバーは 1.0.0 から、組み込みストアは 1.16.0 から)
  • COPY … REPLACE(6.0.0 から)とシャードをまたぐ RENAME(5.0.0 から)が、再生やレプリケーションで宛先の既存の値に書き込まれ、ハッシュのフィールドやリストの要素が古い値に混ざっていた
  • BLPOP と BRPOP による取り出しが AOF(1.4.0 から)にもレプリカ(1.18.0 から)にも届かず、取り出した要素が再起動後に戻っていた。BZPOPMIN、BRPOPLPUSH、XREADGROUP … BLOCK で待っていて応答を受けた側も記録されていなかった
  • BGSAVE のあとに書かれたデータ。macOS と、io_uring を使わない Linux で(5.1.0 から)
  • AOF 書き換えの最後のファイル入れ替えの間に書かれたデータ(io_uring では 5.0.0 から、epoll と kqueue では 5.1.0 から)
  • トランザクションの途中でプロセスが死んだあとに書かれたデータが、その次の再起動で失われていた(4.0.0 から)
  • バックグラウンドの AOF 書き換え後の、ハッシュフィールド自身の TTL(3.0.0 から)
  • GETEX key EX|PX(6.0.0 から)と条件つきの HEXPIRE(3.0.0 から)の期限が、再起動で動いていた
  • RESP3 での個数つき SPOP が、再起動やレプリカでの再生で別のランダムな取り出しになっていた(6.3.0 から)
  • ストリームへの書き込みが、再起動やレプリカでの再生で、応答したときとは違う結果になっていた。XADD * は新しい ID を生成し、XCLAIM や XAUTOCLAIM は別のエントリを取得し、グループでの読み取りは保留中のエントリをすべて配送したばかりのように見せていた(1.4.0 から、レプリカでは 1.18.0 から)
  • 相対 TTL を指定する書き込みをサーバーが記録するちょうどその時に TTL が切れたキーが、expired 通知も期限のフレームもなしに削除されていた(1.8.1 から)
  • 大きなキー空間でインデックスの直後に宣言したマテリアライズドビューが、VIEW.REBUILD まで行の一部しか持っていなかった(3.0.0 から)。また、既存の行の上に作った TOPK つきの DESC ビューが、シャードごとに最大ではなく最小の行を残していた(3.0.0 から)
  • コマンドを介さずに変わった行(期限切れ、追い出し、スクリプトによる別のキーへの呼び出し、レプリカのフル再同期)のインデックスのエントリが、その行を指す書き込みが後で来るまで古いままだった
  • 異なるシャードで同時に実行されたカタログコマンドが、両方とも OK を返したあとで互いの変更を落とすことがあった(3.0.0 から)
  • 組み込みストアで:TTL つきのキーが、期限を過ぎてもリーパーの最大 1 周期分読めていた(1.11.0 から)。文字列以外の COPY が WRONGTYPE を返していた(2.0.13 から)。SET … NX EX が値と TTL を別々のロックの下で設定していたため、その間でクラッシュすると期限の切れないキーが残った(4.0.0 から)。組み込みレプリカが、ストリームと geo の書き込みをすべて捨てていた。また(シャードが二つ以上のとき)ほとんどのキーを、読み取りが探さないシャードに書いていた(1.22.0 から)

それぞれの詳細は changelog にあります。

17. サーバーは kevy-alloc で動く

サーバーのバイナリは、システムアロケータ(Linux では glibc malloc)ではなく、kevy 独自のアロケータ kevy-alloc で割り当てるようになりました。空いたページを OS に返し、シャードの tick 上の整理パスが降格や削除の残す穴を詰めます。何をするか、何を測ったかは alloc.md にあります。設定は要りません。6.4 から上げると次の点が変わります:

  • INFO modules は module:name=alloc,impl=kevy-alloc になる(6.4 は impl=system)
  • INFO allocator にセクションが出る:alloc_mapped、alloc_live と、マップされた各バイトが属するほかの項目。6.4 のリリースビルドにはなかった
  • 階層化したサーバーのメモリガードはこれらの数値を読み、trim は一度もしない:heap_trims_total は 0 のまま
  • LD_PRELOAD で差し込むアロケータ、glibc の MALLOC_* 環境変数、malloc をフックするプロファイラは、サーバーの割り当てに効かなくなる

システムアロケータのままにするには、--no-default-features でビルドします(kevy-alloc は kevy crate の唯一のデフォルト feature なので、ほかは何も変わりません):

cargo install kevy --version 7.0.0 --no-default-features

リリースのバイナリとコンテナイメージはデフォルトビルドなので、システムアロケータのサーバーはソースからビルドすることになります。


入手

# Cargo.toml
kevy-embedded = "7.0.0"
npm install @goliapkg/kevy@7.0.0          # wasm
npm install @goliapkg/kevy-node@7.0.0     # Node native
pip install kevy==7.0.0
go get github.com/goliajp/kevy-go/v7@v7.0.0
cargo install kevy --version 7.0.0        # the server binary, from source
npm install -g @goliapkg/kevy-bin@7.0.0   # the server binary, prebuilt

Linux(x86-64、arm64)と macOS(arm64)向けのビルド済みサーバーバイナリは、それぞれ SHA-256 ファイルとともに v7.0.0 release にあります。ghcr.io/goliajp/kevy:latest を使うコンテナの利用者は次の pull で 7.0.0 になります。固定のタグは ghcr.io/goliajp/kevy:7.0.0 です。