kevy4.0

升级 kevy

两章,新的在前:3.x → 4.0(一次 API 定义型的 major:线协议和磁盘格式照旧,Rust 接口面改了这一次,此后冻结)和 2.x → 3.x(一次能力型的 major:一切照旧带过去)。每一章都明确写清:什么自动升级、什么需要改代码、以及怎么退回去。


3.x → 4.0

4.0 是工业生产级的宣告。这个 semver major 是刻意花掉的——用一次破坏窗口,把所有积欠已久的公开 API 债一次还清,此后接口面冻结:后续的 4.x 只做加法。如果你是通过线协议跟 kevy 说话,4.0 就是换个二进制。如果你链接了某个 kevy crate,预期是一次短促而机械的迁移——下面每一条变更都有一句话的规则。

TL;DR——各组件版本一览

组件3.x 时代4.0动作
kevy(服务器)3.18.x4.0.0换二进制,在同一个数据目录上重启
kevy-embedded3.18.x4.0.0升版本 + 照下面的 API 表改
kevy-client1.14.x4.0.0升版本——版本线统一(kevy-embedded 在 3.0.0 做过同样的动作)+ 那张 API 表
kevy-client-async1.1.x4.0.0同上
kevy-wasm / @goliajp/kevy(npm)——4.0.04.0 新增——见 wasm.md
基础设施 crate(kevy-storekevy-rt……)3.18.x4.0.0跟随工作区版本

什么是自动兼容的

线协议。 RESP 没变,每一个 verb 别名(SLAVEOFHMSET……)都保留。Redis 客户端、脚本、redis-cli 会话照旧可用;对着 valkey 9.1 的回复对等性套件仍然在 CI 里把关。

快照与 AOF。 4.0 的二进制能读每一种 3.x(以及 2.x)的快照格式,并原样重放 3.x 的 AOF。格式在 4.0 里没有变——所以退回 3.18 同样只是换个二进制(这一点和 3.x → 2.x 方向不同,那个方向有一处格式边缘;见 2.x → 3.x 那一章)。

配置。 每一个 3.x 配置键都被接受,含义不变。有两个键的语义变得更严或更真——见下面的“行为变更”(notify_keyspace_events 拒绝未知 flag、min_replicas_max_lag_ms 真正生效)。

移除了一个旋钮。 自定义快照 / AOF 文件名没有了(kevy-embeddedConfig::with_snapshot_filename / with_aof_filename 两个 builder)。磁盘布局现在固定为每 shard 一组 dump-{i}.rdb / aof-{i}.aof。用默认名字写出来的目录——包括每一个遗留的单文件目录——照样原样加载;只有用自定义名字写过的目录,需要在第一次用 4.0 打开之前,做一次性的 mv 改成固定名字。

API 破坏对照表

本节里的一切都是编译期破坏,且都有机械的修法。除非明确点出,否则不改变任何运行期语义。

1. flush() 垫片移除

废弃的别名没有了;活下来的那个名字如实说出它干的事(它会擦掉整个 store):

Crate移除的改调
kevy-embeddedStore::flush()Store::flushall()
kevy-clientConnection::flush()Connection::flushall()
kevy-storeStore::flush()Store::flushall()

2. 单一错误货币:KevyError

kevy-embeddedkevy-client 的每一个公开可失败接口面,现在都返回 KevyResult<T>(即 Result<T, KevyError>),而不是 io::Result<T>。这个类型住在 kevy-store 里,两个 crate 都做了 re-export:

pub enum KevyError {
    Store(StoreError),      // structured engine errors, no longer
                            // flattened into io::Error strings
    Io(std::io::Error),     // real I/O, preserved via From
    Protocol(String),       // server error replies, wire text intact
    ReadOnly,               // replica write rejection
    InvalidInput(String),   // e.g. URL parse errors
    NotFound(String),
    Unsupported(String),    // e.g. remote-only calls on embedded
    TimedOut,               // e.g. Subscription::recv_timeout
    Closed,                 // stream/bus gone; also terminates
                            // subscriber iterators
}

迁移通常只是改一下返回类型标注——? 照常工作,因为 From<io::Error>From<StoreError> 都在:

// 3.x
fn warm(conn: &mut Connection) -> std::io::Result<()> {
    conn.set(b"greeting", b"hello")?;
    Ok(())
}

// 4.0
use kevy_client::{Connection, KevyResult};

fn warm(conn: &mut Connection) -> KevyResult<()> {
    conn.set(b"greeting", b"hello")?;
    Ok(())
}

原本要检查错误的代码,处境严格变好了——匹配 variant,不必再解析字符串:

3.x 的信号4.0
io::Error::other("kevy-store: …") 的包裹文本KevyError::Store(e)——结构化的 StoreError
副本拒绝写入:io::Error::other("READONLY …")KevyError::ReadOnly(它的 Display 仍然以 READONLY 开头)
服务器 -ERR … 回复表现为不透明的 io::ErrorKevyError::Protocol(text)——线上文本原样保留
Subscription::recv_timeout 抛出的 ErrorKind::TimedOutKevyError::TimedOut
ErrorKind::UnexpectedEof 表示订阅流已断KevyError::ClosedSubscriberEvents / SubscriberMessages 迭代器产出 KevyResult<_> 并在此结束
远端专有特性用的 ErrorKind::UnsupportedKevyError::Unsupported(msg)

