VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 写入吞吐/查询延迟/内存占用的数学模型

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 写入吞吐/查询延迟/内存占用的数学模型

当你需要评估 VictoriaMetrics 是否能满足业务需求时,

      • 如何量化其性能?
      • 写入吞吐量能达到多少?
      • 查询延迟的 P99 是多少?
      • 内存占用和时间序列数量是什么关系?

理解了性能模型,你才能做出正确的容量规划。

读完本篇,你应该能回答:VictoriaMetrics 的写入吞吐模型是什么?查询延迟如何计算?内存占用和时间序列数量的关系是什么?硬件配置如何影响性能?

VictoriaMetrics 性能模型 容量规划 写入吞吐 查询延迟 内存模型 v1.146.0

学习重点提示建议先通读全文,再重点回顾标注内容

重点掌握(必须)

  • 写入吞吐模型:CPU 核数与写入速率的关系
  • 查询延迟模型:Part 数量与查询时间的关系
  • 内存占用模型:TSIDCache 37% 策略与内存需求
  • 容量规划公式:实际部署的计算方法

次重点(了解即可)

  • 压缩率的影响因素
  • 网络带宽的影响
  • 磁盘 I/O 的影响

文章目录

一、问题的起点:为什么需要性能模型?

思考记忆提示性能模型是容量规划的基础——没有模型就像盲人摸象

  • 性能模型帮助 预测系统行为
  • 性能模型帮助 容量规划
  • 性能模型帮助 瓶颈定位
  • 面试高频提问:如何评估一个系统的性能上限?

在生产环境中,容量规划是至关重要的工作。如果配置不足,会导致性能下降甚至 OOM;如果配置过剩,会造成资源浪费。

VictoriaMetrics 官方提供了基准测试数据,但理解这些数据背后的原理,才能更好地应用它们。

我理解源码的意思是说

VM 性能模型的核心参数,可以直接从源码里读出来。我们不看任何类比,直接看关键文件中的常量定义,就能理解写入吞吐、查询延迟、内存占用的数学关系:

源码视角一:写入吞吐取决于 CPU 核数和 rawRowsShards 分片数

lib/storage/partition.go 第 46 行和第 484-500 行,会发现:

  • rawRowsShardsPerPartition = cgroup.AvailableCPUs() — 分片数默认等于 CPU 核数,实现并行写入
  • 每个 rawRowsShard 有独立的锁,减少锁竞争
  • 写入吞吐 ≈ CPU核数 × 每核处理能力

源码视角二:Part 合并由 defaultPartsToMerge = 15 控制

lib/storage/partition.go 第 41 行:

const defaultPartsToMerge = 15
if len(rrss.rowssToFlush) >= defaultPartsToMerge {
    // 触发合并
}

当一个分区的 Part 数量达到 15 时,自动触发合并。这就是"Part 数量 10-50 为正常"的底层原因。

源码视角三:缓存大小由 memory.Allowed() 按比例分配

lib/storage/storage.go 第 338-368 行:

func getTSIDCacheSize() int {
    // TSID 缓存 = memory.Allowed() × 37%
    return int(float64(memory.Allowed()) * 0.37)
}
func getMetricNamesStatsCacheSize() int {
    // MetricNamesStats 缓存 = memory.Allowed() × 1%
    return memory.Allowed() / 100
}
func getMetricNameCacheSize() int {
    // MetricName 缓存 = memory.Allowed() × 10%
    return memory.Allowed() / 10
}

三种缓存的比例不同:TSID 37%、MetricName 10%、MetricNamesStats 1%。这就是内存占用模型的底层实现。

源码视角总结:3 个关键常数

  • 1 个分片参数:rawRowsShardsPerPartition = cgroup.AvailableCPUs()
  • 1 个合并参数:defaultPartsToMerge = 15
  • 1 个缓存参数:getTSIDCacheSize() = memory.Allowed() × 37%

