存储引擎性能 Benchmark从可复现测试到统计显著性分析的工程方法一、Benchmark 的结果不可复现比没有 Benchmark 更危险我的 SSD 顺序写能到 2 GB/s——这个数字在什么条件下测的单线程还是多线程直写还是缓冲写数据块大小 4K 还是 1M是否预热是否清除了 OS Page Cache如果这些条件不明确Benchmark 数字就是空中楼阁。存储引擎的 Benchmark 比 SSD 更复杂涉及压缩算法、缓存策略、合并策略、并发控制等多个变量。一个不控制变量的 Benchmark结果可能每次都不同甚至得出相反结论。本文要解决的问题是如何设计可复现、可对比、有统计显著性的存储引擎 Benchmark。二、Benchmark 工程体系与统计方法flowchart TB A[Benchmark 设计] -- A1[变量定义br/固定/可控/观测] A -- A2[工作负载建模br/读写比/数据特征/访问模式] A -- A3[指标定义br/延迟/吞吐/IOPS/尾延迟] A1 -- B[测试执行] A2 -- B A3 -- B B -- B1[环境隔离br/CPU 绑核/NUMA/磁盘独占] B -- B2[预热阶段br/填满缓存/触发合并] B -- B3[稳态测量br/多次迭代取统计值] B3 -- C[统计分析] C -- C1[描述统计br/均值/中位数/P99] C -- C2[变异系数br/CV 5% 才可信] C -- C3[显著性检验br/t-test / Mann-Whitney U] C1 -- D[报告生成] C2 -- D C3 -- D style A1 fill:#e8f5e9 style C2 fill:#fff3e0 style C3 fill:#e3f2fdBenchmark 的工程体系包含三层设计层定义变量、工作负载和指标、执行层环境隔离、预热和稳态测量、分析层统计显著性和变异系数。变异系数CV是判断结果可信度的关键指标——CV 10% 说明测试不稳定结论不可信。三、代码实现与分析3.1 Benchmark 框架核心from __future__ import annotations import time import statistics import numpy as np from dataclasses import dataclass, field from typing import Callable, Any from enum import Enum class WorkloadType(Enum): POINT_READ point_read # 点查 RANGE_SCAN range_scan # 范围扫描 POINT_WRITE point_write # 单行写 BULK_WRITE bulk_write # 批量写 MIXED mixed # 混合读写 dataclass class BenchmarkConfig: Benchmark 配置 name: str workload: WorkloadType duration_seconds: int 60 warmup_seconds: int 10 iterations: int 5 # 重复次数 concurrency: int 1 data_size: int 10_000_000 # 数据量 read_ratio: float 0.8 # 读写比 key_distribution: str uniform # uniform / zipfian / latest value_size: int 256 # 值大小字节 # 环境控制 drop_caches: bool True # 每次迭代前清除 OS 缓存 cpu_affinity: list[int] | None None # CPU 绑核 dataclass class LatencyHistogram: 延迟直方图 values: list[float] field(default_factorylist) def record(self, latency_ms: float) - None: self.values.append(latency_ms) property def count(self) - int: return len(self.values) property def mean(self) - float: return statistics.mean(self.values) if self.values else 0 property def median(self) - float: return statistics.median(self.values) if self.values else 0 property def p90(self) - float: return np.percentile(self.values, 90) if self.values else 0 property def p99(self) - float: return np.percentile(self.values, 99) if self.values else 0 property def p999(self) - float: return np.percentile(self.values, 99.9) if self.values else 0 property def cv(self) - float: 变异系数衡量数据离散程度 if not self.values or self.mean 0: return float(inf) return statistics.stdev(self.values) / self.mean dataclass class BenchmarkResult: 单次 Benchmark 结果 config_name: str iteration: int histogram: LatencyHistogram throughput_ops: float # ops/s duration_seconds: float timestamp: float field(default_factorytime.time) class StorageBenchmark: 存储引擎 Benchmark 框架 def run( self, config: BenchmarkConfig, operation: Callable[[Any], float], setup: Callable[[], None] | None None, teardown: Callable[[], None] | None None, ) - list[BenchmarkResult]: 执行 Benchmark results [] for iteration in range(config.iterations): # 环境准备 if setup: setup() if config.drop_caches: self._drop_os_caches() # 预热阶段 end_warmup time.time() config.warmup_seconds while time.time() end_warmup: operation(None) # 正式测量 histogram LatencyHistogram() ops_count 0 start_time time.time() end_time start_time config.duration_seconds while time.time() end_time: latency operation(None) histogram.record(latency) ops_count 1 actual_duration time.time() - start_time results.append(BenchmarkResult( config_nameconfig.name, iterationiteration, histogramhistogram, throughput_opsops_count / actual_duration, duration_secondsactual_duration, )) if teardown: teardown() return results staticmethod def _drop_os_caches(): 清除 OS Page Cache需要 root 权限 try: with open(/proc/sys/vm/drop_caches, w) as f: f.write(3\n) except (PermissionError, FileNotFoundError): pass # 非 Linux 或无权限跳过3.2 统计显著性分析from scipy import stats dataclass class ComparisonResult: 两组 Benchmark 的对比结果 name_a: str name_b: str metric: str mean_a: float mean_b: float improvement: float # (b - a) / a * 100% p_value: float is_significant: bool # p 0.05 cv_a: float cv_b: float is_reliable: bool # 两组 CV 都 5% class BenchmarkComparator: Benchmark 结果对比器 def compare_throughput( self, results_a: list[BenchmarkResult], results_b: list[BenchmarkResult], alpha: float 0.05, ) - ComparisonResult: 对比两组 Benchmark 的吞吐量 throughputs_a [r.throughput_ops for r in results_a] throughputs_b [r.throughput_ops for r in results_b] mean_a statistics.mean(throughputs_a) mean_b statistics.mean(throughputs_b) cv_a statistics.stdev(throughputs_a) / mean_a if mean_a else float(inf) cv_b statistics.stdev(throughputs_b) / mean_b if mean_b else float(inf) # Mann-Whitney U 检验不假设正态分布 if len(throughputs_a) 3 and len(throughputs_b) 3: _, p_value stats.mannwhitneyu( throughputs_a, throughputs_b, alternativetwo-sided ) else: p_value 1.0 # 样本不足无法检验 improvement (mean_b - mean_a) / mean_a * 100 if mean_a else 0 return ComparisonResult( name_aresults_a[0].config_name, name_bresults_b[0].config_name, metricthroughput_ops, mean_amean_a, mean_bmean_b, improvementimprovement, p_valuep_value, is_significantp_value alpha, cv_acv_a, cv_bcv_b, is_reliablecv_a 0.05 and cv_b 0.05, ) def compare_latency( self, results_a: list[BenchmarkResult], results_b: list[BenchmarkResult], percentile: int 99, alpha: float 0.05, ) - ComparisonResult: 对比两组 Benchmark 的尾延迟 def get_percentile(results: list[BenchmarkResult], p: int) - list[float]: return [ float(np.percentile(r.histogram.values, p)) for r in results if r.histogram.values ] latencies_a get_percentile(results_a, percentile) latencies_b get_percentile(results_b, percentile) mean_a statistics.mean(latencies_a) if latencies_a else 0 mean_b statistics.mean(latencies_b) if latencies_b else 0 cv_a statistics.stdev(latencies_a) / mean_a if mean_a and len(latencies_a) 1 else float(inf) cv_b statistics.stdev(latencies_b) / mean_b if mean_b and len(latencies_b) 1 else float(inf) if len(latencies_a) 3 and len(latencies_b) 3: _, p_value stats.mannwhitneyu( latencies_a, latencies_b, alternativetwo-sided ) else: p_value 1.0 improvement (mean_b - mean_a) / mean_a * 100 if mean_a else 0 return ComparisonResult( name_aresults_a[0].config_name, name_bresults_b[0].config_name, metricfp{percentile}_latency_ms, mean_amean_a, mean_bmean_b, improvementimprovement, p_valuep_value, is_significantp_value alpha, cv_acv_a, cv_bcv_b, is_reliablecv_a 0.05 and cv_b 0.05, )3.3 Benchmark 报告生成def generate_benchmark_report( results: list[BenchmarkResult], comparisons: list[ComparisonResult] | None None, ) - str: 生成 Benchmark 报告 lines [] lines.append( * 70) lines.append(存储引擎 Benchmark 报告) lines.append( * 70) for result in results: h result.histogram lines.append(f\n--- {result.config_name} (迭代 {result.iteration 1}) ---) lines.append(f 吞吐量: {result.throughput_ops:.0f} ops/s) lines.append(f 延迟 - 均值: {h.mean:.2f}ms, 中位数: {h.median:.2f}ms) lines.append(f 延迟 - P90: {h.p90:.2f}ms, P99: {h.p99:.2f}ms, P99.9: {h.p999:.2f}ms) lines.append(f 变异系数: {h.cv:.1%}) if h.cv 0.10: lines.append( ⚠ 变异系数 10%结果不稳定建议增加迭代次数) if comparisons: lines.append(\n * 70) lines.append(对比分析) lines.append( * 70) for comp in comparisons: lines.append(f\n{comp.name_a} vs {comp.name_b} ({comp.metric}):) lines.append(f {comp.name_a}: {comp.mean_a:.2f}) lines.append(f {comp.name_b}: {comp.mean_b:.2f}) lines.append(f 提升: {comp.improvement:.1f}%) lines.append(f p-value: {comp.p_value:.4f}) lines.append(f 统计显著: {是 if comp.is_significant else 否}) lines.append(f 结果可靠: {是 if comp.is_reliable else 否CV 过高}) if not comp.is_reliable: lines.append( ⚠ 变异系数过高结论可能不可靠) return \n.join(lines)四、Benchmark 的边界与架构权衡OS 缓存的干扰Linux 的 Page Cache 会缓存读写数据第一次读磁盘和第二次读缓存的结果可能差 10 倍。控制方法每次迭代前echo 3 /proc/sys/vm/drop_caches清除缓存。但清除缓存会影响其他进程生产环境不能随意操作。建议在独立测试环境执行 Benchmark。预热时间的确定存储引擎的 LSM-Tree 需要 MemTable 刷盘、Compaction 触发后才进入稳态。预热时间取决于写入速度和 Compaction 阈值。经验值预热时间至少是 MemTable 刷盘周期的 2-3 倍。如果不确定观察延迟曲线——当延迟不再单调下降时说明进入稳态。并发度的选择单线程 Benchmark 测的是引擎的内部开销锁、序列化等多线程 Benchmark 测的是并发扩展性。两者结论可能不同——单线程快的引擎可能因锁竞争在多线程下反而慢。建议同时测 1/4/16/64 线程绘制扩展性曲线。尾延迟的测量精度P99 和 P99.9 的测量需要足够大的样本量。如果每次迭代只有 1000 次操作P99 只有 10 个样本点统计意义不大。建议每次迭代至少 100 万次操作确保 P99.9 有 1000 个样本点。五、总结存储引擎 Benchmark 的核心是可复现性和统计显著性。本文的关键实践为用 BenchmarkConfig 明确所有测试变量、用预热 多次迭代保证稳态测量、用变异系数CV 5%判断结果可信度、用 Mann-Whitney U 检验判断差异的统计显著性。Benchmark 数字本身没有意义只有在明确条件、可复现、有统计显著性的前提下才有参考价值。不控制变量的 Benchmark 比没有 Benchmark 更危险——它会给你错误的信心。
存储引擎性能 Benchmark:从可复现测试到统计显著性分析的工程方法
发布时间:2026/6/17 16:44:03
存储引擎性能 Benchmark从可复现测试到统计显著性分析的工程方法一、Benchmark 的结果不可复现比没有 Benchmark 更危险我的 SSD 顺序写能到 2 GB/s——这个数字在什么条件下测的单线程还是多线程直写还是缓冲写数据块大小 4K 还是 1M是否预热是否清除了 OS Page Cache如果这些条件不明确Benchmark 数字就是空中楼阁。存储引擎的 Benchmark 比 SSD 更复杂涉及压缩算法、缓存策略、合并策略、并发控制等多个变量。一个不控制变量的 Benchmark结果可能每次都不同甚至得出相反结论。本文要解决的问题是如何设计可复现、可对比、有统计显著性的存储引擎 Benchmark。二、Benchmark 工程体系与统计方法flowchart TB A[Benchmark 设计] -- A1[变量定义br/固定/可控/观测] A -- A2[工作负载建模br/读写比/数据特征/访问模式] A -- A3[指标定义br/延迟/吞吐/IOPS/尾延迟] A1 -- B[测试执行] A2 -- B A3 -- B B -- B1[环境隔离br/CPU 绑核/NUMA/磁盘独占] B -- B2[预热阶段br/填满缓存/触发合并] B -- B3[稳态测量br/多次迭代取统计值] B3 -- C[统计分析] C -- C1[描述统计br/均值/中位数/P99] C -- C2[变异系数br/CV 5% 才可信] C -- C3[显著性检验br/t-test / Mann-Whitney U] C1 -- D[报告生成] C2 -- D C3 -- D style A1 fill:#e8f5e9 style C2 fill:#fff3e0 style C3 fill:#e3f2fdBenchmark 的工程体系包含三层设计层定义变量、工作负载和指标、执行层环境隔离、预热和稳态测量、分析层统计显著性和变异系数。变异系数CV是判断结果可信度的关键指标——CV 10% 说明测试不稳定结论不可信。三、代码实现与分析3.1 Benchmark 框架核心from __future__ import annotations import time import statistics import numpy as np from dataclasses import dataclass, field from typing import Callable, Any from enum import Enum class WorkloadType(Enum): POINT_READ point_read # 点查 RANGE_SCAN range_scan # 范围扫描 POINT_WRITE point_write # 单行写 BULK_WRITE bulk_write # 批量写 MIXED mixed # 混合读写 dataclass class BenchmarkConfig: Benchmark 配置 name: str workload: WorkloadType duration_seconds: int 60 warmup_seconds: int 10 iterations: int 5 # 重复次数 concurrency: int 1 data_size: int 10_000_000 # 数据量 read_ratio: float 0.8 # 读写比 key_distribution: str uniform # uniform / zipfian / latest value_size: int 256 # 值大小字节 # 环境控制 drop_caches: bool True # 每次迭代前清除 OS 缓存 cpu_affinity: list[int] | None None # CPU 绑核 dataclass class LatencyHistogram: 延迟直方图 values: list[float] field(default_factorylist) def record(self, latency_ms: float) - None: self.values.append(latency_ms) property def count(self) - int: return len(self.values) property def mean(self) - float: return statistics.mean(self.values) if self.values else 0 property def median(self) - float: return statistics.median(self.values) if self.values else 0 property def p90(self) - float: return np.percentile(self.values, 90) if self.values else 0 property def p99(self) - float: return np.percentile(self.values, 99) if self.values else 0 property def p999(self) - float: return np.percentile(self.values, 99.9) if self.values else 0 property def cv(self) - float: 变异系数衡量数据离散程度 if not self.values or self.mean 0: return float(inf) return statistics.stdev(self.values) / self.mean dataclass class BenchmarkResult: 单次 Benchmark 结果 config_name: str iteration: int histogram: LatencyHistogram throughput_ops: float # ops/s duration_seconds: float timestamp: float field(default_factorytime.time) class StorageBenchmark: 存储引擎 Benchmark 框架 def run( self, config: BenchmarkConfig, operation: Callable[[Any], float], setup: Callable[[], None] | None None, teardown: Callable[[], None] | None None, ) - list[BenchmarkResult]: 执行 Benchmark results [] for iteration in range(config.iterations): # 环境准备 if setup: setup() if config.drop_caches: self._drop_os_caches() # 预热阶段 end_warmup time.time() config.warmup_seconds while time.time() end_warmup: operation(None) # 正式测量 histogram LatencyHistogram() ops_count 0 start_time time.time() end_time start_time config.duration_seconds while time.time() end_time: latency operation(None) histogram.record(latency) ops_count 1 actual_duration time.time() - start_time results.append(BenchmarkResult( config_nameconfig.name, iterationiteration, histogramhistogram, throughput_opsops_count / actual_duration, duration_secondsactual_duration, )) if teardown: teardown() return results staticmethod def _drop_os_caches(): 清除 OS Page Cache需要 root 权限 try: with open(/proc/sys/vm/drop_caches, w) as f: f.write(3\n) except (PermissionError, FileNotFoundError): pass # 非 Linux 或无权限跳过3.2 统计显著性分析from scipy import stats dataclass class ComparisonResult: 两组 Benchmark 的对比结果 name_a: str name_b: str metric: str mean_a: float mean_b: float improvement: float # (b - a) / a * 100% p_value: float is_significant: bool # p 0.05 cv_a: float cv_b: float is_reliable: bool # 两组 CV 都 5% class BenchmarkComparator: Benchmark 结果对比器 def compare_throughput( self, results_a: list[BenchmarkResult], results_b: list[BenchmarkResult], alpha: float 0.05, ) - ComparisonResult: 对比两组 Benchmark 的吞吐量 throughputs_a [r.throughput_ops for r in results_a] throughputs_b [r.throughput_ops for r in results_b] mean_a statistics.mean(throughputs_a) mean_b statistics.mean(throughputs_b) cv_a statistics.stdev(throughputs_a) / mean_a if mean_a else float(inf) cv_b statistics.stdev(throughputs_b) / mean_b if mean_b else float(inf) # Mann-Whitney U 检验不假设正态分布 if len(throughputs_a) 3 and len(throughputs_b) 3: _, p_value stats.mannwhitneyu( throughputs_a, throughputs_b, alternativetwo-sided ) else: p_value 1.0 # 样本不足无法检验 improvement (mean_b - mean_a) / mean_a * 100 if mean_a else 0 return ComparisonResult( name_aresults_a[0].config_name, name_bresults_b[0].config_name, metricthroughput_ops, mean_amean_a, mean_bmean_b, improvementimprovement, p_valuep_value, is_significantp_value alpha, cv_acv_a, cv_bcv_b, is_reliablecv_a 0.05 and cv_b 0.05, ) def compare_latency( self, results_a: list[BenchmarkResult], results_b: list[BenchmarkResult], percentile: int 99, alpha: float 0.05, ) - ComparisonResult: 对比两组 Benchmark 的尾延迟 def get_percentile(results: list[BenchmarkResult], p: int) - list[float]: return [ float(np.percentile(r.histogram.values, p)) for r in results if r.histogram.values ] latencies_a get_percentile(results_a, percentile) latencies_b get_percentile(results_b, percentile) mean_a statistics.mean(latencies_a) if latencies_a else 0 mean_b statistics.mean(latencies_b) if latencies_b else 0 cv_a statistics.stdev(latencies_a) / mean_a if mean_a and len(latencies_a) 1 else float(inf) cv_b statistics.stdev(latencies_b) / mean_b if mean_b and len(latencies_b) 1 else float(inf) if len(latencies_a) 3 and len(latencies_b) 3: _, p_value stats.mannwhitneyu( latencies_a, latencies_b, alternativetwo-sided ) else: p_value 1.0 improvement (mean_b - mean_a) / mean_a * 100 if mean_a else 0 return ComparisonResult( name_aresults_a[0].config_name, name_bresults_b[0].config_name, metricfp{percentile}_latency_ms, mean_amean_a, mean_bmean_b, improvementimprovement, p_valuep_value, is_significantp_value alpha, cv_acv_a, cv_bcv_b, is_reliablecv_a 0.05 and cv_b 0.05, )3.3 Benchmark 报告生成def generate_benchmark_report( results: list[BenchmarkResult], comparisons: list[ComparisonResult] | None None, ) - str: 生成 Benchmark 报告 lines [] lines.append( * 70) lines.append(存储引擎 Benchmark 报告) lines.append( * 70) for result in results: h result.histogram lines.append(f\n--- {result.config_name} (迭代 {result.iteration 1}) ---) lines.append(f 吞吐量: {result.throughput_ops:.0f} ops/s) lines.append(f 延迟 - 均值: {h.mean:.2f}ms, 中位数: {h.median:.2f}ms) lines.append(f 延迟 - P90: {h.p90:.2f}ms, P99: {h.p99:.2f}ms, P99.9: {h.p999:.2f}ms) lines.append(f 变异系数: {h.cv:.1%}) if h.cv 0.10: lines.append( ⚠ 变异系数 10%结果不稳定建议增加迭代次数) if comparisons: lines.append(\n * 70) lines.append(对比分析) lines.append( * 70) for comp in comparisons: lines.append(f\n{comp.name_a} vs {comp.name_b} ({comp.metric}):) lines.append(f {comp.name_a}: {comp.mean_a:.2f}) lines.append(f {comp.name_b}: {comp.mean_b:.2f}) lines.append(f 提升: {comp.improvement:.1f}%) lines.append(f p-value: {comp.p_value:.4f}) lines.append(f 统计显著: {是 if comp.is_significant else 否}) lines.append(f 结果可靠: {是 if comp.is_reliable else 否CV 过高}) if not comp.is_reliable: lines.append( ⚠ 变异系数过高结论可能不可靠) return \n.join(lines)四、Benchmark 的边界与架构权衡OS 缓存的干扰Linux 的 Page Cache 会缓存读写数据第一次读磁盘和第二次读缓存的结果可能差 10 倍。控制方法每次迭代前echo 3 /proc/sys/vm/drop_caches清除缓存。但清除缓存会影响其他进程生产环境不能随意操作。建议在独立测试环境执行 Benchmark。预热时间的确定存储引擎的 LSM-Tree 需要 MemTable 刷盘、Compaction 触发后才进入稳态。预热时间取决于写入速度和 Compaction 阈值。经验值预热时间至少是 MemTable 刷盘周期的 2-3 倍。如果不确定观察延迟曲线——当延迟不再单调下降时说明进入稳态。并发度的选择单线程 Benchmark 测的是引擎的内部开销锁、序列化等多线程 Benchmark 测的是并发扩展性。两者结论可能不同——单线程快的引擎可能因锁竞争在多线程下反而慢。建议同时测 1/4/16/64 线程绘制扩展性曲线。尾延迟的测量精度P99 和 P99.9 的测量需要足够大的样本量。如果每次迭代只有 1000 次操作P99 只有 10 个样本点统计意义不大。建议每次迭代至少 100 万次操作确保 P99.9 有 1000 个样本点。五、总结存储引擎 Benchmark 的核心是可复现性和统计显著性。本文的关键实践为用 BenchmarkConfig 明确所有测试变量、用预热 多次迭代保证稳态测量、用变异系数CV 5%判断结果可信度、用 Mann-Whitney U 检验判断差异的统计显著性。Benchmark 数字本身没有意义只有在明确条件、可复现、有统计显著性的前提下才有参考价值。不控制变量的 Benchmark 比没有 Benchmark 更危险——它会给你错误的信心。