对于大量使用 String 键值的 Valkey 集群,内存优化会直接影响单节点容量、扩容频率和故障切换成本。相关测试显示,Valkey 9.1 在以 String 为主的环境中,相比 7.2 最多观察到约 37.5% 的内存下降。这一数字很有吸引力,但真正决定生产收益的,是键长度、值长度、对象编码、分配器碎片以及你选择的测量口径。
与其只看一个百分比,不如回答三个更具体的问题:每个键省了多少字节?哪些编码获得了收益?这些节省最终有没有反映到进程 RSS 和机器账单上?
37.5% 不是所有工作负载的固定折扣
String 密集型场景广泛存在于缓存、会话与鉴权、实时应用、电商、广告技术和游戏系统中。此类系统往往拥有数百万乃至数十亿个较小对象,因此即使每个对象只减少十几个字节,累积效果也可能非常可观。
不过,“内存下降 37.5%”必须连同测试边界一起理解:
- 它是特定测试环境中 Valkey 9.1 相对 7.2 的结果,不是所有数据模型的承诺值。
- 小键、小值通常更容易体现元数据优化,因为对象头、指针和分配器对齐在总占用中的比例更高。
- 大值主要由有效载荷本身决定,固定开销减少后,百分比可能没有那么显眼。
used_memory、used_memory_dataset、used_memory_rss和云主机实际占用不是同一个指标。- 复制缓冲区、客户端缓冲区、持久化、主动碎片整理和后台任务都可能改变结果。
因此,版本升级评估应同时报告总量、每键字节数和 RSS,而不是只摘取最漂亮的百分比。
编码决定了“同样是 String”到底占多少内存
Valkey 会根据对象内容和操作方式选择内部编码。可以使用以下命令检查单个对象:
valkey-cli SET session:1 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
valkey-cli OBJECT ENCODING session:1
valkey-cli MEMORY USAGE session:1
对 String 而言,整数形式、较短字符串和较长字符串可能采用不同编码。具体阈值和布局可能随版本变化,因此不应只凭业务字段类型推断编码,而应在目标版本上通过 OBJECT ENCODING 验证。
设计基准数据时,至少应覆盖以下几组:
| 数据组 | 示例 | 需要观察的内容 |
|---|---|---|
| 整数值 | SET counter:1 12345 |
是否使用整数编码、每键占用 |
| 短字符串 | 32~64 字节会话令牌 | 紧凑编码和固定元数据占比 |
| 长字符串 | 1 KiB 以上 JSON 或二进制数据 | 有效载荷占比、分配器对齐 |
| 不同键长 | 12、32、128 字节键名 | 键名本身及其分配开销 |
| 更新后的字符串 | 创建后执行 APPEND 或覆盖 |
编码变化和额外容量 |
Hash、List 和 Set 等复合结构还可能使用 listpack 等紧凑编码。它们值得单独测试,但不能拿复合结构的结果直接代表 String 密集型集群。
用同一份数据比较 Valkey 7.2 与 9.1
下面是一套可以直接改造的本地测试。它使用官方容器、关闭持久化,并向两个版本装载完全相同的 20 万个键。运行前需要安装 Docker 和 Python 3,并确认所需镜像标签可用;正式报告中还应固定镜像 digest 和 CPU 架构。
先创建数据生成器 load.py:
#!/usr/bin/env python3
import sys
count = int(sys.argv[1]) if len(sys.argv) > 1 else 200_000
value_size = int(sys.argv[2]) if len(sys.argv) > 2 else 64
out = sys.stdout.buffer
value = b"x" * value_size
for i in range(count):
key = f"user:{i:08d}".encode()
command = (
b"*3\r\n"
b"$3\r\nSET\r\n"
+ f"${len(key)}\r\n".encode() + key + b"\r\n"
+ f"${len(value)}\r\n".encode() + value + b"\r\n"
)
out.write(command)
再创建 bench.sh:
#!/usr/bin/env bash
set -euo pipefail
COUNT=200000
VALUE_SIZE=64
run_version() {
local tag="$1"
local output="valkey-${tag}.txt"
docker rm -f valkey-bench >/dev/null 2>&1 || true
docker run -d --name valkey-bench -p 6379:6379 \
"valkey/valkey:${tag}" \
valkey-server --save '' --appendonly no >/dev/null
until docker exec valkey-bench valkey-cli PING >/dev/null 2>&1; do
sleep 0.2
done
python3 load.py "$COUNT" "$VALUE_SIZE" \
| docker exec -i valkey-bench valkey-cli --pipe >/dev/null
sleep 2
{
echo "version=${tag}"
echo "count=${COUNT}"
echo "value_size=${VALUE_SIZE}"
docker exec valkey-bench valkey-cli INFO memory \
| tr -d '\r' \
| grep -E '^(used_memory:|used_memory_rss:|used_memory_dataset:|mem_fragmentation_ratio:)'
echo -n 'sample_encoding='
docker exec valkey-bench valkey-cli OBJECT ENCODING user:00000001
echo -n 'sample_memory_usage='
docker exec valkey-bench valkey-cli MEMORY USAGE user:00000001
} | tee "$output"
docker rm -f valkey-bench >/dev/null
}
run_version 7.2
run_version 9.1
执行:
chmod +x bench.sh
./bench.sh
输出会分别保存在 valkey-7.2.txt 和 valkey-9.1.txt。如果镜像仓库中的补丁版本标签不同,可以把 7.2、9.1 改成实际可用的完整标签。
建议重点计算两个指标:
每键数据集字节数 = used_memory_dataset / key_count
节省比例 = (7.2 的内存 - 9.1 的内存) / 7.2 的内存 × 100%
MEMORY USAGE 适合观察单个样本,used_memory_dataset 更适合比较已装载数据集,used_memory_rss 则更接近操作系统看到的进程占用。三者都应保留,因为对象级节省不一定立即转化为同等比例的 RSS 下降。
怎样避免得到一个“正确但没用”的结果
实验数据必须尽量贴近生产分布,而不是只使用一种固定长度。可以从脱敏后的统计信息中重建测试集,例如:
- 键长度的 P50、P95 和 P99;
- 值长度分桶及各桶占比;
- 整数字符串、令牌、JSON 和二进制值的比例;
- TTL 分布以及有无过期时间;
- 原地更新、覆盖写和删除的频率;
- 主从复制、持久化和碎片整理配置。
每个版本还应使用相同的配置、相同的容器内存限制、相同的 CPU 架构和相同的数据装载顺序。建议重复运行至少三次,并分别记录:
valkey-cli INFO memory
valkey-cli MEMORY STATS
valkey-cli DBSIZE
valkey-cli CONFIG GET maxmemory-policy
如果生产环境启用了 RDB、AOF、复制或主动碎片整理,应再做一轮开启对应功能的测试。关闭这些功能的实验适合隔离对象布局差异,却不足以预测完整生产进程的峰值内存。
升级前的决策清单
Valkey 9.1 对 String 密集型环境的优化可能带来显著收益,相关测试中相对 7.2 达到了约 37.5% 的下降。但是否能转化为节点缩容,需要完成以下检查:
- 用真实键长和值长分布重放数据,而不是只测理想化样本;
- 按编码拆分结果,并通过
OBJECT ENCODING验证假设; - 同时比较
used_memory_dataset、used_memory和used_memory_rss; - 测量稳态内存,也测量快照、重写、复制和故障恢复期间的峰值;
- 在容量规划中保留碎片、流量突增和故障切换余量;
- 先做兼容性与性能回归,再根据实测结果调整
maxmemory或实例规格。
真正有价值的结论通常不是“新版本节省了 37.5%”,而是“在我们的键和值分布下,每个对象减少了多少字节,整台机器最终释放了多少可用内存”。把测量细化到版本、编码和每键成本,升级收益才能进入可靠的容量模型。