避坑提醒(源码视角):

  • 不要以为有单一的 workingsetcacheSize 参数:实际上有多个独立缓存,各自大小由不同函数控制
  • 不要以为 37% 是所有缓存的总比例:37% 只是 TSID 缓存一个,MetricName 是 10%,MetricNamesStats 是 1%
  • 不要以为 -defaultPartsToMerge 存在:实测是 Go 代码中的常量,不是命令行参数

1.1 VictoriaMetrics 官方基准测试

以下是 VictoriaMetrics 官方公布的基准测试数据(Single-Node 模式):

指标基准测试结果测试条件
写入吞吐 ~500K samples/s 8 核 CPU,SSD
查询 P99 延迟 <50ms 100 万 series,随机查询
内存占用 Prometheus 的 1/7 相同数据量,相同查询模式
压缩率 10x-50x 取决于数据特征

注意

基准测试数据是在特定硬件和数据集条件下得出的,实际生产环境可能有所不同。以下公式是基于基准测试数据推导的经验公式,用于容量规划参考。

二、写入吞吐模型:CPU 核数与写入速率

思考记忆提示写入吞吐主要由 CPU 决定——Goroutine 池设计让写入近乎线性扩展

  • 写入吞吐与 CPU 核数 近似线性关系
  • rawRowsShards 分片实现了并行写入
  • 面试高频提问:VM 的写入性能如何随 CPU 核数扩展?

2.1 写入吞吐公式

VictoriaMetrics 的写入吞吐可以表示为:

写入吞吐公式:

T_write = N_cpu × C_write × E_parallel × F_protocol

其中:
  T_write  - 写入吞吐(samples/s)
  N_cpu    - CPU 核数
  C_write  - 每核每秒处理样本数(常数,~50K samples/s per core)
  E_parallel - 并行效率因子(0.7-0.9,经验值)
  F_protocol - 协议解析效率(0.8-0.95,取决于协议复杂度)

简化公式:
  T_write ≈ N_cpu × 50K × 0.8
           ≈ N_cpu × 40K samples/s

示例:
  4 核 → ~160K samples/s
  8 核 → ~320K samples/s
  16 核 → ~640K samples/s
  32 核 → ~1.28M samples/s

2.2 影响写入吞吐的因素

以下是影响写入吞吐的关键因素:

因素影响说明
CPU 核数 正相关 更多核数 = 更高吞吐(近乎线性)
磁盘 I/O 瓶颈 SSD 建议;HDD 会限制吞吐
数据复杂度 负相关 标签越多、metric name 越长,解析越慢
网络带宽 瓶颈 10Gbps 网络下,1Gbit 约等于 125MB/s
压缩开销 负相关 NearestDelta + ZSTD 需要额外 CPU

2.3 写入路径的性能分析

