Valkey 升到 9.1 就回不去了:RDB 头从 REDIS0011 变成 VALKEY080,旧二进制只回一句 Can't handle RDB format version 80
Valkey 的二进制升级本身不复杂:换包、重启、等复制追上。难的是升级之后还能不能回去。7.2 和 8.x 写 RDB 11,9.0 和 9.1 写 RDB 80,9.2 的 RC 写 RDB 81;这三套格式之间并不构成一条可以随意往返的路径。再加上客户端在 HELLO、拓扑字段和错误文本上的改动,一次“看起来只是小版本”的升级,实际会同时关掉两条退路。
先看前 9 个字节
每个 RDB 文件开头都是 9 字节:magic string 加格式版本。
head -c 9 /var/lib/valkey/dump.rdb; echo
# REDIS0011 Valkey 7.2 ~ 8.x(以及 Redis OSS 7.2 及更早)写出
# VALKEY080 Valkey 9.0 / 9.1 写出
# VALKEY081 Valkey 9.2 release candidate 写出
9.1.3 的 src/rdb.h 写得很直白:RDB 11 是最后一个开源 Redis RDB 版本,Valkey 7.x / 8.x 用它;9.0 跳到 80 是为了避开非开源 Redis 的版本号,并且版本号大于 79 的文件用 VALKEY 而不是 REDIS 当魔数。9.2 RC 把 RDB_VERSION 设成 81,新增一种 hash 类型(type 23)和一个 forkless full sync 用的 opcode;如果 rdbcompression 设成 lz4 或 zstd,文件头会变成 VCS 开头的压缩流(默认 yes 仍是逐字符串 LZF)。
12 到 79 这段是被刻意留空的。Redis CE 7.4 到 8.4 写 12,8.6 写 13,8.8 写 14,8.10 写 15,其中 type 22 在 Valkey 9.0 里是带字段 TTL 的 hash,在 Redis 7.4 RC 里是另一种布局;opcode 243 在 Valkey 是槽导入记录,在 Redis 是 key metadata。Valkey 8.1 通过 PR #1604 把 12–79 标成 foreign 并直接拒绝;9.0 的 PR #2543 允许 relaxed 模式接受 foreign 版本号,但只要文件头是 REDIS 且出现 22–243 区间的类型或 opcode,仍然拒绝。
旧二进制到底报什么错
7.2 和 8.0 的加载器只认 REDIS 魔数,读到 VALKEY080 时根本走不到版本判断,日志里是:
# Wrong signature trying to load DB from file
8.1 能读到版本号,默认 strict 下报:
# Can't handle RDB format version 80
# Fatal error loading the DB, check server logs. Exiting.
rdb-version-check 是 8.1.0 才引入的选项,取值 strict / relaxed,默认 strict,可以运行时改:
valkey-cli CONFIG GET rdb-version-check
valkey-cli CONFIG SET rdb-version-check relaxed
对即将做 full sync 的从库、或者对 RESTORE,CONFIG SET 就够了;但如果文件是在启动时加载的,那时还连不上实例,必须写进配置文件:
# valkey.conf,准备用于回滚的 8.1 节点
rdb-version-check relaxed
注意 relaxed 只是 best effort:valkey.conf 的原话是加载遇到未知信息就中止,而且这个开关对复制和 RESTORE 同样生效。8.1 在 relaxed 下能接受更高版本号的 VALKEY 头,但会停在第一个不认识的类型上,比如带字段 TTL 的 hash,报 Unknown RDB encoding type;9.0/9.1 则可能报 Unknown type or opcode when loading DB. Unrecoverable error, aborting now.。7.2 和 8.0 没有任何设置能改变这一点。
复制路径:9.0 恒发 80,9.1 会降级发 11
这是最容易误判的地方。9.0.7 的复制代码里没有“按从库版本挑格式”的逻辑,9.0 主库永远发 RDB 80 加 VALKEY 头,7.2 和 8.0 从库连不上,8.1 从库只有在 relaxed 下才能试。
9.1 起 PR #2735 让主库按从库声明的版本发 RDB 11,所以 7.2 / 8.x 从库能挂在一个 9.1 主库下面做 full sync,而主库自己的 dump.rdb 仍然是 80。例外写在发布说明里:一旦用到从库处理不了的新特性,比如 hash 字段 TTL 和原子槽迁移,full sync 会直接 abort,主库日志是 Can't store key ... in RDB version 11。
更要紧的是 failover 之后的 partial resync:它不发 RDB,只发命令流。8.x 从库执行不了 HSETEX / HEXPIRE,在默认的 propagation-error-behavior ignore 下只记一条错误、跳过命令,然后安静地分叉。所以拿旧版本节点当回滚目标时,把它设成:
propagation-error-behavior panic-on-replicas
否则这个“回滚目标”在你真正 promote 它之前,看起来一直是健康的。
升级前先确认两件事
INFO 里的 redis_version 恒为 7.2.4(9.1.3 的 version.h 把它定义为 Redis OSS 兼容版本,不会超过 7.2.x),真实版本只有 valkey_version 给:
for h in cache-01 cache-02 cache-03; do
echo "$h $(valkey-cli -h "$h" INFO server | grep '^valkey_version:' | tr -d '\r')"
done
然后确认有没有数据或流量必须依赖 9.x 从库:
valkey-cli INFO keyspace
# db0:keys=18240,expires=3115,avg_ttl=412000,keys_with_volatile_items=0
valkey-cli INFO commandstats | grep -E 'cmdstat_(hsetex|hgetex|hexpire|hpexpire|hexpireat|hpexpireat|hgetdel|msetex|delifeq):'
keys_with_volatile_items > 0 意味着库里已经存在带字段 TTL 的 hash,向 8.x 从库做 RDB 11 full sync 会 abort。任何一条 cmdstat_ 命中,说明计数器重置之后跑过 9.0/9.1 新增的命令。
补丁版本自己也会制造跨版本故障
- PR #4819:listpack 的长度字段。当条目编码长度正好是 16383、2097151、268435455 字节时,尾部长度字段有两种合法宽度,9.0 及之前写宽的,9.1.0–9.1.2 写窄的,两边互相读错。这在实际数据里够得着:一个 16378 字节的字符串元素,条目长度正好是 16383。症状是 9.0 与 9.1 之间的 RDB 加载和复制失败。9.1.3 修复。
- PR #4105:双通道复制下的从库版本协商,影响 9.0.5 之前和 9.1.1 之前的 9.x 从库,主库发 RDB 11 时带字段 TTL 的 hash full sync 失败。9.0.5 / 9.1.1 修复。
- PR #4739:
valkey-check-aof对VALKEY魔数的误判,一个合法的多段 AOF(RDB 80 做 base)被报成 invalid。9.0.7 / 9.1.3 修复。
所以规则很简单:从 8.x 或 9.0 直接去 9.1.3 或更高;已经在 9.1.0–9.1.2 的节点,往前走到 9.1.3,不要往回退到 9.0。
DUMP / RESTORE 这条退路也堵着
降级没有官方流程,实际只有三条路:旧二进制直接读新文件(只有 8.1 relaxed 能试)、复制进一个旧版本节点(9.1+ 主库在被 9.x 独有数据污染前可行)、以及 DUMP / RESTORE / MIGRATE。第三条在 8.x strict 下当场失败:
valkey-cli -h cache-new MIGRATE cache-old 6379 "" 0 5000 KEYS session:42
# (error) ERR Target instance replied with error: ERR DUMP payload version or checksum are wrong
payload 尾部的 RDB 版本是 80,8.x 不认。在 8.1 目标上开 relaxed 能让 RESTORE 接受旧类型的 version 80 payload,9.x 独有的编码照样失败。至于把升级前的备份恢复回去,那等于丢掉备份之后写入的所有数据。
另外,aof-use-rdb-preamble yes(默认)时,9.x 的 AOF base 就是 VALKEY080 开头,AOF 也救不了回滚。
可执行的升级 + 回滚骨架
记录每个节点的
valkey_version和在用的特性,确认没有节点跑 9.1.0–9.1.2。打一份 RDB 11 快照,并在一个旧版本备用节点上验证它能加载:
valkey-cli -h cache-01 BGSAVE head -c 9 /var/lib/valkey/dump.rdb; echo先升从库,一台一台来,每台确认
master_link_status:up:valkey-cli -h cache-02 INFO replication | grep '^master_link_status:'留一台从库停在旧版本当回滚目标。用 Sentinel 时把它的
replica-priority设得比新节点高(Sentinel 取最小值,0 表示永不提升)。带写入暂停 failover 到 9.1.3 从库:
valkey-cli -h cache-01 FAILOVER TO cache-02 6379 TIMEOUT 5000不使用任何 9.x 独有特性跑一段,确认 RDB 11 full sync 完成:
grep "sync: Finished with success" /var/log/valkey/valkey.log。需要回退时 fail back 到那台旧从库:
valkey-cli -h cache-02 FAILOVER TO cache-03 6379 TIMEOUT 5000确认不回退了再收尾:升级最后一台从库,然后才启用 hash 字段 TTL、新命令和槽迁移。
不可逆点不是换二进制那一刻,而是第一次写入 9.x 独有数据那一刻。集群场景下还有两条额外约束:官方不建议不同 minor version 的节点长期共处一个集群;原子槽迁移在使用中同样会让 RDB 11 full sync abort,混跑窗口要短,槽迁移要停。
客户端侧:拓扑字段、身份字符串和错误文本
go-redis ≤ 9.20.0 解析 Valkey 9.1 的 CLUSTER SHARDS 会失败,因为 9.1 总在 shard 级别加一个 id 字段:
redis: unexpected key "id"
9.20.1 修复。影响面比听起来小:只有直接调用 ClusterShards() 的代码会崩,集群路由走的是 CLUSTER SLOTS,不受影响。
身份和文本的改动更隐蔽。从 8.0 起 HELLO 返回 server: valkey 和真实的 Valkey 版本(比如 9.1.3),7.2.x 时代还是 server: redis;INFO server 把 redis_mode 改名为 server_mode,redis_version 仍是 7.2.4。五条 -LOADING / -BUSY / -MISCONF 错误文本也改成了 Valkey 措辞。任何按 server: 字段或按错误字符串前缀做分支的代码,都得跟着改。
extended-redis-compatibility yes(8.0 引入,默认关闭)能还原的是身份和那五条错误文本,还原不了 CLUSTER SHARDS 的新字段,而且 valkey.conf 明确说这个开关将来会移除。把它当成过渡期的胶水,不要当长期配置。
哪个坑最致命
不是 Wrong signature,也不是 go-redis 的解析异常——这两个都当场报错,藏不住。最致命的是 9.1 主库把 RDB 11 推给旧从库这件事造成的错觉:回滚目标看起来一直在线、复制正常,但只要有 hash 字段 TTL 或槽迁移介入,full sync 会 abort;而 failover 后的 partial resync 在 propagation-error-behavior ignore 下会静默跳命令、悄悄分叉。等你真的 promote 那台旧从库,才发现它缺了数据。所以旧版本从库必须配 panic-on-replicas,升级必须落在 9.1.3,混跑窗口必须短。
参考: