kevy
本页内容

从 5.0 升级到 5.1

一句话版本:停掉 5.0 的服务器,用 5.1 的二进制在同一个数据目录上 启动。不用改配置、不改 wire、不改客户端。每次打 tag 前,发布门都会 把这套流程双向重演一遍,包括一对混版本的 primary/replica。

如果你用了压缩或 value log,请尽早升级。5.0 的编码器可能写出一个 连 5.0 自己的解码器都拒收的帧——compaction 之后,一个冷值会读回 corrupt。5.1 修了编码器,并教解码器把 5.0 已经写到盘上的那些帧救回来。 细节见下。

这个版本为什么存在

  • 修掉 5.0 的一个读取隐患。当字典携带共享 Huffman 表时, kevy-compress 可能把一个纯字面量帧标记成"用了字典匹配"。写入时 什么都察觉不到(CRC 校验的是已经写下的字节),故障要到后来、在 compaction 路径上,以"一个明明存成功了的值解码失败"的形式浮出来。 5.1 修正了 tag,并额外加了一条显式兼容分支,把 5.0 已经写下的错标 帧解出来。5.1 写的帧,5.0 依然读得了。
  • 巨型集合不再卡住它所在的 shard。list、hash、set、sorted-set 超过约 1.6 万元素后按元素粒度写时复制。rewrite 或快照钉住值期间的 写入只克隆一个段(约 1 毫秒,与集合总尺寸无关),而不是整个值—— 后者过去要付 0.35〜9.5 秒,并让该集合的常驻内存短暂翻倍。
  • appendfsync always 不再阻塞 reactor。两个 reactor 现在都做 组提交:io_uring 上回复由 fsync 完成来放行,epoll/kqueue 上由每 shard 一条 writer lane 做同样的事。ext4、50 并发连接实测:io_uring 353 → 8,273 writes/s,epoll 478 → 10,540。耐久性契约不变——回复 依然意味着"已经落盘"。
  • 修好了 failover 的收敛。切换到新晋升 primary 的 replica 可能 永远挂着不动:link up、心跳在走、也没有 resync 在进行,但 failover 之后的写入就是不到。见下面的复制一节。

如果你是嵌入式使用者

如果你的应用链接的是 kevy-embedded 而不是跑一个服务器,这一页的大半 讲的是你根本没有的机构。有位嵌入式使用者的全部调用面就是 Config::default().with_persist(dir) 加上 Store::openget / set / del / keys——对这种形状,升级只剩两个问题。

5.1 读不读得了 5.0 写下的?读得了。之后 5.0 还读不读得了 5.1 写过的? 读得了。这不是从服务器侧的路径推断出来的,是拿已发布的 crate 实测的: 5.0.0 的嵌入式 store 写了 201 个键(其中一个 200 KB,所以大值路径也在 里面)并重写了 AOF;5.1.0 打开那个目录,201 个全部读回;5.1.0 又追加了 自己的 201 个;5.0.0 重新打开,402 个全部读回。两个方向都是 clean replay, 而且同一个程序对两个版本都无改动编译通过。

这一页其余部分是服务器机构。进程内没有 reactor、没有 io_uring、没有复制、 没有监听器,所以下面这些对你没有意义:KEVY_AOF_OFFLOADappendfsync always 的组提交、快照 resync 及其 -LOADING 窗口、 feed generation、--accept-shards,以及上文引用的全部尾延迟数字。

真正会碰到你的:压缩那条修复——前提是你开了压缩或 value log (Config::default() 不开),以及超过约 1.6 万元素的集合的元素粒度 写时复制,它缩短的是 rewrite_aof() 可能施加给一次写入的停顿。

原样继承的东西

  • 数据文件。5.1 直接打开 5.0 的目录,5.0 也能重新打开 5.1 最后 写过的目录(两个方向都有门禁把关)。无论往哪个方向切,先做一次 干净停机。
  • wire 协议、命令、客户端契约。没有删除、没有改类型。现有客户端 不需要改动,也不需要重新构建。
  • 配置文件与命令行开关。5.0 的每个键都被接受,默认值不变。

行为有变化的地方

重连的 replica 会做一次快照 resync

复制游标拿不出连续性凭据的 replica——retarget、REPLICAOF,或者任何 让 feed generation 移动的版本变更之后的状态——现在会收到一份完整快照, 而不是从当前 offset 空间起点的重放。因为对一个 primary 看不见其内容的 replica,这是唯一能让它收敛的答复:帧流能新增和覆盖键,却永远删不掉 replica 有而 primary 没有的键。

升级后第一次连接时,你会在每个 shard 上看到一次:

kevy: replica fd 42 generation 6843605247850762 != feed generation
      4198793490690396 (sent_offset 0); shipping snapshot

这是预期行为,且能自愈。它同时意味着 replica 本地的分叉会被丢弃而不是 带着走——这正是分叉历史的既定契约。

这次 resync 落地期间,replica 对读请求回 -LOADING——任何全量 resync 本来就是这样:它的键空间正在被替换,给不出一个自洽的答案。PING 依然 豁免。窗口长短取决于数据集大小;如果你不能容忍这段读不可用,请在重启 该 replica 之前先把它从读池里摘掉。

它关掉的 bug:选举一落定,晋升就立刻开放写入,但每个 shard 要等自己的 tick 才给复制 offset 落栅栏。在那个窗口里被接受的写入会被栅栏销毁, 而之后接上来的 replica 被告知"你已经追平了"——于是这条写入存在于 primary 的键空间里,却不存在于任何一条流里,而链路一直报告自己完全 健康。

appendfsync always 共享 fsync 轮次

always 下,并发连接现在共享 fsync 轮次(组提交),而不是各付各的。 单个串行客户端会略慢一点——它的写入现在要等一次排队的 fsync 及其 完成,而不是内联的那次——任何并发负载则快一个数量级。如果你需要旧的 形状,KEVY_AOF_OFFLOAD=0 在两种 reactor 上都能恢复经典同步路径。

feed generation 变成随机身份

feed generation 现在是随机的 53 位历史身份,不再是计数器,所以两个 节点再也不可能用同一个名字称呼两段不同的历史。任何显示或存储过 generation 的地方(REPL.TOKENREPL.WAITFEED.TAIL)都会显示 又大又无序的数字。它们是身份:只做相等比较,永远不要比大小。

推荐流程

  1. 先备份:干净停机(或 BGSAVE + 拷贝),然后复制数据目录。
  2. 先升级 replica,再升级 primary。混版本的一对在两个方向都能工作; 每个 replica 在它的游标 generation 对不上的那一刻 resync 一次。
  3. 验证:每个 replica 的 INFO replication(link up、offset 在前进)、 INFO persistenceDBSIZE 对照预期,以及你自己应用的冒烟测试。
  4. 需要回滚就:干净停掉 5.1,在同一目录上启动 5.0。5.1 写下的值照样 读得出来。