每页六次缓存读取:把基准 QPS 转成应用负载计划

通过一个假设的页面渲染服务,分析命令扇出、响应字节、独立到达的请求和后台刷新如何改变 Lavik 基准吞吐量的实际含义。

页面渲染服务可能尚不需要更多计算资源,缓存就已经超过内存容量。设想一个商品目录:每个页面由多个独立缓存的对象组成,其中包括访问频率很低的长尾商品对象。Lavik 0.1.0 提供了保留更多目录数据、继续通过 Redis 兼容接口直接访问它们的具体理由:键索引留在 DRAM,值存放在 NVMe。接下来,部署决策需要一份分别计算页面、缓存命令和字节数的应用负载模型。

先算清一个页面需要多少工作

假设服务每秒收到 10,000 个页面请求,每页通过独立的 GET 读取六个不同的缓存对象,每个对象恰好为 1 KiB。所有查询均命中,没有重试,也没有共享的本地缓存命中。由此得到每秒 60,000 条 GET,以及每秒 61,440,000 字节的返回值,约 58.6 MiB/s。RESP 封装、请求中的键、传输开销和最终 HTTP 响应还需另算。这些数字来自应用规划假设和算术,并非 Lavik 实测结果。

六条独立 GET 的选择很重要。并发发送时,页面需要等待最慢的对象;串行发送时,各次等待会累加。两种方式都需要返回六个值。改成一条 MGET 会改变命令数和执行路径,但不会减少六个对象的有效载荷。Lavik 通过共享锁事务协调 MGET,并根据可用缓冲区分批提交符合条件的本地磁盘读取。纯 GET 基准并未测量这条 MGET 路径,而且 Cluster 模式要求多键请求中的所有键位于同一哈希槽。

把基准看成一份流量规格

10 月 13 日的小规格机器报告提供了一个有用的参考点。在 1 亿个键、每个值 1,024 字节的数据集上,Lavik 在 80 个连接时达到 132,749 GET 次/秒,客户端观测的 p99.9 延迟为 1.039 ms。流量由 memtier 2.5.1 生成:16 个客户端线程、独立客户端随机种子、流水线深度 1、不限速,均匀随机访问,全部为 GET。每个配置先进行 10 秒 GET 预热,每个测量点仅运行一次、持续 30 秒。服务器为 Azure L2aos_v5,标称 16 GiB 内存、两个 vCPU,实际是同一物理核的两个 SMT 线程;Lavik 使用两个 worker、内核 TCP,以及通过 SPDK 访问的专用本地 NVMe 命名空间。Defrag 全程开启。

流水线深度 1 意味着每个基准连接同时只有一个未完成请求:收到响应后才发送下一条。当响应变慢时,这些连接产生的流量也会下降。页面服务的请求却可以独立到达,即使之前的页面还在等待。因此,不能仅凭应用的 60,000 GET/s 低于基准的 132,749 GET/s,就认定容量足够;也不能把基准结果除以六,当作页面吞吐量。应用还引入了请求扇出、调度、其他工作,以及不同的到达过程。

页面完成时间需要与命令延迟分开测量。并发读取六个对象的页面,只有在六个对象全部返回后才结束缓存读取阶段,其中还包含应用排队时间。基准的 1.039 ms p99.9 是整个测量窗口内的命令百分位,既不是页面 p99,也不是页面 p99.9。共享的存储或调度延迟可能同时影响多个对象读取,因此应用模型不应假设各次延迟相互独立。

为突发流量明确设置排队预算

再假设页面到达速率在四分之一秒内升到每秒 40,000 页。这 10,000 个页面会在突发期间产生 60,000 次对象读取,相当于 240,000 GET/s 的到达速率,并要求返回约 58.6 MiB 的值。作为说明,如果应用把读取派发额度设为 60,000 GET/s,那么这四分之一秒只能派发 15,000 次读取;若全部接收,其余 45,000 次就会等待。这是在自定额度下的排队算术,不是对 Lavik 容量的预测。如果随后正常流量立即恢复到 60,000 GET/s,该额度也没有剩余派发能力来消化积压。

在发出六次读取之前,先以页面为单位决定是否接收。为渲染器设置有界等待队列和截止时间;额度耗尽时,可以返回业务允许的旧页面、略去可选内容,或在产生更多缓存工作之前拒绝该页面。保留从到达到完成的时间戳,让排队时间始终可见。Lavik 的连接数和请求缓冲区限制保护服务器资源,但它们无法判断一个仍在等待的页面对读者是否还有价值。

为刷新工作单独分配预算

假设后台 worker 还会每秒替换 5,000 个缓存对象。按同样的假设,每个对象为 1 KiB,这会额外带来每秒 5,000 条 SET,以及约 4.9 MiB/s 的入站值数据。完整计划因此是每秒 60,000 次读取加 5,000 次写入,任何一条纯命令曲线都不代表这个混合负载。在 80 个连接时,同一份 1 亿键基准的 SET 结果为 133,977 次/秒、p99.9 延迟 3.423 ms;这是按相同方法单独测得的均匀随机覆盖写窗口。不能把 GET 和 SET 的速率相加,作为混合负载容量。

为刷新 worker 设置独立派发额度和对象最大可接受年龄。页面突发期间,可以推迟尚未触及新鲜度期限的刷新,随后在有界的追赶预算内恢复。分开的额度能够控制需求,但读写仍共享服务器资源。Lavik 追加写入替换版本,并在后台回收过时存储,因此持续刷新还需要存储余量,以及对脏字节、待完成刷盘和回收进度的观测。在这个设计中,让源站保持权威,让缓存可以重建:普通写入返回成功,并不等于跨过了同步的崩溃持久化屏障。

为保留容量而采用,用页面到达流量来验证

同一份小规格机器基准在标称 16 GiB 内存的主机上容纳了 1 亿个键,逻辑值载荷为 95.37 GiB。这支持了应用的容量理由:更大的目录可以保留在 NVMe 支撑的 Redis 兼容存储中。但内存仍然重要。报告中该数据集的进程 RSS 为 5,697.3–5,733.6 MiB,约 5.6 GiB,另有 2 GiB 的大页预留;键、索引及其他开销也必须考虑。把值容量转移到 NVMe,改变了需要扩容的资源,并不能证明实测总成本下降或 SLA 等价。

下一步是在稳定版 0.1.0 上做一次应用演练:保留计划中的目录规模,让页面按独立于完成速度的节奏到达,读取实际的六个对象,并同时运行刷新 worker。加入上述四分之一秒突发及其后的恢复阶段。记录计划到达和实际接收的页面数、完成页面数、排队时长、页面延迟、各命令延迟、返回字节数和刷新对象年龄,再据此确定页面接收额度与刷新额度。Lavik 以 Apache 2.0 开源;环境准备请使用出版页脚链接的、经过测试的 0.1.0 手册。

出版参考资料——当前 0.1.0 安装、兼容性说明与手册入口:https://github.com/eloqdata/lavik/blob/471f4864357f4a5034c64e7d4972ec0e55b82d94/README.md 。10 月基准及完整测量方法:https://github.com/eloqdata/lavik/blob/471f4864357f4a5034c64e7d4972ec0e55b82d94/perf_reports/lavik-v0.1.0-2vcpu-latest-capacity-2026-10-13/README.md 。