规划缓存增长,既要算容量,也要数键

通过一个假设容量模型,说明将值移到 NVMe 后,如何分别核算索引内存、空间回收余量与服务器共享成本。

Redis 缓存需要扩容,可能是已有值变大了,也可能是应用保留了更多键。这两种增长方式会给 Lavik 带来不同的压力。Lavik 将紧凑的键索引保留在 DRAM 中,把值放到存储上,因此可以利用 NVMe 扩展值容量。实际规划时,应分别预测键数量和值字节数。本文讨论的是采用 Apache 2.0 许可证的 0.1.0-beta.1 测试版。

相同的值容量,可能需要不同的内存预算

先列出三个量:存活键数量、平均值大小,以及每个键的有效索引内存开销。前两者相乘,可以估算逻辑值字节数;键数量乘以第三项,可以估算索引内存。但两项都不是完整的服务器预算。Lavik 还需要运行时状态和 I/O 缓冲区,存储上还存在记录元数据、等待回收的旧版本以及预留空间。

假设一个 String 缓存从 5,000 万个键增长到 10 亿个键,每个值恰好为 1 KiB。逻辑值容量将从 51.2 GB 增长到 1,024 GB,即原来的 20 倍。仅为演示计算,假设每个键的有效索引开销为 64 字节,则索引预算从 3.2 GB 增长到 64 GB。这里的 GB 使用十进制;64 字节是假设值,并非 Lavik 的实测内存开销或容量保证。

再改变应用的数据组织方式:把相同的 1,024 GB 值数据存为 20 亿个键,每个值为 512 字节。沿用上述索引假设,索引预算将变为 128 GB,而值容量没有变化。只预测值字节数,就会漏掉这部分翻倍的内存需求。真实索引开销与键的表示方式、过期状态等可选信息以及索引容量有关;集合类型还涉及其他布局因素。应使用应用实际的键和数据结构测量有效内存开销。

这里的 20 倍仅表示示例中两组逻辑值容量之比。它不能证明成本节省 20 倍、设备可用容量增加 20 倍,也不是实测总拥有成本或 SLA 等价结论。示例同样没有声称任何一组数据能装进某台特定服务器。要形成部署估算,还必须补上内存和存储余量,并验证工作负载性能。

关注压力最大的 worker,也要考虑回收延迟

总空闲内存可能掩盖局部限制。Lavik 将配置的保留内存预算划分为固定的 worker 份额;worker 不能借用其他 worker 的未用余额,保留状态的准入上限为各自份额的 90%。哈希槽会映射到 worker,因此除了总键数,还要关注键的分布。容量评估应同时记录 worker 间的分布和聚合内存指标。配置的内存限制约束的是持续保留的状态,并非进程 RSS 的瞬时硬上限。

存储余量也需要按数据生命周期估算。覆盖写会追加新版本,在替代版本完成刷盘之前,旧的持久化版本仍计入占用。废弃空间只有完成回收后才能重新使用。过期可能留下墓碑及其索引项;删除索引项后,索引才有机会逐步缩容。因此,一轮集中到期之后,存活值字节数、保留的索引内存和可用存储空间可能在不同时间发生变化。

如果缓存的数据更替频繁,应把持续覆盖写和过期流量纳入容量评估。在维持目标存活数据规模的同时,观察可用块、保留内存、RSS、待处理刷盘和回收活动。这是建议进行的评估,并非已经报告的测试结果。其目的是先判断部署能否在目标流量下持续回收空间,再决定需要预留多少余量。

按整个部署核算共享成本

值存储只是预算中的一项。CPU、DRAM、网络容量、副本、恢复所需容量和运维投入,都属于提供服务的整个部署。只有在性能和恢复行为仍满足要求时,更大的逻辑数据集才可能把共享服务器成本分摊到更多字节上。所提供存储分层报告中的历史成本估算,比较的是托管实例与自管虚拟机;报告明确说明,这不是完整 TCO。

下方计算器用于说明容量假设如何影响模型成本比。三个可调参数分别是 DRAM/SSD 容量单价比、Lavik 内存占值容量的比例,以及共享成本占 Redis 值内存成本的比例。模型将存储放大固定为 1,并假设两端副本数相同且不使用压缩。默认值仅作演示;两种介质各自的绝对价格与空间回收余量需要在自己的部署工作表中另行核算,这个模型不是实测总成本。

可调整的假设

默认值用于解释公式,不是硬件报价或实测内存占用。假设相同副本数、不使用压缩、存储放大为 1。

此假设下的模型成本比6.47×

模型中 Lavik 成本降低 84.5%

Redis = shared + payload × DRAM
Lavik = shared + index × DRAM + payload × SSD

该模型不预测延迟、持久性或可用性。性能需求还可能改变服务器数量与配置。

应把基准结果视为候选配置的证据,而不是将 TB 换算成服务器数量的系数。9 月 18 日的 SPDK 报告使用六块 NVMe 设备和 16 个 Lavik worker,测量了 10 亿个 1 KiB 值。存储组的各测试点采用 60 秒窗口、均匀随机 GET 或覆盖 SET,流水线深度为一。整个报告的两组数据集共包含 22 个新测 Lavik 点和 124 个复用的对照系统点。这些结果不能证明上述假设缓存的长期运行、过期操作密集的工作负载下的容量、相同的崩溃持久性或 SLA。

写入成功的响应并不代表同步 fsync 已完成。请按实际工作负载评估持久性、复制和恢复要求。

把示例变成自己的容量工作表

记录当前及预计的键数量、值大小分布、键长度、TTL 行为和命令组合。用代表性数据集的观测结果替换每键 64 字节的假设,再分别加入运行时内存和存储余量。核算部署成本之前,先评估恢复和复制要求。Lavik 的广泛 Redis 兼容性仍取决于具体命令和行为,因此也应验证应用依赖的语义。

先在符合要求的 Linux 主机上使用下面的基本命令配方,再用可丢弃的代表性数据集开展容量评估。该配方用于检查基本接口,不能验证内存模型或确定生产容量。发布包面向 x86_64 和 ARM64,需要 Linux 6.1 或更新版本、可用的 io_uring 和兼容的 glibc。Minimal 包使用内核 TCP 与 io_uring;SPDK 基准配置需要 Standard 包。

读写第一组键

先安装所选 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-09-26

此记录验证功能行为,不验证 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"
}