刻意没有 From<KevyError> for io::Error:那条回边会把这次改动所消除的有损降级重新装回来。真的需要 io::Error 的边界上,请显式转换,并自己承担那份信息损失。

有一个例外留在明处:CDC feed 的 embedded 接口面(changes_tail / changes_since)保留它自己的 FeedError——ResyncFuture 是这条流独有的控制信号,不是通用错误。

kevy-resp-client 有意保留 io::Result 接口面——它是一块纯传输的石头,io::Error 就是它诚实的货币。)

3. 构造函数命名:资源用 open,网络用 connect

每一类东西只有一个动词,处处如此。本地的、有文件支撑的东西用 open;有对端的东西用 connect;纯内存的值用 new。改名清单:

Crate3.x4.0
kevy-clientConnection::open(url)Connection::connect(url)
kevy-clientSubscriber::open(url, channels)Subscriber::connect_channels(url, channels)
kevy-client-asyncAsyncConnection::open(url)AsyncConnection::connect(url)
kevy-client-asyncAsyncSubscriber::open(url, channels)AsyncSubscriber::connect_channels(url, channels)
kevy-resp-clientRespClient::from_url(url)RespClient::connect_url(url)

本来就合规、原样不动的:kevy_embedded::Store::openkevy_persist::Aof::openkevy_store::Store::newClusterClient::connectRwClient::connectSubscriber::connect(url)RespClient::connect(host, port)

4. kevy_rt::Runtime 改为构建,不再靠位置传参

位置参数的构造函数没有了;Runtime 自己就是 builder:

// 3.x
let rt = Runtime::new([127, 0, 0, 1], 6004, 4, commands);

// 4.0
let rt = Runtime::builder(commands)
    .bind([127, 0, 0, 1], 6004)
    .shards(4);

builder(commands) 的默认值:bind 127.0.0.1:6004、1 个 shard、AOF 开启(EverySec)、数据目录 "."bind / shards#[must_use] 的 setter,和既有的 with_* 链一致。这同时也是 4.0 实例化工作的可见面:Runtime 不再触碰任何全局状态,所以一个进程里可以跑好几个互相独立的 kevy 实例。

5. kevy-store 的写方法收借用 argv

持有所有权的那套形态被移除;借用形态(原来的 _borrowed 双胞胎)接管了正式名字:

3.x(持有所有权)4.0(借用,同名)
del(&[Vec<u8>]) / exists(&[Vec<u8>])del(&[&[u8]]) / exists(&[&[u8]])
hset(&[(Vec<u8>, Vec<u8>)]) / hdel(&[Vec<u8>]) / hmget(&[Vec<u8>])hset(&[(&[u8], &[u8])]) / hdel(&[&[u8]]) / hmget(&[&[u8]])
sadd / srem / lpush / rpush / zrem (&[Vec<u8>])同名,(&[&[u8]])
zadd(&[(f64, Vec<u8>)])zadd(&[(f64, &[u8])])
zadd_flags_borrowed(…)改名为 zadd_flags(…)
// 3.x
store.del(&[b"k1".to_vec(), b"k2".to_vec()]);

// 4.0 — pass slices; no allocation
store.del(&[b"k1".as_slice(), b"k2".as_slice()]);

这是一次穿着 API 变更外衣的性能修复:kevy-embedded 的门面现在把借用的 argv 直接递下去,embedded 写路径上每次调用一份的 to_vec() 拷贝没有了。

6. Commands trait + Route(自带命令集的嵌入方)

只有你自己 impl kevy_rt::Commands 时才相关:

行为变更(不用改代码,但运维可见)

Feature 系统(4.0 新增)

kevy-embedded 现在按 feature 分档,好让小目标只为自己用到的东西付钱。默认仍然是全都开:

Feature加进来什么拉进哪些 crate
coreKV + TTL + pubsub + 原子块 / pipeline(无)
persist快照 + AOFkevy-persist
index声明式索引 + 视图kevy-index
text全文(BM25)段indexkevy-text
vectorHNSW ANN 段indexkevy-vector
replicate复制 + CDC feedpersistkevy-replicate
listener只读 RESP listener(无)

core 档可以交叉编译到 musl 目标,并扛着一份强制预算(二进制 ≤ 700 KB,空 store RSS ≤ 2 MB);另有五个基础 crate 能构建 no_std。见 iot.md。在体积谱的另一端,同一个 embedded 内核现在能以 @goliajp/kevy 的身份跑在浏览器里——见 wasm.md

从 4.0 退回 3.18

换回二进制即可;快照和 AOF 格式是共享的。唯一的边缘:配置文件里用了新的 notify flag(x / e / n)时,3.18 能解析它,但那些事件在那边永远不会触发。


2.x → 3.x

