kevy上のRDSワークロード
MySQLやPostgreSQLの上で運用系のワークロードを回していて、それを——全部または一部——kevyへ移すことを検討している。このページはそのためのリファレンスマトリクスです。SQL面の構成要素ひとつひとつについて、kevyでは何がそれを担うのか、正確なverbは何か、そしてセマンティクスの差分は何かを示します。kevyがある構成要素を断る場合、その拒否は、手当ての代替とともに、はっきりと述べられます。対になるクックブックは、これらの行を実行可能なレシピに変えます。designing-on-kevyは、エンジンそのものの1ページの地図です。
分業
RDSでは、まずデータを書いておいて、アクセスパスはあとで決められます。クエリプランナが、どんなWHERE句からでも、クエリ時に*なんらかの*プランを作るからです。kevyはそれを反転させます。アクセスパスは書き込み時に宣言し(インデックス、集約、ビュー)、エンジンがそれをすべての書き込みと同期して維持し(構成上の派生物、ドリフトゼロ)、サービングはマイクロ秒スケールのルックアップになります。プラン探索は決して起きません。取引は明示的です。
- 手放すもの:宣言されていないパス上のアドホッククエリ。マッチするインデックスのない
WHEREは、kevyでは遅いのではありません——*設計上、存在しない*のです(アクセスパスをモデル化するか、そのクエリを出さないか)。 - 得るもの:予測可能なサービング遅延(hydratedページのp99 < 1ms、index+viewフック経由の書き込みファンアウトのp99 < 200µs——フルスタックを載せた1台のサーバー上で。ゲートされたラインはdesigning-on-kevyにあります)、そして、ディスク上のB木エンジンが出す値をはるかに超える、Redisパリティの生スループット(サイジングを参照)。
この境界はバックログではなく、憲章です。クエリ言語なし、プランナなし、JOINなし、サーバーサイドのバリデーションDSLなし、トリガーなし(「法3」、designing-on-kevy)。以下はすべて、SQLの構成要素をこの固定された面の上へ写像したものです。
テーブル、行、カラム
| RDS | kevy |
|---|---|
| テーブル | キープレフィックス(user:) |
| 行 | プレフィックス配下のハッシュ(user:42) |
| カラム | ハッシュフィールド |
| PRIMARY KEY | キーそのもの |
| NULL | フィールドが存在しないこと(センチネル文字列は決して使いません) |
HSET user:42 name ada email ada@example.com age 36
HGETALL user:42 # SELECT * WHERE id = 42
HGET user:42 email # SELECT email WHERE id = 42
HMGET user:42 name age # SELECT name, age WHERE id = 42存在しないフィールドへのHGETはnilを返します——それがNULLのセマンティクスそのものです。そしてインデックスの仕様は、フィールドの欠落を「行を除外」として扱います(数えられ、IDX.VERIFYで可視になります)。
型システム
kevyはバイト列を格納します。型はそれが意味を持つ場所——インデックス作成時——に宣言します(TYPE i64|f64|str|vector)。宣言された型強制に失敗したフィールドを持つ行は、そのインデックスから除外され、数えられます(IDX.VERIFY / IDX.LISTのcoerce_failures)——宣言的なフェンスであって、書き込みエラーでは決してありません。
| RDSの型 | kevyでの形 | 備考 |
|---|---|---|
| INT / BIGINT / SERIAL | フィールドのバイト列、インデックスはTYPE i64 | i64の全域 |
| DECIMAL / NUMERIC | 整数の補助単位をi64で(セント、マイクロ) | kevyにdecimal型はありません。f64は2進浮動小数点です——金額を決してそこに入れないでください。KIND aggの合計はf64で累積されるため(精度の限界は文書化済み)、総額が2^53を十分下回るような単位を選んでください |
| FLOAT / DOUBLE | フィールドのバイト列、インデックスはTYPE f64 | IEEE 754のセマンティクス |
| VARCHAR / CHAR / TEXT | フィールドのバイト列、インデックスはTYPE str | バイナリセーフ。辞書式のバイト順序 |
| BLOB / BYTEA | フィールドのバイト列(インデックスなし)、または専用の文字列キー | 値はどこでも任意のバイト列です |
| DATETIME / TIMESTAMP | Unixエポック(秒またはミリ秒)をi64で | rangeインデックスがBETWEENを与えます。kevyに日付の算術はありません——暦の計算はアプリがやります |
| BOOL | 0 / 1をi64で | インデックス可能、合成可能(EQ 0) |
| JSON / JSONB | 平坦化したハッシュフィールド(profile.city) | フィールド単位の読み出し、TTL(HEXPIRE)、インデックス可能性。JSONパスクエリは恒久的に対象外です——クックブックのレシピ9 |
| 埋め込み(pgvector) | dim × 4バイトのf32-LE、インデックスはTYPE vector | vector-search |
PRIMARY KEY、UNIQUE、AUTO_INCREMENT
主キー —— キーそのものです。ポイントルックアップ(WHERE pk = ?)はGET/HGETALL/HMGETです。1シャード、ハッシュプローブ1回、インデックス不要です。
AUTO_INCREMENT / シーケンス —— INCR seq:orderがidを1つ発行します。INCRBY seq:order 100はブロックを確保し、アプリのメモリから配っていけます(高スループット版)。クラッシュ時に穴が空くのは、PostgreSQLのシーケンスが与えるのと同じ契約です(クックブックのレシピ3)。
UNIQUE —— KIND uniqueはフェンスであって、ロックではありません。書き込みをブロックしません(ブロックすれば、シャードをまたぐ書き込みを直列化することになります)。重複は数えられ(IDX.VERIFYのduplicates)、EQ読み出しが複数ヒットする形で可視になります。硬い一意性のゲートが必要なら、次のいずれかを使ってください。
SET uniq:email:ada@example.com 42 NX # atomic claim, NX = the gate
WATCH + MULTI/EXEC check-then-write # CAS loop (cookbook recipe 4)
{hashtag} prefix + single-shard domain # cluster modeSELECT
| SQL | kevy | verb |
|---|---|---|
WHERE pk = ? | キーの直接読み出し | GET / HGETALL / HMGET |
WHERE col = ? | 宣言済みインデックス上の等価 | IDX.QUERY idx EQ v [FIELDS f…] |
WHERE col BETWEEN a AND b | 宣言済みインデックス上の範囲スキャン | IDX.QUERY idx RANGE a b |
WHERE col > a | 半開区間 | RANGE a <max-sentinel> |
WHERE a = ? AND b BETWEEN … | 2インデックスの合成 | IDX.QUERY COMPOSE AND idxA EQ v idxB RANGE a b |
WHERE col IN (v1, v2) | 2本足のOR、またはアプリ側でN回のEQクエリ | IDX.QUERY COMPOSE OR idx EQ v1 idx EQ v2 |
LIKE 'abc%'(前方一致) | strのrangeインデックス、プレフィックスを境界に | IDX.QUERY idx RANGE 'abc' 'abc\xff' |
LIKE '%abc%'(中間一致) | そのままでは非対応 | トークンの形をしているなら全文のMATCH。さもなければフィールドを設計し直す |
| 全文検索 | KIND text、BM25 | IDX.QUERY idx MATCH "…" |
SELECT col1, col2(射影) | フィールドのhydration | … FIELDS col1 col2 |
COUNT(*) WHERE … | 実体化せずに数える | IDX.COUNT idx RANGE a b |
EXPLAIN | パース + プラン行、実行なし | IDX.EXPLAIN idx RANGE a b |
セマンティクスと限界を、正直に。
- インデックス1つにつきフィールド1つ。 インデックスは、1つのプレフィックスの、宣言された1つのフィールドを覆います。複数カラムの述語は、ちょうど2つのインデックスの
COMPOSE AND|OR(キー順です——2つの値ドメインが異なるので、キー順だけが共有できる順序です)か、最大4葉までの名前付き・再利用可能な合成であるビューになります。それより広いアドホックな連言は、クエリプランナへの坂道です——断ります。残余の述語は、hydrationのあとアプリ側でフィルタしてください。 RANGE/EQ/COMPOSE:デフォルトLIMIT 100、最大10000、カーソルページング([next-cursor, rows]、"0"は開始または枯渇)。カーソルの契約はSCANクラスです。走査を通じて安定していた行はちょうど1回見えます。並行する書き込みは現れるかもしれないし、現れないかもしれません。グローバルスナップショットはありません——SCAN/DBSIZEと同じ包絡線です。- 前方一致の
LIKE:TYPE str KIND rangeのインデックスは辞書式のバイト順序で並ぶため、LIKE 'abc%'はRANGE 'abc' 'abc<0xff>'になります——キー空間の走査ではなく、インデックススキャンです。(SCAN 0 MATCH 'user:*'によるキー名の前方一致も存在しますが、これはキー空間全体を漸進的に歩きます——運用の雑務に使うものであり、サービングパスとして使ってはいけません。) MATCHはクエリトークン上のORセマンティクスで、BM25でランクづけされ、CJKのbigramが組み込まれています。LIMITは1000で頭打ち、カーソルなし、フレーズクエリなし、ブール構文なし、ハイライトなしです(text-search)。FIELDSは、各行を所有するシャード上で、指定されたハッシュフィールドを同じ呼び出しの中でhydrateします——「インデックススキャン + 主キー参照」という二重ホップの、ワンホップの置き換えです。これはJOINの形をしたhydrationのプリミティブでもあります(後述)。IDX.EXPLAINは診断専用です。 kind、state、est_rows、そしてあなたがすでに書いたクエリのプラン行を報告します——複数のプランから選ぶオプティマイザは存在しません。
ORDER BY / LIMIT / OFFSET
- rangeインデックスが順序そのものです。
IDX.QUERY … RANGEは、インデックス値の昇順で行を返します。ORDER BY col ASC LIMIT nは、colにrangeインデックスを宣言してLIMIT nでクエリすることに等しくなります。 - 素のインデックス面での降順は、
IDX.QUERYのオプションにはありません。ビューを宣言する(VIEW.CREATE … ORDER BY idx DESC)か、書き込み時に符号を反転した(あるいは補数を取った)ソート用フィールドを保存してください。 ORDER BY a, b(複合):複合キーを、書き込み時に1つのインデックス対象フィールドへ符号化してください——有界な整数ならa * 1_000_000 + b、辞書式の複合ならゼロ埋めした文字列です(クックブックのレシピ8)。OFFSETは存在しません。 「N行読み飛ばす」は意図的に存在しません——どのエンジンにおいてもO(N)の浪費であり、深いところではアンチパターンだからです。返されたカーソルでページングしてください(ページあたり定数コスト、SCANクラスの契約のもとで位置が安定します)。プロダクトが「47ページ目へ飛ぶ」を必要とするなら、ページのアンカーを事前計算するか、UXを考え直してください。kevyはそのコストを隠しません。
GROUP BYと集約
KIND aggは、集約を書き込みパスの中で維持します——GROUP BYの反転です。RDSはクエリ時に行をスキャンしますが、kevyは各書き込みを、それが着地するそばから自分のグループへ畳み込みます。だからグループの読み出しはO(1)です。
IDX.CREATE ord_amt ON PREFIX ord: FIELD amount TYPE i64 KIND agg GROUPBY status
IDX.QUERY ord_amt GROUP paid → [count, sum, min, max, avg]
IDX.QUERY ord_amt GROUPS BY sum LIMIT 100 → ranked [group, count, sum, min, max]GROUP gはSELECT COUNT(*), SUM(v), MIN(v), MAX(v), AVG(v) WHERE group = gです。GROUPS BY count|sum|min|maxは、ランクづけされたGROUP BY … ORDER BY agg LIMIT nです(LIMITは1000以下)。- min/maxは削除のもとでも正確なままです(グループごとの値の多重集合)。sumはf64で累積されます——金額を扱うなら精度の限界に注意してください(総額が2^53を十分下回るように補助単位のサイズを取ること)。
HAVINGなし、集約式なし、任意の述語に対するGROUP BYなし —— (有界な)GROUPSの結果をアプリでフィルタしてください。インデックス1つにつきgroup-byフィールドは1つです。複数のグループ分けが要るなら、aggインデックスを複数宣言してください。
JOIN
kevyはJOINしません。 2つのプレフィックスをサーバーサイドで結合するverbは存在せず、これからも存在しません(法3)。置き換えは3つあり、望ましい順に挙げます。
- 書き込み時に非正規化する。 サービングモデルの答えです。ページが
order.totalの隣にuser.nameを出すなら、注文の行を作るときにuser_nameをそこへ書き込んでください。ストレージは安いですし、書き込みフックがそのコピー上のインデックスも最新に保ちます。親側の更新のファンアウト(表示用フィールドではまれです)は、CDCコンシューマの仕事です。 - アプリ側のhydration(2ホップ)。 外部キーは行の中に住み、インデックスが逆方向をワンホップにします。
IDX.QUERY order_user EQ 42 FIELDS total statusはSELECT total, status FROM orders WHERE user_id = 42です——インデックスがキーを見つけ、同じ呼び出しの中でフィールドをhydrateします(クックブックのレシピ2)。順方向は親ごとのHMGETです——idをまとめてパイプラインしてください。 VIAhydrationを持つビュー。VIEW.QUERY v FIELDS name VIA user:{key.1}は、各メンバーキーをテンプレート経由で*ターゲット*キーへ参照解決し、そこでフィールドを読みます——宣言された、再利用可能な「メンバー→親」の参照解決です(内部ファンアウト1回、クライアントの往復はゼロ)。クエリではなく参照解決なので、ターゲットに述語は付けられません。
SQLに対して失うもの:任意のN方向JOIN、遠い側のテーブルに対するJOIN時のフィルタリング、プランナが選ぶJOIN順序。得るもの:JOINがレイテンシのヒストグラムに現れることは二度とありません。
VIEW
VIEW.*は、宣言済みインデックス上の名前付きAND/OR/DIFF合成に、順序づけ用インデックスを添えたものです——SQLのビューというより、インデックス付きの*materialized*ビューに近いものです。
| SQLビューの性質 | kevyのビュー |
|---|---|
| 任意のSELECT本体 | 不可 —— インデックスの形の木のみ(深さ3以下、葉4つ以下、形はCREATE時に固定) |
| 常に最新(virtual) | MODE virtual —— クエリごとに評価、書き込みコストゼロ |
| materialized + REFRESH | MODE materialized —— 書き込みフックの中で増分維持され、古くなることが決してなく、リフレッシュのジョブも不要。任意でTOPK kにより有界化 |
| ビュー内のORDER BY | ORDER BY <index> [DESC] —— 宣言されたrangeインデックスが順序を供給する |
| ビュー上のビュー | 不可 —— 1階層、インデックスの上のみ |
VIEW.CREATE live_adults QUERY '( AND user_live EQ 0 user_age RANGE 18 200 )'
ORDER BY user_age MODE materialized TOPK 100
VIEW.QUERY live_adults LIMIT 10 [FIELDS name] [VIA …]キラーアプリはホットリストです。「優先度順の準備完了ジョブ上位100件」をTOPK materializedビューにすれば、書き込み税はおよそ2%で、マイクロ秒で答え、恒久的に最新です——RDSが、カバリングインデックスと規律あるクエリと運とを揃えて、ようやく近似できるものです(views)。
トランザクション
kevyのトランザクションの物語はRedisのそれです(法1)。SQLへの写像はこうなります。
| SQL | kevy | 差分 |
|---|---|---|
BEGIN … COMMIT(バッチ) | MULTI … EXEC | コマンドはキューされ、1単位として適用されます。内側でのインタラクティブな読み出しはできません —— 分岐の判断材料にする読み出しはMULTIの前に行います |
SELECT … FOR UPDATE | WATCH key + MULTI/EXEC | ロックではなく楽観的CASです。watchしたキーが変わっていればEXECはnilを返します——読み直して再試行してください(クックブックのレシピ4) |
ROLLBACK | なし | キュー時のエラーはバッチ全体を中止します(-EXECABORT、何も走りません)。EXECの内側の実行時エラーは、ほかのコマンドを取り消しません —— Redisのセマンティクスです |
| ストアドプロシージャ / 1つのアトミックなRMW | EVAL(Lua) | スクリプト全体がKEYS[1]のシャード上の1つのアトミックな単位です。本物の分岐を伴うread-decide-writeができます(lua) |
| 単一エンティティの直列化可能トランザクション | 組み込みのstore.atomic(key, …) | シャードロックされたクロージャ。読み出しは自分の書き込みを見ます。コミットはアトミックで、fsyncは1回です(クックブックのレシピ5) |
| エンティティをまたぐ直列化可能トランザクション | 組み込みのatomic_all_shards | 決定論的なロック順序。ハンマーです——控えめに使ってください |
分離性について、正直に。kevyにMVCCはなく、分離レベルのノブもありません。1つのシャードの内側では、すべてのコマンド(そしてすべてのEVAL、MULTIバッチ、組み込みのアトミックブロック)が直列化されます——1シャード / 1スクリプトに収まるものについては、*直列化可能*な振る舞いが得られます。シャードをまたぐとグローバルスナップショットはありません。マルチキーの読み出し(MGET、IDX.QUERYのマージ)はシャード単位でアトミックであり、全体としてはSCANクラスです。ちぎれた単一コマンドを観測する読み出しはありませんし、コミットされていないMULTIバッチのdirty readもありません。ただし、別々の2つのコマンドをまたぐrepeatable readは存在しません —— それが重要な場面ではWATCH(CAS)かLua(1単位)を使ってください。
「コミット」の耐久性についてはpersistenceを参照してください。appendfsync alwaysなら、ackされた書き込みは応答の前にディスク上にあります。デフォルトのeverysecは最大1秒の窓を開けます(Redisと同じ取引です)。Store::fsync_aof()(組み込み)が、トランザクション単位のsynchronous_commitにあたる脱出口です。
制約とトリガー
| SQL | kevyの答え |
|---|---|
NOT NULL / 型検査 | 型強制のフェンス。インデックスが宣言したTYPEに失敗する行は除外され、数えられます(coerce_failures)。アプリはIDX.VERIFYを見張ります——書き込みの拒否ではなく、宣言的な可視化です |
CHECK (expr) | 不変条件をアトミックな単位の内側で評価します。Luaスクリプト(サーバー)またはatomicブロック(組み込み)が読み、判断し、書きます——判断とコミットが1単位であることをエンジンが保証します(クックブックのレシピ5) |
UNIQUE | フェンス + 数えられる重複、あるいは硬いSET … NXゲート(前述) |
FOREIGN KEY(存在性) | 強制しません。その不変条件が必要なら、1つのアトミックな単位の内側で親→子の順に書いてください |
ON DELETE CASCADE | アプリのパターンです。アトミックブロック(小規模)、kevy-cli delete-prefix(一括)、あるいは親の削除に反応するCDCコンシューマ(非同期——クックブックのレシピ10) |
| トリガー | CDCコンシューマ。FEED.READが、コミットされたすべての書き込みを変更フレームとして配送します——コミット後、疎結合、リプレイ可能で、書き込みパスを壊すことができません(クックブックのレシピ10〜12) |
サーバーサイドの制約DSLもトリガーDSLも、意図的に存在しません。サーバーサイドのロジックはLuaスクリプトだけであり、それは書き込みに*付随して*走るのではなく、書き込み*として*走ります。
セカンダリインデックスのDDL
| SQL | kevy | ||
|---|---|---|---|
CREATE INDEX idx ON t (col) | `IDX.CREATE idx ON PREFIX t: FIELD col TYPE i64\ | f64\ | str KIND range` |
CREATE UNIQUE INDEX | KIND unique(前述のフェンスのセマンティクス) | ||
CREATE INDEX … USING gin (tsvector) | KIND text(text-search) | ||
pgvectorのUSING hnsw | `KIND ann DIM d [DISTANCE cosine\ | l2\ | ip] [M m] [EF ef]`(vector-search) |
DROP INDEX | IDX.DROP idx | ||
\d / information_schema | IDX.LIST / VIEW.LIST(state、entries、bytes) |
- オンラインビルド:
IDX.CREATEは即座に返り、バックグラウンドでバックフィルします(小さい行なら100万件あたりおよそ7秒。数KBのテキスト本体ならおよそ85秒/100万件)。準備できるまでクエリは-INDEXBUILDINGを返します——IDX.LISTをポーリングしてstate=readyを待ってください(migration)。データの可用性がインデックスのビルドを待つことは決してありません。 - インデックスの内容は派生状態です。スナップショットにもAOFにも決して記録されず、再起動後にバックグラウンドで再構築されます。カタログ(宣言のほう)は、データディレクトリのサイドカーに永続化されます。
- インデックスは最大64個です。
MAXMEMはビルドを宣言的に上限づけます(-INDEXOVERBUDGET)。際限なく成長することはありません。 - バルクロードの鉄則:先にインポートし、あとからインデックスを宣言してください——バルク速度でバックフィルするほうが、インポートする行ごとに書き込みフックの代金を払うより速く済みます(クックブックのレシピ15)。
ページングと楽観ロックのパターン
- キーセット / カーソルページング(良いほうの
OFFSET):ページングするすべての面(IDX.QUERY RANGE/EQ/COMPOSE、VIEW.QUERY、SCAN)が[next-cursor, rows]を返します。カーソルを"0"になるまで戻し続けてください。カーソルは不透明で、1回の走査限りのものとして扱ってください。 - バージョンカラムによる楽観ロック:
versionフィールドを持ち、行をWATCHし、読み、MULTI+HSET … version n+1+EXEC。nilが返ればレースに負けたので、再試行してください(クックブックのレシピ4)。
バックアップとPITR
| RDS | kevy |
|---|---|
mysqldump / pg_dump | kevy-cli export(論理的なRESPストリーム。redis-cli --pipeと互換) |
| バイナリバックアップ | スナップショットファイル(SAVE/BGSAVE → dump-<id>.rdb) |
| WAL / binlog | AOF(シャードごとの、追記専用コマンドログ) |
synchronous_commit | appendfsync always(あるいはeverysec + fsync_aofのバリア) |
| PITR(ベース + WALリプレイ) | recovery-point契約:スナップショット + そのスナップショットが記録したカーソル以降のCDCフレーム = 以降の任意のカーソル時点における正確な状態(persistence) |
PITRについてのスコープ上の注意:フィードの窓はインメモリのバックログです(feed_buffer_size、シャードあたり最大1 GiB)。正確な時点への復旧に頼るなら、少なくともその窓が一巡するのと同じ頻度でスナップショットを取ってください。検証は一級市民です。PREFIX.DIGEST / kevy-cli diffが、順序にもトポロジにも依存せずに、2つのキー空間が等しいことを証明します。
レプリケーションと読み出しのスケーリング
プライマリ + N台のレプリカ、デフォルトで非同期——リードレプリカの形です。RDSとの違いはすべて整合性ラダー(availability)にあります。ラダーは、各保証にインスタンス単位ではなく呼び出し単位で値段を付けます。
| RDSでの概念 | kevyの段 |
|---|---|
| 非同期レプリカ(MySQLのデフォルト) | 段0:プライマリはローカル適用後にackし、レプリカは遅れる |
| セッションのread-your-writes | 段1:プライマリでREPL.TOKEN → レプリカでREPL.WAIT token → 次の読み出しが自分の書き込みを観測する |
| 準同期レプリケーション | 段2:書き込みごとのWAIT n timeout —— プライマリ上にあり、かつn台以上のレプリカにackされている(ackはfsyncではありません) |
| レプリカの最大ラグのガード | 段3:replica_max_staleness_ms —— 古いレプリカは、古い読み出しを返す代わりに-STALEを返す |
| スタンバイがなければ書き込みを拒否 | 段4:min_replicas_to_write(-NOREPLICAS) |
| フェンシング / スプリットブレインのガード | 段5:クォーラムリース —— 分断されたプライマリは自分の書き込みをフェンスする |
フェイルオーバー:計画的な引き継ぎはFAILOVER verbです(quiesce → drain → 昇格——ゼロロス)。クラッシュフェイルオーバーは3ノードのelectクォーラムです(MTTRはおよそ5〜8秒。オフセット最良の候補が勝つので、WAITでackされた書き込みは生き残ります)。再合流してくる元プライマリの、レプリケートされなかった末尾は破棄されます(分岐した末尾は、定義上ackされていません)——MySQLの準同期で、まだ送られていないbinlogを失うのと同じ形のデータロスを、明示したものです。劣化した状態はすべて名前を持つエラー(-READONLY、-STALE、-NOREPLICAS、-QUIESCED、-MISDIRECTED)であり、ルーティングクライアントはそこから自己修復します(error-replies)。
CDC
フィードはkevyの「APIとしてのbinlog」です——DebeziumがRDSから抽出しているものを、ネイティブに提供します(cdc)。
| Debezium / binlogでの概念 | kevy |
|---|---|
| binlogの位置 / LSN | シャードごとの(generation, offset)カーソル |
| コネクタのスナップショット + ストリーム | 再構築(自分のプレフィックスをSCAN)+ FEED.TAILから再開 |
| テーブルごとのトピックによる絞り込み | FEED.READのPREFIXフィルタ(マルチキーのフレームにはfail-open) |
| at-least-onceの配送 | 同じです —— コンシューマは冪等でなければなりません |
| トランザクショナルoutboxパターン | 不要:コミットされた書き込みは、すでにそれ自体が変更フレームです(クックブックのレシピ11) |
意図的に提供しないもの:シャードをまたぐグローバル順序(キー単位の順序は、シャードのストリーム内で保証されます)、サーバーサイドのコンシューマ位置、コンシューマグループ。保持期間はインメモリのバックログです。遅れすぎると、再構築の出発点となるカーソルとともに-FEEDRESYNCが返ります。
kevyが決してやらないこと
拒否を1か所にまとめます。どれも憲章上の決定であって(designing-on-kevy、プロジェクトのスコープログ)、ロードマップの穴ではありません。
| 拒否するもの | 理由 | 代わりに使うもの |
|---|---|---|
| SQLパーサ / クエリDSL | プランナへの坂道はここから始まる | 明示的なIDX.*/VIEW.* + このマトリクス |
| クエリプランナ / 自動インデックス選択 | エンジンは宣言されたパスを実行すべきであって、決めるべきではない | IDX.EXPLAIN(診断専用) |
| JOIN | サーバーサイドのプラン探索は存在しない | 非正規化 / hydration(FIELDS、VIA)/ ビュー |
インデックスなしのWHERE | 偶発的なO(n)は、いつかあなたを叩き起こすための嘘である | パスを宣言するか、そのクエリを出さないか |
| サーバーサイドの制約DSL / トリガー | アプリのロジックを蓄えることはエンジンのジャンルではない | LuaのEVAL(アトミックな単位)+ CDCコンシューマ |
| DECIMAL型 | i64/f64を超えるサーバーサイドの算術は持たない | 整数の補助単位 |
| JSONパスクエリ | ハッシュフィールドこそがカラムモデルである | フィールドへ平坦化する(クックブックのレシピ9) |
HAVING / 集約式 | クエリ言語への坂道 | GROUPSの出力をアプリ側でフィルタする |
OFFSETページング | O(N)の読み飛ばしはアンチパターン | カーソル |
マルチデータベースのSELECT n | サーバーごとにキー空間は1つ | プレフィックス、または別インスタンス |
| AUTH / TLS | 信頼境界はネットワークの外周である | ループバックbind(デフォルト)、サイドカープロキシ |
| クロスDCのアクティブ・アクティブ、CRDT | シングルDCの憲章 | アプリレベルのフェデレーション |
| 動的メンバーシップ / 自動置換 / リシャーディング | トポロジはオペレータが宣言する | config + ローリング再起動 |
| HTTP/REST API | RESPとMCPが2つのアクセスプレーンである | 任意のRedisクライアント |
サイジングと運用上の差分
すべてがRAM常駐です。 RDSがディスクからページインしてくるワーキングセットは、ここではメモリに住みます。ディスクは耐久性のログとスナップショットであって、サービングの階層ではありません。キャパシティプランニングを1行で書くと、こうです。
RAM ≈ 行数 × (平均キー長 + 平均行バイト数 + キーあたりのオーバーヘッド) + Σ インデックスごとの式 + ビューのメンバー数 × エントリサイズ ——そのうえで、 ロード済みのサンプル上で
MEMORY USAGE/IDX.LISTのバイト数と突き合わせて検証すること。
サブシステムごとの式(それぞれCIで実測RSSに対してゲートされています):rangeインデックスは概算でrows × (value_width + avg_key_len + 48)。textとANNの式はtext-search / vector-searchにあります(1M × 1024次元のベクトルでおよそ4.1 GiB)。aggはグループ数が支配的です(indexes)。ビューのメンバーは概算でorder_value_width + key_len + 48です(views)。maxmemoryとエビクションポリシーを設定するか、-OOMによる拒否を受け入れてください(書き込みは受付の時点で拒否されます。既存のデータが壊れることはありません)。
サービングの余力。 v3.18.0のリリースアリーナ(kevy対valkey 9.1、fair-fightプロトコル、5回の中央値——bench/PERF-LEDGER.md):GET 3.00倍、SET 3.99倍、INCR 3.00倍、SADD 2.50倍、HSET 2.25倍、ZADD 1.73倍、LPUSH 1.64倍——レプリケーション/ハートビートのパイプラインを載せきったうえで、7戦7勝です。ディスクファーストのRDSに対するポイント読み出しでは、差はさらに大きくなります。ただしそこでの正直な比較は、クエリ単位のベンチマークではなく、「kevyはキャッシュ層*と*運用系クエリの両方を置き換える」というものです。
RDSに対する運用上の差分を、手短に。キー空間は1つ(スキーマもデータベースもありません——プレフィックスが名前空間です)。TTLは一級市民です(EXPIRE、フィールド単位のHEXPIRE——cronの削除ジョブが消えます)。FLUSHALLはコマンド1つで届きます(外周で守ってください)。派生状態(インデックス、ビュー、フィード)は再起動後にロードされるのではなく再構築されます(100万行あたり数秒——再起動時間に織り込んでください)。そしてエラーの面は契約の一部です——クライアントはメッセージをパースするのではなく、エラーのプレフィックスにマッチさせるべきです(error-replies)。
このページとクックブック
このページはリファレンスマトリクスです——SQLの構成要素を入れると、kevyの形が出てきて、差分が述べられます。クックブックはレシピ集です——20の実行可能なパターン(コマンドブロックはすべてCIでsmokeされています)。それぞれが、置き換えるSQLの構成要素でタグづけされ、上のマトリクスの行へ相互リンクされています。本当に移行するのですか。段階を追ったプレイブックはmigrationにあります。