写入流程中各步骤的性能开销取决于数据特征和硬件配置,源码在 lib/storage/partition.go 中实现。关键性能参数:

  • rawRowsShards 分片数:等于 CPU 核数(lib/storage/partition.go 第 46 行),减少锁竞争
  • 合并阈值:Part 数量达到 15 时触发合并(第 41 行 defaultPartsToMerge = 15
  • 内存分配:每条写入记录涉及 TSID 查找、索引更新、数据块写入

小贴士性能优化方向

  • 使用 NVMe SSD(加速 indexDB 写入)
  • 增加 CPU 核数(提升并行处理能力)
  • 避免高基数标签(减少索引写入开销)

三、查询延迟模型:Part 数量与扫描时间

思考记忆提示查询延迟主要由 Part 数量决定——合并策略是控制延迟的关键

  • 查询延迟与 Part 数量 近似线性关系
  • blockCache 命中率显著影响缓存命中延迟
  • 面试高频提问:为什么 VM 的查询延迟比 Prometheus 低?

3.1 查询延迟公式

查询延迟公式:

T_query = T_index + T_scan + T_merge

其中:
  T_index  - 索引查询时间
            = f(series_count) × (1 - cache_hit_rate)
            ≈ N_series × 100ns × (1 - cache_hit_rate)

  T_scan   - 数据扫描时间
            = N_parts × T_part_scan
            = N_parts × (data_size_per_part / disk_throughput)

  T_merge  - 结果归并时间
            = O(N_parts × log(N_parts))

简化公式(无缓存命中):
  T_query ≈ N_parts × 1ms + N_series × 100ns

示例:
  10 个 Part → ~10-20ms
  50 个 Part → ~50-100ms
  100 个 Part → ~100-200ms

3.2 影响查询延迟的因素

因素影响说明
Part 数量 正相关 更多 Part = 更高扫描开销
blockCache 命中率 负相关 命中缓存 → 避免磁盘 I/O
查询范围 正相关 更大时间范围 → 更多 Part
标签过滤器 正相关 正则匹配 → 额外计算
磁盘类型 瓶颈 NVMe SSD >> SATA SSD >> HDD

3.3 Part 数量对查询的影响

┌─────────────────────────────────────────────────────────────────────────┐
│                    Part 数量与查询延迟的关系                               │
│                                                                          │
│  Part 数量    │ 查询延迟(P99)  │ 状态                                 │
│  ──────────────────────────────────────────────────────────────────────  │
│  1-10         │ < 50ms           │ 理想状态                             │
│  10-50        │ 50-200ms         │ 可接受                               │
│  50-100       │ 200ms-1s         │ 警告,需要合并                       │
│  > 100        │ > 1s             │ 严重,需要立即合并                    │
│                                                                          │
│  控制 Part 数量的方法:                                                  │
│  1. defaultPartsToMerge = 15 是 Go 代码中的常量,不是命令行参数        │
│  2. 监控 vm_parts{type="..."} 指标(app/vmstorage/main.go 第 427-431 行) │
│  3. /internal/force_merge 手动触发合并                                    │
│  4. 调整 retentionPeriod 清理旧数据                                       │
└─────────────────────────────────────────────────────────────────────────┘

设计精髓

VM 查询性能优于 Prometheus 的核心原因:

  • blockCache 按需缓存:只缓存命中的数据块,避免无关数据
  • MergeSet 无分层:查询只需扫描相关 Part,不需要跨层合并
  • 倒排索引:标签查询直接定位 series,不需要扫描全量数据
  • PromQL 优化:并行执行、rollup 缓存、streaming 算子

四、内存占用模型:时间序列数量与 RAM

思考记忆提示内存占用是 VM 的核心优势——按需缓存策略减少内存占用

  • 内存占用与 series 数量 近似线性关系
  • 缓存按比例分配:TSID 缓存 37%、MetricName 缓存 10%、MetricNamesStats 缓存 1%
  • 面试高频提问:VM 的内存占用模型是怎样的?

4.1 内存占用公式

内存占用公式(源码实测):

memory.Allowed() = 系统内存 × -memory.allowedPercent(默认 60%)
  源码:lib/memory/memory.go 第 14 行(default 60)

VM 缓存分配(lib/storage/storage.go 第 338-370 行):
  TSID 缓存              = memory.Allowed() × 37%
  MetricID 缓存            = memory.Allowed() / 16 ≈ 6.25%
  MetricName 缓存         = memory.Allowed() × 10%
  MetricNamesStats 缓存    = memory.Allowed() × 1%
  ─────────────────────────────────────────────────
  合计 workingsetcache    ≈ memory.Allowed() × 54%

注意:这些是缓存最大分配上限,实际占用取决于数据量和访问模式。
公式中的系数基于源码常量推导,不是生产环境的精确测量值。

4.2 影响内存占用的因素

因素影响说明
series 数量 正相关 series 越多,TSID 缓存需求越大。实际内存取决于 memory.Allowed() 配置
标签数量 正相关 更多标签 = 更大 metric name = 更大索引
查询模式 影响 热点查询 → 高缓存命中率 → 低内存需求
memory.Allowed() 控制 限制 VM 可用内存

4.3 按需缓存的原理

┌─────────────────────────────────────────────────────────────────────────┐
│                    VM 内存模型:按需缓存                                        │
│                                                                          │
│  memory.Allowed() 配置(lib/memory/memory.go 第 14 行):                   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ -memory.allowedPercent = 60%(默认)                              │   │
│  │ -memory.allowedBytes 可覆盖此配置                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  VM 缓存分配(lib/storage/storage.go 第 338-370 行):                    │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ TSID 缓存              = memory.Allowed() × 37%                │   │
│  │ MetricID 缓存           = memory.Allowed() / 16 ≈ 6.25%        │   │
│  │ MetricName 缓存         = memory.Allowed() × 10%               │   │
│  │ MetricNamesStats 缓存    = memory.Allowed() × 1%               │   │
│  │ ─────────────────────────────────────────────────────          │   │
│  │ 合计                 ≈ memory.Allowed() × 54%                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  核心优势:冷数据从磁盘读取,按需缓存,无需全量 mmap                         │
└─────────────────────────────────────────────────────────────────────────┘

五、压缩与存储:数据压缩对容量的影响

思考记忆提示压缩是 VM 高效存储的关键——NearestDelta + ZSTD 组合拳

  • 压缩率与数据特征强相关
  • NearestDelta 对时序数据特别有效
  • 面试高频提问:VM 的压缩率是多少?

5.1 压缩率公式

压缩率公式:

CR = CR_timestamp × CR_value × CR_index

其中:
  CR           - 总压缩率
  CR_timestamp - 时间戳压缩率(NearestDelta 算法)
               ≈ 10x-30x(取决于采样间隔稳定性)
  CR_value     - 值压缩率(NearestDelta + ZSTD)
               ≈ 2x-10x(取决于值的变化幅度)
  CR_index     - 索引压缩率(倒排索引 + commonPrefix)
               ≈ 2x-5x(取决于标签基数)

典型压缩率:
  - Counter 类型(平滑变化):20x-50x
  - Gauge 类型(波动较大):10x-20x
  - Histogram 类型(稀疏):5x-10x

原始数据 vs 压缩后:
  1 个样本(16 bytes)→ 压缩后 ~0.5-2 bytes
  1 天数据(86400 秒采样)→ 约 40KB - 170KB

5.2 存储空间计算

存储空间:使用官方推荐的实测法(docs.victoriametrics.com):
  1. 运行 1 天测试,测量 storageDataPath 目录大小
  2. 按 retention 天数外推:预计总空间 = 单日增长 × retention 天数
  示例:测试 1 天后占用 10GB → retention=100 天 → 预计 10GB × 100 = 1TB

注意:以下系数为估算,未在源码中精确验证:
  - 样本存储:取决于压缩率(NearestDelta + ZSTD)
  - 索引存储:取决于标签数量和基数
  - 建议通过实测获取精确数字

小贴士磁盘空间规划建议

生产环境磁盘空间规划:

  • 运行 1 天测试,测量实际存储增长
  • 按 retention 天数外推(见官方公式)
  • 预留 20% 空余空间(减少磁盘碎片)

六、容量规划:实际部署的计算方法

思考记忆提示容量规划是性能和成本之间的权衡——理解模型才能做出正确决策

  • 容量规划需要考虑三个维度:吞吐、延迟、存储
  • 瓶颈往往是木桶效应:最弱的环节决定整体
  • 面试高频提问:如何为 1000 万 series 规划 VM 集群?

6.1 容量规划模板

┌─────────────────────────────────────────────────────────────────────────┐
│                    容量规划检查清单                                        │
│                                                                          │
│  1. 确定业务需求                                                         │
│     □ series 数量:当前 N,当前增长速率                                    │
│     □ 写入吞吐:samples/s 或 samples/day                                 │
│     □ retention:90 天 / 180 天 / 1 年                                 │
│     □ 查询 QPS:查询频率和复杂度                                          │
│                                                                          │
│  2. 计算资源需求                                                         │
│     □ 内存 ≈ memory.Allowed() × 60%(默认值,可通过 -memory.allowedPercent 调整)       │
│     □ CPU = 吞吐 / 40K samples per core                                  │
│     □ 磁盘 = 存储需求 × 2.0 安全系数                                    │
│                                                                          │
│  3. 选择部署模式                                                         │
│     □ Single-Node:< 500 万 series                                     │
│     □ Cluster 模式:> 500 万 series 或高可用需求                          │
│                                                                          │
│  4. 验证配置                                                             │
│     □ 吞吐测试:能否满足写入需求                                          │
│     □ 延迟测试:P99 延迟是否可接受                                       │
│     □ 内存测试:OOM 是否发生                                             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

6.2 实际案例计算

案例:1000 万 series,90 天 retention,500K samples/s 写入

1. 内存计算:
   memory.Allowed() = 机器总内存 × 60%(默认 -memory.allowedPercent = 60)
   缓存总大小 ≈ memory.Allowed() × 54%(TSID 37% + MetricID 6.25% + MetricName 10% + MetricNamesStats 1%)
   10M series 时,建议 memory.Allowed() ≥ 16GB

2. CPU 计算:
   N_cpu = 500K / 40K
         = 12.5 cores
   建议:16 核 CPU

3. 磁盘计算:
   样本数 = 500K/s × 86400s × 90 days
          = 3.89 trillion samples
   
   存储(压缩后)= 3.89T × 1 byte
                = 3.89 TB
   
   安全系数 = 3.89TB × 2.0 = 7.78TB
   建议:8TB - 10TB NVMe SSD

4. 部署模式:
   1000 万 series > 500 万 series → Cluster 模式
   
   Cluster 配置建议:
   - 3 个 vmstorage 节点,每个 32GB RAM
   - 2 个 vminsert 节点
   - 2 个 vmselect 节点

注意

以上计算是简化模型,实际需求可能因数据特征、查询模式、硬件配置等因素而有所不同。建议在生产部署前进行实际压测验证。

七、FAQ:常见疑问

思考记忆提示FAQ 是全篇的"临考前速背"模块,20 组覆盖全链路

  • Q1-Q5 围绕写入吞吐:CPU 核数与写入速率的关系
  • Q6-Q10 围绕查询延迟:Part 数量与延迟的关系
  • Q11-Q15 围绕内存占用:series 数量与 RAM 的关系
  • Q16-Q20 围绕容量规划:实际部署的计算方法

Q1. VM 的写入吞吐与 CPU 核数是线性关系吗?

近似线性,但不是完美的线性关系。由于锁竞争、内存带宽、缓存一致性等开销,实际吞吐约为线性值的 70-90%。

Q2. 为什么我的 VM 写入吞吐达不到官方基准?

可能的原因:磁盘 I/O 瓶颈、网络瓶颈、数据复杂度高、配置不当。建议使用 pprof 分析 CPU 和内存使用,找出瓶颈。

Q3. Part 数量多少算正常?

正常情况下,每个分区应有 10-50 个 Part。超过 100 个 Part 会显著影响查询延迟,需要检查合并是否正常。源码在 lib/storage/partition.go 第 41 行,defaultPartsToMerge = 15 是触发合并的阈值。

Q4. 如何监控 Part 数量?

通过 /metrics 端点的 vm_parts{type="..."} 指标监控。源码在 app/vmstorage/main.go 第 427-431 行,可看到按类型分的 Part 数量:storage/inmemory、storage/small、storage/big、indexdb/inmemory、indexdb/file。

Q5. 什么是 active_merges?

active_merges 表示当前正在进行的合并任务数量。源码在 app/vmstorage/main.go 第 381-385 行,指标名为 vm_active_merges{type="..."},按 storage 和 indexdb 分别统计。

Q6. TSIDCache 命中率多少算正常?

正常情况下命中率应高于 90%。如果命中率低于 80%,可能需要增加内存或调整缓存大小。

Q7. blockCache 命中率多少算正常?

正常情况下命中率应高于 80%。命中率受查询模式和数据集大小影响。

Q8. VM 的压缩率是多少?

典型压缩率 10x-50x,取决于数据特征。Counter 类型(平滑变化)压缩率高,Histogram 类型压缩率低。

Q9. 磁盘空间如何计算?

公式:存储 = 样本数 × 1 byte × 2.0(安全系数)。示例:90 天 retention,500K samples/s ≈ 6.5TB 原始数据 → 约 8TB 磁盘空间。

Q10. retention 对存储空间的影响是什么?

存储空间与 retention 成正比。90 天 = 30 天 × 3,存储空间也约为 3 倍。

Q11. memory.Allowed() 如何设置?

通过 -memory.allowedPercent-memory.allowedBytes 参数设置(源码在 lib/memory/memory.go 第 14 行,默认 60%)。

Q12. 内存不足会发生什么?

内存不足会导致 OOM 或性能急剧下降。建议监控 process_resident_memory_bytes 指标(来自 vendor/github.com/VictoriaMetrics/metrics/process_metrics_linux.go),以及 vm_parts{...} 指标观察存储状态。

Q13. 如何调整缓存大小?

VictoriaMetrics 使用多个独立缓存,各自有不同的命令行参数。源码在 app/vmstorage/main.go 第 73-85 行,包括:-storage.cacheSizeStorageTSID(TSID 缓存)、-storage.cacheSizeStorageMetricName(MetricName 缓存)等。默认大小由 lib/storage/storage.go 中的 getTSIDCacheSize() 等函数按 memory.Allowed() 比例计算。

Q14. 为什么 VM 比 Prometheus 省内存?

因为 VM 使用按需缓存策略,不是全量 mmap。源码在 lib/storage/storage.go 第 338-370 行,四种缓存按 memory.Allowed() 分配:TSID 缓存 37%、MetricID 缓存 6.25%、MetricName 缓存 10%、MetricNamesStats 缓存 1%。冷数据从磁盘读取,按需缓存,避免了 Prometheus 全量 mmap 带来的内存浪费。

Q15. 什么场景下 VM 内存占用会很高?

高基数标签、全量查询、缓存未命中等场景。建议避免高基数标签,控制查询范围。具体缓存配置参数见 app/vmstorage/main.go 第 73-85 行。

Q16. Single-Node 模式支持多少 series?

官方建议 < 500 万 series,理论上限取决于可用内存。每百万 series 约需 500MB 内存。

Q17. Cluster 模式的优势是什么?

支持水平扩展、高可用、租户隔离。适合 > 500 万 series 或需要高可用的场景。

Q18. 如何选择 SSD 还是 HDD?

生产环境强烈建议使用 NVMe SSD。HDD 的随机 I/O 性能严重影响查询延迟。

Q19. 网络带宽会影响性能吗?

会,特别是 Cluster 模式下。建议使用 10Gbps 网络,避免网络成为瓶颈。

Q20. 如何进行容量规划?

内存需求取决于缓存大小(由 memory.Allowed() 控制),CPU 与写入吞吐线性相关,磁盘与存储数据量成正比。具体参考:内存由 lib/storage/storage.go 第 338-368 行的缓存分配策略决定;CPU 参考写入吞吐公式(每核约 40K samples/s);磁盘建议预留 2 倍存储空间。

全篇必记总纲

VictoriaMetrics 性能模型的三个核心来源(全部可从源码实测):rawRowsShardsPerPartition = cgroup.AvailableCPUs()(并行写入)、defaultPartsToMerge = 15(合并阈值)、TSIDCache = memory.Allowed() × 37%(缓存分配)。理解这三个源码常量,才能做好容量规划。

八、Roadmap:后续预告

本篇覆盖了 VictoriaMetrics 的性能模型,但还有很多细节尚未展开:

  • #10 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM——理解 VM 在竞品中的定位
  • #11 开源生态:VM 在 CNCF 生态中的位置——理解 VM 的生态位
  • #147 写入吞吐极限:100 万 samples/s 写入调优——性能优化专题
  • #148 查询延迟 P99:50ms 内返回的查询优化策略——性能优化专题
  • #107 内存调优:memory.Allowed() 与 cache 大小规划——排障调优专题

本文参考与源码链接:
  • VictoriaMetrics 官方 FAQ
  • VictoriaMetrics Cluster 文档
  • lib/storage/ · 存储核心
  • lib/mergeset/ · MergeSet 存储

posted @ 2026-06-29 01:18  左扬  阅读(38)  评论(0)    收藏  举报