缓存还是一千万个键,值却变大了

缓存对象变大,变化的不只是容量。通过一个假设场景,分析 Lavik 如何将压力转移到响应字节量、替换写入和存储空间回收。

缓存对象增加了字段,键和请求速率却没有变化。应用仍在执行同样数量的查询,容量曲线却持续上升。Lavik 将键索引放在 DRAM、将值放在 SSD,这让此类场景值得单独评估:更大的值无需全部装入内存,但每次替换和响应仍然需要搬运更多字节。在基础设施评审中,对象增长应有独立的工作负载预算。

假设一个缓存以 String 值保存一千万个序列化对象。键、过期策略和访问分布保持不变,每个值则从 1 KiB 增至 8 KiB。按十进制 GB 计算,当前有效值的总字节数从 10.24 GB 增至 81.92 GB。这只是载荷计算,不包括键、记录元数据、旧版本和空闲空间预留,也不是 Lavik 内存或磁盘占用的实测结果。

索引分离了容量需求,但不意味着内存占用固定

Lavik 的运行时索引保留键身份、逻辑版本和记录位置,值则存储在文件、裸块设备或 SPDK NVMe 命名空间中。在这个场景里,载荷增长本身不会让顶层索引条目数量成倍增加。应用可以扩大对象,而不必仅为容纳完整值,就按同样幅度增加 DRAM。

这种分离并不保证进程内存恒定。活跃追加流使用暂存缓冲区,读取需要 I/O 缓冲区,请求和响应也有各自的生命周期。对于使用外部 extent 存储的大值,读取路径会读取、校验各个 extent,再组装逻辑值。这里的 8 KiB 假设并不能证明已经跨过 extent 使用阈值。Lavik 的内存限制约束持续保留的状态,并非严格的 RSS 上限,因此评估时应分别观察保留内存和 RSS。

QPS 不变,载荷流量可能增至八倍

继续假设该应用每秒替换 50,000 个完整对象。按十进制 MB 计算,写入携带的值字节量将从 51.2 MB/s 增至 409.6 MB/s。如果每秒成功读取 50,000 个完整对象,返回值的字节量也会发生相同变化。这些计算仅描述应用载荷,不包括协议封装,也不代表实测设备带宽、物理 I/O 次数或可达到的吞吐量。

对序列化对象来说,这一区别很实际:应用只修改一个字段,也可能需要替换整个 String。Lavik 追加不可变的记录版本,并把索引更新到新位置。普通替换发生后,在新版本刷写完成之前,旧的持久化版本仍计入物理有效数据。因此,键数量和当前有效对象数量都保持稳定时,存储层仍可能持续处理大量版本更替。

空间回收有明确的完成边界

过时数据占用的字节不会立即变成可分配空间。普通记录块只有在已持久化、不再活跃、没有 pin,且有效数据比例不超过 50% 时,才会成为 Lavik 的碎片整理候选。回收过程会搬迁当前记录,等待目标记录持久化并释放所有 pin,然后才能退役源块。之后,块还要经过分配位图和冷块复用流程。对象变大后,评估需要回答另一个问题:在实际覆盖写模式下,回收空间的速度能否跟上需求?

应同时观察可用存储块、脏暂存字节、待完成刷写和碎片整理活动。仅看当前有效载荷大小,无法判断持续替换还剩多少空间。也要区分写入确认与持久性:普通写入成功响应不代表同步 fsync 已完成。只检查成功响应的容量测试,既无法说明空间回收表现,也无法明确故障语义。

已发布基准没有测量这次对象扩容

2026 年 9 月 18 日的 beta SPDK 报告使用 1,024 字节的值、均匀随机 GET 或覆盖式 SET,以及深度为一的流水线。一千万键测试点持续 30 秒,十亿键测试点持续 60 秒。报告组合了 22 个新测得的 Lavik 数据点和 124 个复用的其他产品对照数据点,每种命令、连接数和服务端线程配置组合只测量一次。这些结果支持所记录的工作负载,不能用于预测 8 KiB 值的吞吐量,也不能证明另一种载荷大小下的持续回收能力。

同样,更大的值可能让值容量脱离 DRAM 更有吸引力,却不能据此认定整个部署节省了多少成本。值容量的算术关系不是总成本测量,也不代表延迟、持久性、可用性或运维服务等价。历史报告中的实例价格估算明确不是完整的总拥有成本比较。在这个场景下,成本模型除了计算保留更大载荷所需的容量,还应计入搬运这些载荷和回收空间所需的资源。

为载荷增长安排独立验收