kevy 3.x 是 2.x 的超集:每一个 2.x 负载原样可跑,服务器侧的升级是换个二进制,embedded 用户则是升一下依赖。这一章明确写清:什么自动带过去、什么改了名字或数字,以及唯一需要小心的那个方向(退回 2.x)。

TL;DR——各组件版本一览

组件2.x 时代3.x(终版 3.18.x)动作
kevy(服务器)2.0.x3.18.x换二进制,在同一个数据目录上重启
kevy-embedded1.x(1.4–1.16)3.x升依赖——1.x 这条线在 v3.0.0 整个工作区统一到同一个版本时终结
kevy-client1.12.x1.13–1.14升版本;API 不变
kevy-client-async1.0.x1.1.x升版本;API 不变
kevy-cli未发布3.xcargo install kevy-cli——现在带着整套迁移工具链
基础设施 crate(kevy-storekevy-rt……)2.0.x3.x跟随工作区版本

kevy-embedded 从 1.x 跳到 3.x 是一次版本线统一,不是 API 重写:1.16 的接口面完整包含在 3.x 里。如果你的 Cargo.toml 写的是 kevy-embedded = "1",改成 "3" 重新编译即可——或者直接照着上面那一章去 "4"

什么是自动兼容的

线协议。 RESP 没变。3.x 在 CI 里仍然对着 valkey 9.1 做逐字节的回复核对(98 个命令)。现有的 Redis 客户端、脚本、redis-cli 会话照旧可用。

快照。 3.x 的加载器能读每一种 2.x 快照格式(KEVYSNAP 版本 2–5):相对 TTL 的 v2 文件、绝对 TTL 的 v3、带 stream group 的 v4、带 feed 游标的 v5。把一台 3.x 服务器指向一个 2.x 数据目录,它加载得起来。

AOF。 AOF 是一份 verb 日志,而 3.x 的 verb 集是 2.x 的超集——重放原样可用。appendfsync 语义不变。

配置。 每一个 2.x 配置键都被接受。新加的段([replication] single_source--accept-shards……)都是加法,默认值复现 2.x 的行为。

升级步骤

服务器部署

  1. 在还在跑的 2.x 上拍一份快照(SAVE 或者你平常的备份方式),并留一份副本——为什么要留,见下面的“退回”。
  2. 停掉 2.x,用同样的参数和数据目录启动 3.x 二进制。
  3. 校验:DBSIZE 对得上;想要密码学级别的把握,就在升级前后各跑一次 kevy-cli digest -p <port> <prefix>——digest 相等即代表键空间完全一致。

滚动升级一对副本:先升副本,让它重新同步,然后把流量切过去,再升原来的主节点。(2.x 没有托管切主,所以从 2.x 出发,这就是常规的手工切换。一旦到了 3.15+,切主这一步本身就变成一个 verb——FAILOVER host port,见 availability.md。)

Embedded 应用

  1. Cargo.toml 里写 kevy-embedded = "3"
  2. 重新编译。1.16 的 API 原样都在;新的能力面(索引 / 视图 / 全文 / 向量 / feed / 复制)是加法形式的方法和 Config 选项。
  3. 有一条 trait 注意事项:当且仅当你自己写了 impl kevy_rt::Commands 并且用字面量构造 ResolvedCmd 时,v2 那一段加了两个字段(block_hintwake_idx)。默认的 resolve() 会填好它们;字面量构造则要把这两个字段补上。
  4. 一个 1.x embedded 应用留在磁盘上的数据原样可加载(快照格式与服务器相同)。

客户端

kevy-client 1.13+ / kevy-client-async 1.1 是直接替换:这次 minor 升版只是把内部 crate 重新钉到 3.x 工作区。通用 Redis 客户端库不受任何影响。

3.x 加了什么(你为什么要升)

带补水的声明式索引(IDX.*)、具名视图(VIEW.*)、写入时聚合(GROUP BY / 分布式 top-K)、无词典的 CJK 全文检索加 BM25、HNSW 向量 KNN(外加 BM25 + KNN 的混合融合)、带恢复点契约的 CDC feed(FEED.*)、embedded 作为主节点的复制、机器可读的契约(COMMAND DOCS、生成式参考文档、kevy-mcp 这个 MCP 服务器)、可用性这条弧(复制滞后真值、FAILOVER、多数派崩溃选举,以及 WAIT / REPL.TOKEN / REPL.WAIT 一致性阶梯——见 availability.md),还有迁移工具链(kevy-cli import/export/--verify/diff/inspect/digest)。从 designing-on-kevy.mdcookbook.md 开始读;性能凭据在 bench/PERF-LEDGER.md

这些没有一个是隐式生效的:一台跑着 2.x 负载的 3.x 服务器,目录是空的,而空目录上的索引钩子被 perfgate 棘轮盯着(相对 2.x 无回归)。

退回(唯一需要小心的方向)

3.x 服务器写出的是快照格式 v4;一旦存在 CDC feed 游标,写出的就是 v5。而 2.x 二进制最多只读到 v4:

3.x 里引入的 verb(IDX.*VIEW.*FEED.*……)在 2.x 二进制上自然重放不了——如果你用过它们,正确的退回路径是 export / import,不是 AOF 重放。

版本历史,每个一句话