kevy4.0

組み込みの読み取り専用RESPリスナー

組み込み(プロセス内)のkevyストアは、読み取り専用のリスナーを通じて、外部のRESPクライアント——redis-cli、運用ツール、ダッシュボード——に自分を公開できます。ストアはあなたのプロセスの中のライブラリのままです。リスナーはそこを覗く窓であって、2台目のサーバーではありません。書き込みは、所有プロセスの排他的な権利であり続けます。

use kevy_embedded::{Config, Store};

fn main() -> kevy_embedded::KevyResult<()> {
    let store = Store::open(
        Config::default()
            .with_shards(4)
            .with_resp_listener("127.0.0.1:6009".parse().unwrap()),
    )?;
    store.hset(b"row:42", &[
        (b"state".as_slice(), b"live".as_slice()),
    ])?;
    // ... the application keeps running; clients can peek:
    Ok(())
}
$ redis-cli -p 6009 hgetall row:42
1) "state"
2) "live"
$ redis-cli -p 6009 scan 0 match 'row:*' count 100
$ kevy-cli -p 6009 DBSIZE

RESPクライアントならどれでも動きます。リスナーはkevyサーバーと同じプロトコルを話し、フレーム化されたリクエストもインラインコマンド(redis-cliのPING形式)も受け付けます。

有効にする

ホワイトリストのみです。それ以外はすべて-ERR READONLY embedded listenerを返します。

PING ECHO GET MGET EXISTS TYPE TTL PTTL DBSIZE KEYS SCAN
HGET HMGET HGETALL HLEN LRANGE LLEN SMEMBERS SCARD SISMEMBER
ZSCORE ZCARD ZRANGE FEED.READ FEED.TAIL FEED.SHARDS INFO

拒否される側には、すべての書き込みverb、MULTI、ブロッキングpop、pub/sub、そして拡張プレーン(IDX.* / VIEW.*——これらはプロセス内から型付きAPIでクエリしてください)が含まれます。FEED.*の3つはreplicate cargoフィーチャを必要とします(デフォルトで有効)。

INFOは、組み込み向けの小さなレポートを返します。crateのバージョン、シャード数、キー数、そしてlistener:readonly——ダッシュボードが「自分は何と話しているのか」を識別するには十分な内容です。

この拒否テキストは契約です。ツールは書き込みを試み、READONLY embedded listenerにマッチするかどうかで「これは本物のサーバーか、組み込みの窓か」を判定できます。

verbのセマンティクス

ホワイトリストのverbは、サーバー側の同名verbと同じように振る舞います。

整合性

読み出しはストア自身のシャードロックのもとで走ります。どの応答も、コミットされた、ある時点の答えです。書き込みプロセスと同時刻であり(レプリケーションなし、ラグなし、スナップショットの古さなし)。マルチキーの読み出し(MGETSCANKEYSDBSIZE)は、グローバルスナップショットなしにシャードごとにマージされます——サーバーと同じ、SCANクラスの包絡線です。

コネクションごとに1スレッドです。これは運用ツール向けの面であって、サービングパスではありません(kevy*サーバー*こそがサービングパスです)。コネクション数はツールの規模にとどめてください。数秒おきにポーリングするダッシュボード、というのが想定している形です。

フィードとread-your-writes

FEED.TAILは現在の(generation, offset)を返します。FEED.READ <gen> <offset> <limit> [PREFIX p…]は変更フレームを配送します——組み込みのchanges_since APIと同じat-least-onceの契約です(generationが古ければResyncが返るので、FEED.TAILからやり直してください)。組み込みの書き込みパスは全シャードを1本のストリームに直列化するため、FEED.SHARDSは1を返し、フィードのverbはシャード引数を取りません——サーバー面に対して書いたコンシューマループ(cdc.md)が、そのままここでも動きます。

プロセスをまたぐフィード越しのread-your-writesは、ブロッキングのプリミティブではなく、カーソルのパターンです。書き込むプロセスが、自分の書き込みのあとにchanges_tail()を控えておきます。読むプロセスは、まずFEED.READをそのカーソルの先までdrainし、それから読みます。プロセス内の読み出しは常にread-your-writesです(書き込みは同期的にコミットするからです)。(サーバーからレプリカへのレプリケーションには、ブロッキングのプリミティブが*実際にあります*——REPL.TOKEN / REPL.WAITavailability.mdを参照——が、それはレプリケーションのプレーンであって、このフィードリスナーではありません。)

ソケットなしで覗く

リスナーのverbテーブルは、public methodでもあります。

let mut out = Vec::new();
store.dispatch_readonly(
    &[b"HGETALL".to_vec(), b"row:42".to_vec()], &mut out);
// `out` holds raw RESP bytes — the exact reply the listener
// would have written to a socket.

Store::dispatch_readonly(argv, out)は、同じホワイトリストに対して1件のリクエストに答えます(書き込みverbには同じ-ERRが返ります)——リスナーのプログラム的な顔であり、所有プロセスに埋め込まれたツール向けです。

そして、自分が所有していないストアに対しては、kevy-cli --embed <dir>があります。組み込みストアのデータディレクトリの、読み取り専用・ある時点のビューを開きます。dump/aof/shards.metaの各ファイルはスクラッチディレクトリへコピーされてからリプレイされるので、所有プロセスは触れられることなく走り続けます。REPLも単発実行も動きます。書き込みverbにはリスナーと同じ-ERR READONLYが返ります。これは、このリスナーの生きた窓に対する、オフラインの補完物です。

窓より多くを求めるとき

機構の重さの順に、エスカレーションの道筋を示します。

  1. このリスナー — 生きた読み出し、書き込みコストゼロ、1プロセス。
  2. CDCフィードcdc.md) — 相手プロセスに読み出しをポーリングさせるのではなく、変更をそちらへpushします。
  3. 組み込みをプライマリにするレプリケーションwith_embed_writerがレプリケーションソースを公開し、[replication] single_source = trueのkevyサーバーが、その組み込みストアをレプリカとして追随します。あなたのプロセスが供給し、サーバーのハードウェア上で読み出しをフルにファンアウトする形です。replication.mdの*組み込みをプライマリにする*節を参照してください。

パフォーマンス

bench/topogate.shがclampであり、これは本物の2プロセステストです。連続的なHSET負荷の下にあるライターのバイナリと、生きたデータを表明する別プロセスのリーダーからなります。

読み出しがシャードロックのもとで走るという設計は、リスナーのトラフィックが重ければ、同じシャード上で所有者の書き込みと競合し得ることを意味します。それが、ラグゼロの真実と引き換えに払う代償です。ツール規模の読み出しレート(ダッシュボード、スポットチェック)は、所有者の書き込みスループットの中では計測不能なほど小さくなります。サービング規模の読み出しが必要なら、このリスナーではなく、レプリケーション(上記)を使ってください。

関連項目