服务器还有空闲内存,缓存为何已无法继续增长

规划 Lavik 缓存增长,既要计算整台服务器的容量,也要计算每个工作线程的预算。一个假设的键分布案例说明:NVMe 仍有空间、总内存仍有余量,并不保证下一个键能被接纳。

容量预测可能显示 SSD 能容纳更多值,内存也能容纳更多索引,却仍然漏掉阻止缓存增长的约束。Lavik 将保留内存的准入预算划分为固定的工作线程份额,各线程不能借用其他线程的闲置余额。因此,规划缓存增长时,除了总键数和值字节数,还需要考虑键在工作线程之间的分布。

Lavik 在 DRAM 中保留紧凑的键索引,将值放在存储设备上,适合评估那些内存需求主要由值容量推动的场景。本文讨论的是采用 Apache 2.0 许可证的 0.1.0-beta.1 测试版。它的存储设计改变了容量预算的构成,但仍然需要索引内存,而且这些内存的使用位置受到约束。

准入边界位于各个工作线程

Lavik 用分区编号对工作线程数取模,将 16,384 个逻辑分区分配给各线程。同一线程的索引共享条目分配区,避免稀疏分区各自占住无法共享的条目容量。但在线程之间,配置的内存预算仍按固定份额划分,保留状态的准入上限为各自份额的 90%。线程内部共享容量,并不意味着可以使用其他线程的闲置预算。

因此,键分布和流量分布需要分别考虑。一个线程即使拥有的键很少被访问,也可能承受较大的索引压力,而它未必是请求量最高的线程。反过来,请求量高也不能单独说明该线程拥有多少保留索引状态。容量规划应同时估算数据归属和访问活动。

一个通过总量检查的假设增长方案

假设配置 16 个工作线程,保留内存预算为 64 GiB。每个线程分到 4 GiB,准入阈值为 3.6 GiB,所有线程的阈值之和为 57.6 GiB。为便于演算,假设每个键占用 128 字节的保留索引内存,每个线程另有 0.5 GiB 的其他保留状态。这些都是建模假设,并非 Lavik 的实测内存开销;实际索引表示、键长度、过期状态、哈希表增长及其他保留分配,都需要按目标工作负载评估。

现在让键数从 8,000 万增长到 1.6 亿,每个值始终为 1 KiB。逻辑值字节数从约 76.29 GiB 增至 152.59 GiB。按每键 128 字节的索引假设,索引总内存从约 9.54 GiB 增至 19.07 GiB。再加上假设的 8 GiB 其他保留状态,总量分别为 17.54 GiB 和 27.07 GiB,都明显低于 57.6 GiB 的准入阈值之和。这里的逻辑值字节数不包括记录元数据、过时版本和存储预留空间。

假设应用的实际分区分布使某个线程在整个增长过程中始终拥有 20% 的键。它负责的键数会从 1,600 万增至 3,200 万,模型中的索引内存从 1.91 GiB 增至 3.81 GiB;加上其他保留状态后,总量从 2.41 GiB 增至 4.31 GiB。尽管整台服务器的总量看起来仍有充足余地,这个线程的预测需求已经越过 3.6 GiB 阈值。20% 的分布是本案例的假设输入,并非已报告的 Lavik 观测结果。

如果 1.6 亿个键均匀分布,同一模型中每个线程只需约 1.19 GiB 的索引状态,加上 0.5 GiB 的其他保留状态。差异完全来自数据归属分布。对于需要新增或替换索引条目的记录,Lavik 会在发布记录前进行索引容量准入;准入失败可以拒绝该操作,而不会发布无法进入索引的记录。增加 SSD 空闲空间无法解除这一内存约束。

分别核算三类预算

值预算描述逻辑载荷增长,以及支持这些载荷所需的物理存储空间。追加式替换会留下需要回收的过时版本,存储预留和元数据也会占用活跃值之外的空间。索引预算描述保留内存需求,包括它在工作线程之间的分布。任何一项都不能单独代表整台服务器的需求。

共享服务器成本包括提供缓存服务及恢复缓存所需的 CPU、主机内存、存储设备、网络和运维工作。Lavik 的内存配置不是严格的 RSS 上限:客户端缓冲区有单独的配额,分配器、请求处理和 I/O 峰值也需要余量。因此,提高该配置时,既要检查受限线程能否获得足够预算,也要检查主机内存是否足够。存储分层报告中的历史实例价格比较,并不是这个假设部署的实测总成本模型。

调整工作线程数也需要单独评估。它会同时改变分区归属和各线程固定内存份额的大小。Lavik 能在工作线程数变化后恢复数据,无需在启动期间重写数据,但旧物理块可能暂时不归键所属线程管理,之后由前台替换和碎片整理逐步迁移活跃记录。因此,增加线程数并不必然解决索引需求分布不均的问题。

下次容量评审,关注最先耗尽预算的线程

选择有代表性的数据集,按分区归属估算每个线程的键数,再根据真实的键长度、TTL 使用方式和数据类型评估保留内存需求。将拥有最多键的线程所占比例带入增长预测,不要默认每个线程都持有平均数量的键。使用计划中的线程数演练增长,记录准入失败、保留内存总量、RSS、可用存储空间和回收行为。这是建议开展的评估,并非本文报告的测试结果。

首次本地检查可以使用下面的 basic-commands 配方。所提供的主机回执记录了 ARM64 Minimal 测试版软件包在 Linux 7.0.12、glibc 2.39 环境下通过五项客户端操作及正常关闭后的重启检查。这份回执提供了基础起点,但并未验证本文的假设容量模型、内存压力下的行为或崩溃持久性。普通写入成功的响应并不代表同步持久化屏障已经完成。

读写第一组键

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

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