
Garnet 基准测试指南从 RESP 端到端压测到 BenchmarkDotNet 微基准与 Tsavorite 设备层【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnetGarnet 仓库的benchmark/目录集中承载了面向不同层次的基准测试工具面向完整 RESP 服务的Resp.benchmark端到端吞吐量与延迟、基于 BenchmarkDotNet 的BDN.benchmark单命令 CPU/分配微基准以及 Tsavorite 存储引擎层级的Device.benchmark原始 IDevice IOPS与KV.benchmark引擎吞吐。本文以 benchmark/README.md 为骨架逐一讲解各工具的作用、关键参数、三大压测场景与可复现的 8×NVMe RAID-0 实测矩阵帮助读者掌握从纯内存命中到NVMe 随机读再到LocalMemory 软件上限的完整压测方法论以及如何用命令行一键复现仓库文档中的官方结果。基准测试工具全景四层压测栈Garnet 的基准测试从下到上分为四层每层都在上一层的基础上叠加每操作的额外工作Device 层Device.benchmark直接测 TsavoriteIDevice后端的随机读 IOPSLinux 上为 Native libaio / io_uringWindows 上为 IOCP另有 FileStream、RandomAccess 与纯内存的LocalMemory用于确认后端能否打满原始 NVMe 上限。KV 层KV.benchmark通过 Tsavorite 引擎的BasicContext安全路径测量 load插入与 runRUMD 读 / 更新 / RMW / 删除吞吐。Resp 层Resp.benchmark以完整 RESP 服务器为被测对象驱动 GET/SET/INCR/MGET 等负载是四层中的最顶层。fio 层作为磁盘原始能力的参照物用于划定设备层之上各层的理论上限。四层的关系可以表达为Resp ≤ KV ≤ Device ≤ fio每层都有对应的磁盘NVMe storage-bound与LocalMemory内存场景用于横向对比。需要注意三层基准使用的数据集和配置各不相同因此它们的绝对数值不能直接互相比较——比较时应关注各层相对其参照物fio 或 LocalMemory的百分比而不是跨层数字本身。Resp.benchmark端到端吞吐与延迟压测Resp.benchmark是面向完整 RESP 服务器的端到端基准工具位于 benchmark/Resp.benchmark。它既可以通过 TCP 驱动任何 RESP 服务器Garnet、Redis、KeyDB、Dragonfly也可以驱动进程内嵌的 Garnet 服务器。其完整可选参数可通过dotnet $RB --help查看参数定义见 Options.cs。两种测量模式离线模式Offline--op X使用预构建的请求批次大小为-bN 个线程循环执行Send → CompletePending。报告吞吐量ops/sec用于打满服务器。离线模式默认使用LightClient——一个零分配的流水线客户端。在线模式Online--online每个线程在途请求数为 1--itp K可增加每操作延迟通过 HdrHistogram 记录并每 2 秒打印一次。用于获取延迟曲线。在线模式默认使用GarnetClientSession异步流水线客户端支持--itp。可用客户端客户端通过--client选择枚举定义见 Common/ClientTypes.cs客户端说明LightClient离线模式默认零分配流水线客户端GarnetClientSession在线模式默认异步流水线支持--itpGarnetClientGarnet 异步客户端SERedisStackExchange.Redis用于与 Redis 做同类对比apples-to-applesInProc进程内嵌无 TCP只测服务器 CPU构建与运行dotnet build benchmark/Resp.benchmark/Resp.benchmark.csproj -c Release -f net10.0 dotnet build main/GarnetServer/GarnetServer.csproj -c Release -f net10.0 RBbenchmark/Resp.benchmark/bin/Release/net10.0/Resp.benchmark.dll GSmain/GarnetServer/bin/Release/net10.0/GarnetServer.dll dotnet $GS --port 6379 # 启动服务器 dotnet $RB --op GET --dbsize 1000000 -t 8 -b 512 --runtime 15 # 离线吞吐量务必在Release构建下测量。--op支持的操作类型定义在 Common/OpType.cs涵盖字符串类GET、SET、MSET、MGET、INCR、SETEX、DEL、Bitmap 类SETBIT、GETBIT、BITCOUNT、BITPOS、BITOP_AND/OR/XOR/NOT、BITFIELD 系列、HyperLogLog 类PFADD、PFCOUNT、PFMERGE、有序集合与 Geo 类ZADD、ZREM、ZCARD、GEOADD、ZADDCARD、PubSubPUBLISH、SPUBLISH、事务与脚本类等。关键参数参数默认值作用--opGET离线下压测的操作GET、MGET、INCR、SET、ZADD……--dbsize1024不同键的数量除非使用-s否则会预加载--valuelength8值字节数使用--keylength 16 --valuelength 96可得到 128 B 记录与 KV/Device 基准对齐-t1,2,4,8,16,32线程数扫描离线-b4096每流水线的请求数离线吞吐量的主导旋钮1024是较好的默认值在线强制为 1--runtime15每个单元的运行秒数0 仅加载不运行-sfalse跳过加载对已预加载的服务器运行--itp1在线模式每线程在途请求数--zipffalse使用偏斜键θ0.99代替均匀分布包括在每个--cluster-bench分片内部另有--load-threads初始数据加载阶段线程数默认 8、--auth认证密码、--tls/--tlshost/--cert-file-name/--cert-passwordTLS 支持、--cluster-bench跨集群分片分发负载、--replica-ops-percent写操作按百分比路由副本、--logger-level、--file-logger等参数完整列表见 Options.cs。两大阶段加载与运行工具分两个阶段工作先加载键到数据库再对单个命令运行基准。两个阶段都使用ReqGen生成请求其构造函数见 Resp.benchmark/OfflineBench/ReqGen.cs暴露了以下核心参数public ReqGen( int Start, // 键偏移量 int DbSize, // 数据库中的键数量 int NumOps, // 要执行的操作总数 int BatchSize, // 一批中的操作数 OpType opType, // 要执行的操作如 GET、MSET、INCR bool randomGen true, // 按键顺序还是随机生成 bool randomServe true, // 随机还是按生成顺序服务请求 int keyLen default, // 键的最小字节数默认等于 DbSize 的最大位数 int valueLen default) // 值的总字节数LoadData(loadDbThreads, BatchSize, keyLen, valueLen)用于把数据加载进数据库Run(opType, TotalOps, NumThreads, BatchSize, runTime, ...)用于执行一轮指定操作的基准迭代。三大压测场景读落在哪里决定了场景三个服务器配置的核心区别在于读请求落在哪里并通过线程扫描获得离线吞吐量。其中分散-聚合 GET--sg-get默认开启会把连续的 pending GET 合并成一次向量化 IO——这对设备支撑的场景至关重要。场景 1内存受限——数据全部在 RAM默认服务器配置数据集完全落在内存日志中读请求永远不会触达设备dotnet $GS --port 6379 dotnet $RB --op GET --dbsize 16777216 --valuelength 100 --runtime 0 # 加载 16 M × 100 B dotnet $RB -s --op GET --dbsize 16777216 --valuelength 100 -t 1,2,4,8,16,32 -b 1024场景 2NVMe 存储受限——读请求真实命中磁盘通过给存储分层storage tiering配一个极小的内存日志让 100 M 数据集中的约 99.9% 落在 NVMe 上每个 GET 都是一次随机设备取数。使用 100 M × 128 B 记录--keylength 16 --valuelength 96与 KV/Device 基准一致——128 B 记录在阵列的 512 B 扇区上读取。参考主机为8×NVMe RAID-0/raidfio随机读上限 ≈4K 时 8.24 M IOPS/512 B 时 8.20 M——阵列是 IOPS 受限的块大小几乎不影响Garnet 端到端可维持约 7.3 M约为 fio 的 89%。DATA/raid/garnet; mkdir -p $DATA # 服务器固定到 NUMA 节点 0客户端在节点 1 上驱动 numactl --cpunodebind0 --membind0 dotnet $GS --port 6379 --bind 127.0.0.1 \ --memory 16m --page 4m --segment 1g --index 8g --storage-tier --logdir $DATA \ --device-type Native --device-io-backend Libaio --logger-level Warning numactl --cpunodebind1 --membind1 dotnet $RB --op MSET --dbsize 100000000 \ --keylength 16 --valuelength 96 --client LightClient --load-threads 32 -b 4096 --runtime 0 numactl --cpunodebind1 --membind1 dotnet $RB -s --op GET --dbsize 100000000 \ --keylength 16 --valuelength 96 --client LightClient -t 8,32,48,64 -b 1024 --runtime 12在相信 GET 数字之前务必核对加载是否完整。一个从未被写入的键会直接从内存应答而不触达设备而一次未命中比一次磁盘读便宜约 30 倍因此部分加载会静默虚高结果——未加载完成的存储在参考主机上会报告 150 M ops/sec。确认MSET步骤在其[Total time]行报告约 1 亿次操作仓库自带的生成脚本 nvme-raid0-matrix.sh 会强制这一校验它以加载器报告的 op 数为准要求不低于 dbsize 的 99%因为对 100 M 键的存储做全索引扫描作为启动探针太慢。backendNUMAt8t32t48t64Libaiosrv node-0 / cli node-11.82 M5.70 M6.97 M7.06 MLibaiono pin1.38 M5.09 M5.72 M5.57 M几个要点均来自仓库 README 的实测结论--index 8g用于 100 M 键默认 128 m 会因哈希链造成 3–4 倍降速。峰值落在t48–64区间RESP 服务器的流水线客户端连接通过服务器自身的网络线程与完成线程驱动在途深度因此吞吐会持续爬升越过原始设备在 t32 的峰值后才被排队成本追上。NUMA 固定在这里最重要有状态服务器把服务器固定到节点 0、客户端固定到节点 1可使 t48 从 5.72 M 提升到 6.97 M、t64 从 5.57 M 提升到 7.06 M。Libaio是 Linux 默认后端无需 ring 调优。Uring现在会自动把 ring 数调整为min(2 × cores, 64)——与--device-completion-threads解耦——因此开箱即可与 libaio 竞争超高并发时可用--device-io-contexts N显式设置 ring 数应大于等于连接数。容量旋钮在上述命令中保留默认值可追加--device-completion-threads 8 --device-throttle-limit 4096手工调优。--device-throttle-limit 4096适合本阵列单盘/SATA 盘应降到 512/128。场景 3内存设备受限——读请求命中内存中的设备与场景 2 相同的分层服务器但使用无系统调用的LocalMemory设备完整 RESP pending 路径磁盘延迟为零软件上限与 KV 和 Device 的 LocalMemory 运行对齐。把场景 2 中的设备参数替换为... --device-type LocalMemory --device-completion-threads 4 --device-throttle-limit 512 ...参考数据10 M × 100 Bt16-b 1024时约2.7 M ops/sec-b 256时约3.7 M。可复现的实测矩阵8×NVMe SSD RAID-0以下是场景 2完整 GET 吞吐矩阵采用开箱即用的设备默认配置——只设置--storage-tier和--device-io-backend--device-completion-threads、--device-throttle-limit、--device-io-contexts全部保持服务器默认因此这代表运维人员不做任何设备调优即可获得的水平。每个单元取 3 次的中位数。主机2× Intel Xeon Platinum 8480CL每插槽 56 核 × 2 线程224 个逻辑 CPU2 个 NUMA 节点约 2 TB DDR58× Kioxia KCM6DRUL3T843.84 TB PCIe-Gen4 NVMe 组成 LinuxmdRAID-0/dev/md1512 KB chunkext4约 28 TBUbuntu 24.04.4 LTS内核 6.8.0-136.NET 10.0.302fs.aio-max-nr 4194304。该阵列的fio随机读上限4K 时 8.24 M IOPS、512 B 时 8.20 M IOPS32 jobs × QD64io_uringO_DIRECT8 个文件——阵列在这些尺寸下是 IOPS 受限的上限实际上与块大小无关。负载100 M × 128 B 记录--keylength 16 --valuelength 96分层落到阵列上--memory 16m --page 4m --segment 1g --index 8g100% 随机 GET客户端-b 1024每单元 12 秒。生成脚本会在测量前验证加载确实写入了约 1 亿个键因此下面每个单元都是存储受限的而非部分由内存未命中服务。backendNUMAt8t32t48t64Libaiosrv node-0 / cli node-11.82 M5.70 M6.97 M7.06 MLibaiono pin1.38 M5.09 M5.72 M5.57 MUringsrv node-0 / cli node-11.82 M6.13 M7.32 M6.89 MUringno pin1.48 M4.60 M5.55 M6.29 M结论要点峰值约 7.3 M ops/securing、固定、t48——约为同形态随机读fio上限的89%128 B 记录经阵列的 512 B 扇区取数端到端穿过 RESP 协议和 Tsavorite pending-read 路径而非原始设备 IO驱动。与 fio 的剩余差距来自 RESP pending-read 路径原始设备层在该阵列上可达约 8.4 M。两个后端在不同线程数达到峰值。Uring 在 t48 见顶、t64 回落——额外的客户端并发带来的排队成本超过了在途深度的收益libaio 在 t64 仍在爬升。应扫描 t48–64 区间而不是假定单一最优值。默认配置即可达到调优后的峰值。Uring 的智能 ring 数默认值min(2 × cores, 64)个 ring与 4 个完成线程解耦无需任何参数即可把 ring 匹配到硬件libaio 无需 ring 调优。显式调优--device-completion-threads 8 --device-throttle-limit 4096uring 加--device-io-contexts 96在本主机上每个单元只移动几个百分点。NUMA 固定是这台双插槽机器上最大的单一因素有状态服务器例如 uring 在 t48 从 5.55 M 升到 7.32 M——把服务器固定到节点 0、客户端固定到节点 1。单插槽主机上固定/不固定两行会趋同。两个后端在各自拿到所需 ring 配置后都达到约 7 M 的平台——uring 在 t32–48 领先 5–8%libaio 在 t64 领先约 2%——因此任选一个后端并调优线程数即可。用生成脚本一键复现仓库提供了自动化生成脚本需 Release 构建的GarnetServerResp.benchmark、numactl以及 NVMe / O_DIRECT 挂载点DATA/mnt/nvme/garnet benchmark/Resp.benchmark/scripts/nvme-raid0-matrix.sh该脚本扫描 两个后端 × 固定/不固定 × 线程数对每个单元取PASSES默认 3次的中位数并打印上文的 Markdown 表格。可通过环境变量覆盖DATA、THREADS、PASSES、RUNTIME或设置CT/THROTTLE/URING_IOCTX来运行显式调优配置而非默认配置。脚本内置了加载完整性校验加载 op 数低于 dbsize 的 99% 即中止和按真实GarnetServer.dllPID 停止服务器的逻辑停止dotnet启动器会遗留存活的运行时子进程泄漏的自旋服务器会造成巨大的方差。离线变体MGET、InProc、SERedis、Zipf 与集群dotnet $RB --op MGET --dbsize 16777216 --valuelength 8 -t 16 -b 512 # MGET分散-聚合 dotnet $RB --op GET --dbsize 1000000 -v 100 -t 16 -b 1024 --client InProc # 仅服务器 CPU无 TCP dotnet $RB --op GET --dbsize 1000000 -v 100 -t 16 -b 256 --client SERedis # 与 Redis 同类对比 dotnet $RB --op GET --dbsize 16777216 -v 100 -t 16 -b 1024 --zipf # 偏斜键θ0.99 dotnet $RB --cluster-bench --op GET --dbsize 16777216 -t 16 -b 1024 --zipf # 每个分片内部偏斜在线模式延迟测量# 单客户端 GET 延迟最干净的读数 dotnet $RB --online --op-workload GET --op-percent 100 --dbsize 1000000 -t 1 -b 1 --runtime 30 # 50/50 GET/SET 尾延迟16 个连接 dotnet $RB --online --op-workload GET,SET --op-percent 50,50 --dbsize 1000000 -t 16 --runtime 60 --client GarnetClientSession # 固定施加负载8 连接 × 64 在途 dotnet $RB --online --op-workload GET --op-percent 100 --dbsize 1000000 -t 8 --itp 64 --client GarnetClientSession--runtime -1表示运行到被中断为止在线模式下0无效。在线模式也支持--client SERedis如 ZADD/ZCARD 混合--op-workload ZADD,ZCARD --op-percent 50,50 --keylength 8 --client SERedis。在线模式会强制-b 1并发度用--itp表达--pool参数在LightClient下不受支持需改用 GarnetClientSession / GarnetClient / SERedis。在线模式方法论在相信数字之前必读纯加载--op GET --runtime 0只播种键空间不进入运行阶段之后读阶段加-s。验证数据确实在磁盘上redis-cli INFO store——Log.TailAddress应与数据集大小匹配且Log.HeadAddress ≈ TailAddress数据已从小的内存区域驱逐到设备。确认读确实命中设备。在大内存主机上整个数据集会落在页缓存里存储设备以O_DIRECT打开读会绕过页缓存。读阶段中iostat -x 1应显示 NVMe 的r/s ≈ ops/sec、aqu-sz ≈ --device-throttle-limit。在共享机器上应读取每进程的/proc/GarnetServer pid/io的read_bytes——它排除其他租户的 IO。多跑几轮读阶段并取稳态——第一轮是热身。公平 A/B每个变体分别构建/加载/运行。为对抗 CPU 时钟漂移可在不同端口同时跑两个服务器并交错运行。停止服务器要按其真实GarnetServer.dllPID——停止dotnet启动器会遗留存活的运行时子进程泄漏的自旋服务器会造成巨大方差。输出解读离线模式[Total time]: ms for ops和[Throughput]: ops/sec ops_done × batch / runtime。在线模式每 2 秒打印min; 5th; median; avg; 95th; 99th; 99.9th; total_ops; iter_tops; tpt(Kops/s)单位为 µs每线程 HdrHistogram。常见问题排查症状解决办法Skipload not supported with --online去掉-s或改用离线模式在线模式出现-b N1警告工具会强制-b 1并发请用--itp--pool与LightClient组合不受支持改用 GarnetClientSession / GarnetClient / SERedis磁盘受限吞吐远低于 KV.benchmark服务器端瓶颈——用dotnet-trace做性能剖析--dbsize % loadThreads ! 0把--dbsize取整为加载线程数的倍数另外若想知道命中率可用任意客户端执行INFO stats查看garnet_hit_rate指标理想情况下应接近 100前提是服务器启用了指标采样--latency-monitor --metrics-sampling-freq 5。BDN.benchmark精确可复现的微基准BDN.benchmark命令行工具基于 BenchmarkDotNet它先解析 Garnet 自定义参数--frameworks/--fw过滤 .NET 版本--opparams/--op过滤 ACL/AOF/AAD 参数组合再把剩余参数透传给 BenchmarkDotNet。默认BaseConfig使用工作台 GC非并发以便 MemoryDiagnoser 确定性地测量分配Server GC 下GC.GetTotalAllocatedBytes会间歇性高估对 net8.0/net10.0 分别建立 Job 并显式禁用动态 PGODOTNET_TieredPGO0保证结果可复现。用法列出所有可用基准--list flat平铺或--list tree树状dotnet run -c Release -f net8.0 --list flat运行特定基准可用--filter。例如以默认配置运行所有 RESP 协议写类基准默认会在所有 .NET 目标运行时上运行禁用动态 PGOdotnet run -c Release -f net8.0 --filter *RespIntegerWriteBenchmarks*--filter支持通配符可按类名或方法名筛选。Garnet 自定义参数示例dotnet run -f net10.0 -c Release -- --fw net10.0 --op none -f *RawStringOperations*其中--op none表示只跑无 ACL/AOF/AAD 参数组合的基准。更多 BenchmarkDotNet 命令行选项见其控制台参数文档。编写微基准微基准类按功能域组织在 benchmark/BDN.benchmark 下例如Operations/RawStringOperations.csSET/SETEX/SETNX/GET/INCR/DECR 等字符串命令、Operations/HashObjectOperations.cs、Operations/SortedSetOperations.cs、Operations/JsonOperations.cs、Operations/ScriptOperations.cs、Cluster/、Lua/、Auth/等。所有操作基准继承 OperationsBase.cs它利用EmbeddedRespServer在进程内创建真实服务器与RespServerSession通过 GlobalSetup 一次性完成配置支持按参数开关 ACL 认证、AOF 或 AAD 认证从而测量不同运行模式下的命令成本并以批大小 100 执行命令——据此可便捷换算吞吐5 µs ≈ 20 Mops/sec、10 µs ≈ 10 Mops/sec。例如BasicOperations.InlinePing直接以PING\r\n原始字节衡量内联命令解析成本。编写新微基准时应遵循 BenchmarkDotNet 的 Microbenchmark Design Guidelines基准方法应幂等、避免 JIT 消除、使用全局 Setup 预热等。Device.benchmark 与 KV.benchmark下两层的纵深参考Resp.benchmark的三个场景与下层基准一一对应理解它们有助于定位瓶颈到底在哪一层。Device.benchmark原始 IDevice IOPS位于 libs/storage/Tsavorite/cs/benchmark/Device.benchmark用扇区对齐的图案填充 backing 文件然后 N 个 worker 各在随机偏移上发起device.ReadAsync并在完成回调中回收缓冲区。吞吐只统计成功完成。其 NVMe 存储受限场景使用与 KV/RESP 兼容的配置--sector-size 512阵列的逻辑块大小与--file-size 1280101580812.8 GB 100 M × 128 B。在 8×NVMe RAID-0 上libaio8 个 io_context与 uring智能默认 64 个 ring都能达到 fio 上限——libaio 8.23 M / uring 8.45 M——而把 uring 的 ring 显式欠配到 8--device-io-contexts 8会暴露每 ring SpinLock 上限跌到约 2.9 M。这解释了 RESP 层为什么会建议uring使用min(2 × cores, 64)的智能默认。核心参数--device-throttle-limit用户侧在途上限快阵列用 4096、单 NVMe 用 512、LocalMemory 用大值、--device-completion-threads后台 drainer 数快阵列 8、--device-io-contexts内核 io_context/io_uring ring 数与 drainer 解耦。LocalMemory 场景中当--device-completion-threads --threads每提交者一个 SPSC ring时测得提交/完成路径上限约78 MIOps/st32——这是 KV/RESP LocalMemory 运行的上界。其完成模型为 block-on-signal非忙等在云级高延迟设备Azure/EBS 级约 0.5–2 ms上是稳态路径吞吐按延迟×并发受限。KV.benchmarkTsavorite 引擎吞吐位于 libs/storage/Tsavorite/cs/benchmark/KV.benchmark在 8 字节键 定长值数据集上测量 load 与 runRUMD吞吐通过安全的BasicContext路径运行刻意做到零每操作分配、NUMA 固定 worker、无伪共享的记分板与中央 tick 计时。同样有三个场景--device null的纯引擎上限、--device native --log-memory 16m的 NVMe 存储受限libaio 在 t64 达约7.7 M为 fio 的约 94%、--device localmemory --device-inline-completion的内存设备受限约19.6 Mt32。在磁盘受限场景负载必须用 100 M 键——更小的数据集只触及少量 NAND die 会低估 IOPS。结语一套贯穿四层的压测方法论从Resp.benchmark的三大场景出发Garnet 提供了自上而下贯穿完整 RESP 服务器 → Tsavorite 引擎 → 原始 IDevice → fio的四层压测栈先在 RESP 层用--dbsize/--valuelength/--keylength对齐 128 B 记录、用-b 1024控制流水线深度、用-s --runtime 0完成纯加载再借助redis-cli INFO store与iostat -x 1验证读请求真正落在设备上若吞吐低于预期用 Device/KV 层判断瓶颈属于设备后端ring 配置、throttle 上限还是服务器侧NUMA、完成线程、流水线深度。对单条命令的成本分析则落到BDN.benchmark的嵌入服务器微基准。这套方法论既给出了开箱即用的复现脚本nvme-raid0-matrix.sh也给出了手工验证与调优的完整手段是理解与优化 Garnet 性能的可靠起点。【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考