kevy
このページ

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にあります)、そして同じ耐久性設定ではRDSが追いつけない書き込み吸収力(サイジングを参照)。

PostgreSQL 18との実測比較

主張ではなく、実行したものです。200万行/854 MBのCSV、同一のharnessで両方を駆動、PGは素の設定、kevy 5.4 の 3 パス交互の中央値:

kevy(everysecPG 18 素の設定
単一行更新 p9941 µs1,676 µs
単一行更新 p99(耐久性を揃えた場合。書き込みごとfsync)3,339 µs1,676 µs
セカンダリインデックス参照 p99160 µs154 µs
一覧ページ p99(絞り込み+範囲+整列+件数制限)200 µs162 µs
CSV 1 MBあたりのメモリ5,821 KB883 KB

この数値は、同じものを比べていなかった一組を置き換えるものです。 5.4 までは kevy 側の読み取り形状が列をひとつも要求しておらず、対する SQL は 2〜3 列を選んでいました。さらに kevy のテーブルは、行が実際に持つ 7 列のうち 6 列しか宣言していませんでした。どちらも修正され、どちらも答えを動かしました:セカンダリインデックス参照は 212 µs 対 PostgreSQL の 126 µs として公開されていましたが、いまは 4% 以内の互角です。

向きの違う結論として読んでください。1秒の損失を許容するなら、kevyは書き込みを41倍速く吸収します——RDSの前段にキャッシュを置かせるのは、まさにこの圧力です。書き込みごとにfsyncする同条件では、PostgreSQLが2.0倍速い:PGはWALをグループコミットし、kevyはコマンド単位でfsyncします。索引参照は互角、一覧ページはPostgreSQLが1.2倍速い——B木の上のプランナは本業で強いのです。そして同じデータを保持するのに kevy は6.6倍のメモリを使います:行をプロセス内に置けばRAMを食います。データが収まらなくなったとき——11.5 GBのCSV、PostgreSQLはメモリ2 GBに制限、kevyは1 GBのティアリング予算——持ち主が変わるのはランダムアクセスの形だけです。ランダムな主キーでの参照はPG 184 µsに対しティアリングkevy 78 µs。kevyの行はディスク上にありながら、PGのバッファプールミスより速く答えます。上の索引形状は依然PGが勝ちます。しかも表全体に散る高カーディナリティ述語で測り直すと、差は縮まるどころか広がりました——索引参照で176 µs対385 µsです。I/Oの効果ではありません:kevyは全データをRAMに置いていても負けるので、コストは取得ではなくクエリ経路にあります。ティアリングは同じ12 GBを17.95 GBではなく2.95 GBのRSSで保持し、しかもその索引参照は、ディスク上にある98%の行に触れずに答えます。

この境界はバックログではなく、憲章です。クエリ言語なし、プランナなし、JOINなし、サーバーサイドのバリデーションDSLなし、トリガーなし(「法3」、designing-on-kevy)。以下はすべて、SQLの構成要素をこの固定された面の上へ写像したものです。

テーブル、行、カラム

RDSkevy
テーブルキープレフィックス(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.LISTcoerce_failures)——宣言的なフェンスであって、書き込みエラーでは決してありません。

RDSの型kevyでの形備考
INT / BIGINT / SERIALフィールドのバイト列、インデックスはTYPE i64i64の全域
DECIMAL / NUMERIC整数の補助単位i64で(セント、マイクロ)kevyにdecimal型はありません。f64は2進浮動小数点です——金額を決してそこに入れないでください。KIND aggの合計はf64で累積されるため(精度の限界は文書化済み)、総額が2^53を十分下回るような単位を選んでください
FLOAT / DOUBLEフィールドのバイト列、インデックスはTYPE f64IEEE 754のセマンティクス
VARCHAR / CHAR / TEXTフィールドのバイト列、インデックスはTYPE strバイナリセーフ。辞書式のバイト順序
BLOB / BYTEAフィールドのバイト列(インデックスなし)、または専用の文字列キー値はどこでも任意のバイト列です
DATETIME / TIMESTAMPUnixエポック(秒またはミリ秒)をi64rangeインデックスがBETWEENを与えます。kevyに日付の算術はありません——暦の計算はアプリがやります
BOOL0 / 1i64インデックス可能、合成可能(EQ 0
JSON / JSONB平坦化したハッシュフィールド(profile.cityフィールド単位の読み出し、TTL(HEXPIRE)、インデックス可能性。JSONパスクエリは恒久的に対象外です——クックブックのレシピ9
埋め込み(pgvector)dim × 4バイトのf32-LE、インデックスはTYPE vectorvector-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.VERIFYduplicates)、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 mode

SELECT

SQLkevyverb
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、BM25IDX.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と同じ包絡線です。
  • 前方一致のLIKETYPE 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、そしてあなたがすでに書いたクエリのプラン行を報告します——複数のプランから選ぶオプティマイザは存在しません。

Scalar functions

移行してきたクエリの SQL 式はスカラー関数の語彙に依存します。kevy はそれをエンジンではなく SQL 面でカバーします:kevy-cli sql eval(および sql ツールボックスの定数畳み込み)が PostgreSQL 準拠のセマンティクスでクライアント側で式を評価し、サービングエンジンは式に一切触れません——このページの他の場所と同じ分業です。

現在のカバレッジ(関数名は大文字小文字を区別せず、PG と同じ折りたたみ規則):

ファミリ関数
文字列lower upper initcap length char_length concat concat_ws trim btrim ltrim rtrim replace split_part repeat lpad rpad strpos position left right reverse translate substr substring format
数学floor ceil ceiling round trunc mod power pow sqrt sign abs
演算子AND OR NOT(三値論理)、比較 = <> != < <= > >=、`\\ 連結、searched CASE WHEN … THEN … [ELSE …] END::bool`(PG の入力語彙)
NULL ファミリcoalesce nullif greatest least
日付時刻extract date_part date_trunc age to_charinterval は PG 本来の三成分形式:months、days、micros)
正規表現(POSIX ERE)regexp_replace regexp_matches regexp_split_to_array
ハッシュmd5
MySQL エイリアスdate_format unix_timestamp from_unixtime(MySQL のセマンティクスから逐一転写——WordPress/Laravel の表示トリオ)

この表を支える主張の規律:

  • セマンティクスは PostgreSQL 自身の回帰コーパスから逐一転写され、CI ゲート(funcgate)で守られています:カバー済み関数が PG と異なる答えを返せばハード失敗——ゲートの誤答数はゼロでなければなりません。コーパスのカバレッジ比率は上がる一方のラチェットです(現在、サブセット畳み込み可能なプローブの ≥ 82%)。
  • 表の外の関数は名前を挙げて拒否され、推測は決してしません。カバー済み関数の内部でも、基数を変えてしまう形は同様です——regexp_matches が零行または複数行を返すケースは、誤ったスカラーに潰されるのではなく拒否されます。

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 gSELECT 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つあり、望ましい順に挙げます。

  1. 書き込み時に非正規化する。 サービングモデルの答えです。ページがorder.totalの隣にuser.nameを出すなら、注文の行を作るときにuser_nameをそこへ書き込んでください。ストレージは安いですし、書き込みフックがそのコピー上のインデックスも最新に保ちます。親側の更新のファンアウト(表示用フィールドではまれです)は、CDCコンシューマの仕事です。
  2. アプリ側のhydration(2ホップ)。 外部キーは行の中に住み、インデックスが逆方向をワンホップにします。IDX.QUERY order_user EQ 42 FIELDS total statusSELECT total, status FROM orders WHERE user_id = 42です——インデックスがキーを見つけ、同じ呼び出しの中でフィールドをhydrateします(クックブックのレシピ2)。順方向は親ごとのHMGETです——idをまとめてパイプラインしてください。
  3. VIA hydrationを持つビュー。 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 + REFRESHMODE materialized —— 書き込みフックの中で増分維持され、古くなることが決してなく、リフレッシュのジョブも不要。任意でTOPK kにより有界化
ビュー内のORDER BYORDER 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への写像はこうなります。

SQLkevy差分
BEGIN … COMMIT(バッチ)MULTIEXECコマンドはキューされ、1単位として適用されます。内側でのインタラクティブな読み出しはできません —— 分岐の判断材料にする読み出しはMULTIの前に行います
SELECT … FOR UPDATEWATCH key + MULTI/EXECロックではなく楽観的CASです。watchしたキーが変わっていればEXECはnilを返します——読み直して再試行してください(クックブックのレシピ4)
ROLLBACKなしキュー時のエラーはバッチ全体を中止します(-EXECABORT、何も走りません)。EXECの内側の実行時エラーは、ほかのコマンドを取り消しません —— Redisのセマンティクスです
ストアドプロシージャ / 1つのアトミックなRMWEVAL(Lua)スクリプト全体がKEYS[1]のシャード上の1つのアトミックな単位です。本物の分岐を伴うread-decide-writeができます(lua
単一エンティティの直列化可能トランザクション組み込みのstore.atomic(key, …)シャードロックされたクロージャ。読み出しは自分の書き込みを見ます。コミットはアトミックで、fsyncは1回です(クックブックのレシピ5)
エンティティをまたぐ直列化可能トランザクション組み込みのatomic_all_shards決定論的なロック順序。ハンマーです——控えめに使ってください

分離性について、正直に。kevyにMVCCはなく、分離レベルのノブもありません。1つのシャードの内側では、すべてのコマンド(そしてすべてのEVALMULTIバッチ、組み込みのアトミックブロック)が直列化されます——1シャード / 1スクリプトに収まるものについては、*直列化可能*な振る舞いが得られます。シャードをまたぐとグローバルスナップショットはありません。マルチキーの読み出し(MGETIDX.QUERYのマージ)はシャード単位でアトミックであり、全体としてはSCANクラスです。ちぎれた単一コマンドを観測する読み出しはありませんし、コミットされていないMULTIバッチのdirty readもありません。ただし、別々の2つのコマンドをまたぐrepeatable readは存在しません —— それが重要な場面ではWATCH(CAS)かLua(1単位)を使ってください。

「コミット」の耐久性についてはpersistenceを参照してください。appendfsync alwaysなら、ackされた書き込みは応答の前にディスク上にあります。デフォルトのeverysecは最大1秒の窓を開けます(Redisと同じ取引です)。Store::fsync_aof()(組み込み)が、トランザクション単位のsynchronous_commitにあたる脱出口です。

制約とトリガー

SQLkevyの答え
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

SQLkevy
CREATE INDEX idx ON t (col)`IDX.CREATE idx ON PREFIX t: FIELD col TYPE i64\f64\str KIND range`
CREATE UNIQUE INDEXKIND unique(前述のフェンスのセマンティクス)
CREATE INDEX … USING gin (tsvector)KIND texttext-search
pgvectorのUSING hnsw`KIND ann DIM d [DISTANCE cosine\l2\ip] [M m] [EF ef]`(vector-search
DROP INDEXIDX.DROP idx
\d / information_schemaIDX.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/COMPOSEVIEW.QUERYSCAN)が[next-cursor, rows]を返します。カーソルを"0"になるまで戻し続けてください。カーソルは不透明で、1回の走査限りのものとして扱ってください。
  • バージョンカラムによる楽観ロックversionフィールドを持ち、行をWATCHし、読み、MULTI + HSET … version n+1 + EXEC。nilが返ればレースに負けたので、再試行してください(クックブックのレシピ4)。

バックアップとPITR

RDSkevy
mysqldump / pg_dumpkevy-cli export(論理的なRESPストリーム。redis-cli --pipeと互換)
バイナリバックアップスナップショットファイル(SAVE/BGSAVEdump-<id>.rdb
WAL / binlogAOF(シャードごとの、追記専用コマンドログ)
synchronous_commitappendfsync 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.READPREFIXフィルタ(マルチキーのフレームにはfail-open)
at-least-onceの配送同じです —— コンシューマは冪等でなければなりません
トランザクショナルoutboxパターン不要:コミットされた書き込みは、すでにそれ自体が変更フレームです(クックブックのレシピ11)

意図的に提供しないもの:シャードをまたぐグローバル順序(キー単位の順序は、シャードのストリーム内で保証されます)、サーバーサイドのコンシューマ位置、コンシューマグループ。保持期間はインメモリのバックログです。遅れすぎると、再構築の出発点となるカーソルとともに-FEEDRESYNCが返ります。

kevyが決してやらないこと

拒否を1か所にまとめます。どれも憲章上の決定であって(designing-on-kevy、プロジェクトのスコープログ)、ロードマップの穴ではありません。

拒否するもの理由代わりに使うもの
SQLパーサ / クエリDSLプランナへの坂道はここから始まる明示的なIDX.*/VIEW.* + このマトリクス
クエリプランナ / 自動インデックス選択エンジンは宣言されたパスを実行すべきであって、決めるべきではないIDX.EXPLAIN(診断専用)
JOINサーバーサイドのプラン探索は存在しない非正規化 / hydration(FIELDSVIA)/ ビュー
インデックスなしのWHERE偶発的なO(n)は、いつかあなたを叩き起こすための嘘であるパスを宣言するか、そのクエリを出さないか
サーバーサイドの制約DSL / トリガーアプリのロジックを蓄えることはエンジンのジャンルではないLuaのEVAL(アトミックな単位)+ CDCコンシューマ
DECIMAL型i64/f64を超えるサーバーサイドの算術は持たない整数の補助単位
JSONパスクエリハッシュフィールドこそがカラムモデルであるフィールドへ平坦化する(クックブックのレシピ9)
HAVING / 集約式クエリ言語への坂道GROUPSの出力をアプリ側でフィルタする
マルチデータベースのSELECT nサーバーごとにキー空間は1つプレフィックス、または別インスタンス
AUTH / TLS信頼境界はネットワークの外周であるループバックbind(デフォルト)、サイドカープロキシ
クロスDCのアクティブ・アクティブ、CRDTシングルDCの憲章アプリレベルのフェデレーション
動的メンバーシップ / 自動置換 / リシャーディングトポロジはオペレータが宣言するconfig + ローリング再起動
HTTP/REST APIRESPとMCPが2つのアクセスプレーンである任意のRedisクライアント

その2行目がどこで止まるか。 「決めない」は*クエリ時*の話です:あなたのクエリが自分で経路を名指すので、プランはあなたのために選ばれません。将来ひとつも足さない、という拒否ではありません。TABLE.DECLARE … AUTODECLARE nは、拒否され続けている形をエンジンが宣言済みの経路に変えることを許します——頼まない限りオフ、あなたが書いたnで上限が付き、宣言したカラムの上だけ、追加のみ、そしてIDX.LISTautoと印が付きます(tables.md)。予測できないオプティマイザと、あなたが頼んだスキーマ変更は別のものであり、ここで拒否されるのは前者だけです。

かつてこの表にあり、そこに属さない項目がひとつ:OFFSET 拒否として載っていましたが、エンジンはクローズつきの面(tables.md)で常に提供してきました。つまりこの表は、振る舞いではなく意図を書いていたのです。本当のところは、範囲内 30 000 行に対してLIMIT 20を返す実測(30 回のp50)です:

OFFSET1シャード8シャード
0219 µs526 µs
1 0001.53 ms6.90 ms
10 00011.5 ms20.1 ms

オフセットに対して線形で、シャードを増やすほど悪化します——どのシャードも自分のヒットのどれが全体マージを生き残るか知り得ないため、各シャードがlimit + offsetを取得しなければならず、原点はlimit件を返すために(limit + offset) × シャード数件を実体化します。このエンジンの他の軸はコアを増やすほど速くなりますが、この軸だけは速くなりません。

カーソルを使ってください。 OFFSETは存在し、拒否されておらず、そして深いOFFSETは、この表の残りが、あなたのレイテンシから締め出すために存在している当のO(N)スキップです。

サイジングと運用上の差分

すべてが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にあります。