在可丢弃的部署中,对比旧对象编码与拟采用的新编码,同时固定键数量和应用操作比例。为应用延迟、成功吞吐量、保留内存、RSS 和可用存储设定验收标准。加入持续覆盖写和有代表性的过期行为。既要观察流量持续时可用容量能否稳定,也要观察流量减小后后台维护能否跟上。这些是建议开展的评估步骤,不是新增的已报告测试。

Lavik 0.1.0-beta.1 是采用 Apache 2.0 许可证的 beta 项目,提供广泛的 Redis 兼容接口。采用前应验证应用依赖的具体命令和运维行为。可以从下面的基础配方开始:所提供的 Docker 回执记录了 ARM64 minimal beta 软件包在 Linux 7.0.12-linuxkit、glibc 2.39 环境下通过五项命令检查和一次正常关闭后的重启检查。这个有限结果不能验证假设中的大对象工作负载、SPDK 性能或断电恢复。部署仍需要 Linux 6.1 或更新版本、可用的 io_uring 和兼容的 glibc。

读写第一组键

先安装所选 Linux 软件包和 redis-cli(Ubuntu/Debian:sudo apt-get install redis-tools)。适用于 Minimal 和 Standard。在终端 1 进入解压后包含 ./lavik 的软件包目录,再粘贴下面的 Bash 脚本并保持运行。在终端 2 执行客户端命令。启动 Lavik 无需修改 PATH、使用 root 权限或配置 systemd 服务。

终端 1 · 启动服务器
set -euo pipefail
if [ ! -x ./lavik ]; then
  echo "Run this from the extracted Lavik package directory (the folder containing ./lavik)." >&2
  exit 1
fi
mkdir -p /tmp/lavik-example
if [ ! -e /tmp/lavik-example/data ]; then
  fallocate -l 512M /tmp/lavik-example/data
fi
./lavik --bind 127.0.0.1 --port 6379 --metrics-port 0 \
  --network kernel --storage uring \
  --threads 2 --no-pin-workers --registered-buffer-mb-per-worker 64 \
  --shutdown-checkpoint --data-file /tmp/lavik-example/data \
  --log-dir /tmp/lavik-example/logs
终端 2 · 命令与预期结果
$ redis-cli -h 127.0.0.1 -p 6379 --raw PING
PONG

$ redis-cli -h 127.0.0.1 -p 6379 --raw SET greeting 'hello from Lavik'
OK

$ redis-cli -h 127.0.0.1 -p 6379 --raw GET greeting
hello from Lavik

$ redis-cli -h 127.0.0.1 -p 6379 --raw HSET user:42 name Ada
1

$ redis-cli -h 127.0.0.1 -p 6379 --raw HGET user:42 name
Ada
✓ 查看实际执行记录

0.1.0-beta.1 · Linux-7.0.12-linuxkit-aarch64-with-glibc2.39 · 2026-10-05

此记录验证功能行为,不验证 NVMe 性能、断电持久性或 SLA。

{
  "recipeId": "basic-commands",
  "platform": "Linux-7.0.12-linuxkit-aarch64-with-glibc2.39",
  "steps": [
    {
      "argv": [
        "redis-cli",
        "-h",
        "127.0.0.1",
        "-p",
        "6379",
        "--raw",
        "PING"
      ],
      "expected": "PONG",
      "actual": "PONG"
    },
    {
      "argv": [
        "redis-cli",
        "-h",
        "127.0.0.1",
        "-p",
        "6379",
        "--raw",
        "SET",
        "greeting",
        "hello from Lavik"
      ],
      "expected": "OK",
      "actual": "OK"
    },
    {
      "argv": [
        "redis-cli",
        "-h",
        "127.0.0.1",
        "-p",
        "6379",
        "--raw",
        "GET",
        "greeting"
      ],
      "expected": "hello from Lavik",
      "actual": "hello from Lavik"
    },
    {
      "argv": [
        "redis-cli",
        "-h",
        "127.0.0.1",
        "-p",
        "6379",
        "--raw",
        "HSET",
        "user:42",
        "name",
        "Ada"
      ],
      "expected": "1",
      "actual": "1"
    },
    {
      "argv": [
        "redis-cli",
        "-h",
        "127.0.0.1",
        "-p",
        "6379",
        "--raw",
        "HGET",
        "user:42",
        "name"
      ],
      "expected": "Ada",
      "actual": "Ada"
    }
  ],
  "status": "passed",
  "binaryVersion": "lavik 0.1.0-beta.1",
  "executableOnPath": false,
  "workingDirectory": "/opt/lavik",
  "sourceCommit": "3955b98d43b312324aa8d52775df52cfb111c0d0",
  "packageVersion": "v0.1.0-beta.1",
  "gracefulRestart": "passed"
}