FAQ:常见疑问

VictoriaMetrics 和 Prometheus 是什么关系?

>VM 是 Prometheus 的长期存储后端,不是替代品。Prometheus 仍然负责抓取(scraping)和告警评估(alerting),数据通过 remote_write 协议写入 VictoriaMetrics。VM 提供了比 Prometheus 原生存储更高的扩展性和更低的资源占用。

Q2. 为什么 VictoriaMetrics 能比 Prometheus 省 7x RAM?

>来自三大设计的协同:MergeSet LSM-less + TSIDCache 37% + blockCache 三层。Prometheus 把所有索引和数据块 mmap 到内存,而 VM 只把热点数据(TSIDCache + blockCache)保留在内存,冷数据放在磁盘按需读取。

Q3. Single-Node 和 Cluster 模式有什么区别?什么时候用哪个?

>生产用 Cluster(支撑 100 万~10 亿 series),Single-Node 仅用于快速验证。Single-Node 是单进程,部署简单;Cluster 模式把 vminsert/vmstorage/vmselect 分离部署,支持水平扩展和多租户。

Q4. VictoriaMetrics 支持多租户吗?

>Cluster 模式原生支持多租户,通过 accountID/projectID 实现隔离。每个租户的数据在存储层完全隔离,适合 SaaS 或内部多业务线监控平台。

Q5. MergeSet 和 LSM Tree 有什么区别?

MergeSet 采用 LSM-less 设计:只合并不分层。LSM Tree(如 RocksDB)有 L0→L1→L2→... 多层合并,开销大;MergeSet 只有 Small Parts 和 Big Parts 两类,查询时只需读最新的 Big Part + 最近的 Small Parts。

Q6. 什么是 commonPrefix 压缩?为什么能节省 30-50% 空间?

**commonPrefix 压缩利用标签值的共同前缀来减少存储。**例如:metric{job="prometheus",instance="localhost:9090"} 和 metric{job="prometheus",instance="localhost:9091"} 共享 metric{job="prometheus",instance="localhost:909"} 前缀,只需存储一次。

Q7. TSIDCache 37% 是怎么来的?

>37% 是经验值,经过大量测试发现这个比例能最大化缓存命中率。TSIDCache 全称 Time Series ID Cache,缓存「metricName→metricID」映射,是写入和查询的关键路径。37% 的比例在 cache hit rate 和 memory usage 之间取得了最优平衡。详见 #170 TSIDCache 37% 计算:内存分配逻辑。

Q8. BloomFilter 在 VictoriaMetrics 中起什么作用?

>BloomFilter 用于基数限制(cardinality limiting),防止高 cardinality 数据打爆 VM。每个小时和每天的 BloomFilter 会记录见过的 metricIDs,如果某小时 metricID 数量超过限制,会拒绝写入。

Q9. VictoriaMetrics 有 WAL(Write-Ahead Log)吗?

**没有!这是 VM 的设计选择。**Prometheus TSDB 使用 WAL 来保证崩溃恢复,但 WAL 会增加写入延迟。VM 通过 InMemoryPart 每 2 秒刷盘的设计(pendingRowsFlushInterval),在保证数据安全的同时避免了 WAL 的开销。

Q10. InMemoryPart 刷盘失败会丢数据吗?

**正常情况下不会丢数据。**InMemoryPart 在内存中维护多个副本,刷盘时会先写临时文件再原子重命名。如果进程崩溃,正在刷盘的数据可能丢失,但这是 Prometheus remote_write 协议允许的"最多一次"语义范围内。

Q11. Cardinality 爆炸的根本原因是什么?

**高 cardinality 来自标签值组合过多。**例如:metric{job="foo",instance="ip-1"}、metric{job="foo",instance="ip-2"}... 每个 instance 都是一个唯一的 time series。如果有 1 万个 instance,就是 1 万个 series。

Q12. 如何诊断 slow query?

**使用 QueryTracer 分析查询各阶段耗时。**VM 的 QueryTracer 会在查询的每个阶段记录耗时:parse → plan → execution → merge。打开 debug 日志可以看到详细的阶段信息。

Q13. blockCache 三层(ibCache/idxbCache/ibSparseCache)分别缓存什么?

>ibCache 缓存数据块(items.bin),idxbCache 缓存索引块(index.bin),ibSparseCache 是 ibCache 的稀疏访问优化。三者合计占用 memory.Allowed() 的 40%,加上 10% 的 metricNameCache、约 3% 的 tagFiltersCache 等共同瓜分 memory.Allowed()。

Q14. 如何规划 VictoriaMetrics 的内存容量?

没有统一的拆分公式,VM 在启动时由 memory.Allowed() 统一核算后分配给多个独立缓存池(TSIDCache / blockCache / metricNameCache / tagFiltersCache / dateMetricIDCache 等)。日常只需重点关注 vm_slow_row_inserts_total、vm_cache_size_bytes、vm_cache_misses_total 这几项指标;命中率下降或缓存溢出时按需调整对应缓存大小或节点规格。100 万 series 起步建议预留 4--8GB 内存做基线,再根据实际命中率与慢插入比例动态扩容。

Q15. retention 设置后数据会立即删除吗?

**不会立即删除,数据会在后台分多层异步清理。**VM 的清理机制分三层:分区级默认每 1 分钟左右巡视一次过期的月份分区目录,Part 级默认每 7 分钟左右巡视一次过期 Part 文件,Block 级则在每次合并时按需裁剪过期样本。磁盘空间释放可能有延迟,这是设计如此,并非异常。

VictoriaMetrics 的 retention 清理采用三层异步清理机制,源码实现非常精妙:

15.1 三层清理机制概览
复制代码
  Retention 清理三层架构
  │
  ├── [Layer 1] 分区级清理 (table.retentionWatcher)
  │   ├── 触发周期: ~1 分钟 (+ 随机抖动)
  │   ├── 职责: 删除整个过期分区目录
  │   └── 源码: lib/storage/table.go:428-467
  │
  ├── [Layer 2] Part 级清理 (partition.stalePartsRemover)
  │   ├── 触发周期: ~7 分钟 (+ 随机抖动)
  │   ├── 职责: 标记过期 Part 并在 Merge 时删除
  │   └── 源码: lib/storage/partition.go:1742-1754
  │
  └── [Layer 3] Block 级裁剪 (mergeBlocks)
      ├── 触发时机: 每次 Merge 时
      ├── 职责: Block 内过期样本的精确裁剪
      └── 源码: lib/storage/merge.go:85-89, 205-216
15.2 分区级清理 (table.retentionWatcher)

lib/storage/table.go:428-467

分区是按月组织的(202306、202307 等),当整个分区超过 retention 期限时,整个分区目录会被删除:

复制代码
  func (tb *table) retentionWatcher() {
    d := timeutil.AddJitterToDuration(time.Minute)
    ticker := time.NewTicker(d)
    for {
        select {
        case 
15.3 Part 级清理 (partition.stalePartsRemover)

lib/storage/partition.go:1742-1754(stalePartsRemover 主体)

Part 是 MergeSet 存储的基本单元(包含 metaindex/index/items/lens 四个文件)。每个 Part 独立检查是否过期:

复制代码
  // stalePartsRemover 每 ~7 分钟运行一次
func (pt *partition) stalePartsRemover() {
    d := timeutil.AddJitterToDuration(7 * time.Minute)
    ticker := time.NewTicker(d)
    for {
        select {
        case 
15.4 Block 内样本级裁剪 (mergeBlocks)

lib/storage/merge.go:85-89 与 205-216

即使 Part 没有完全过期,在每次 Merge 过程中也会裁剪过期的样本:

复制代码
  // mergeBlockStreamsInternal 中
for bsm.NextBlock() {
    b := bsm.Block
    
    // 跳过整个 Block 都在 retention 之外的情况
    retentionDeadline := bsm.getRetentionDeadline(&b.bh)
    if b.bh.MaxTimestamp < retentionDeadline {
        localRowsDeleted += uint64(b.bh.RowsCount)
        continue  // 跳过这个 Block
    }
    
    // 合并时裁剪 Block 内过期的样本
    mergeBlocks(tmpBlock, pendingBlock, b, retentionDeadline, &localRowsDeleted)
}

// mergeBlocks 中调用 skipSamplesOutsideRetention
func skipSamplesOutsideRetention(b *Block, retentionDeadline int64, rowsDeleted *uint64) {
    if b.bh.MinTimestamp >= retentionDeadline {
        return  // 快速路径:Block 内所有样本都在 retention 范围内
    }
    // 遍历时间戳,跳过 < retentionDeadline 的样本
    for nextIdx < len(timestamps) && timestamps[nextIdx] < retentionDeadline {
        nextIdx++
    }
    if n := nextIdx - nextIdxOrig; n > 0 {
        *rowsDeleted += uint64(n)
        b.nextIdx = nextIdx  // 裁剪掉过期样本
    }
}
15.5 清理时机总结表
清理层级 触发周期 清理对象 源码位置
分区级 (table) ~1 分钟 整个月份分区目录 table.go:428-467
Part 级 (partition) ~7 分钟 过期 Part 文件 partition.go:1742-1754
Block 级 (merge) 每次 Merge 时 Block 内的过期样本 merge.go:85-89, 205-216
15.6 为什么不是"立即删除"?

清理被分散到多层、多周期、带随机抖动地执行,原因是:

  • 分区级:~1 分钟检查一次过期月份目录
  • Part 级:~7 分钟检查一次过期 Part
相关推荐
某不知名網友2 分钟前
ROS2进阶:服务通信、自定义SRV接口、参数服务器与基于服务的参数通信详解
java·开发语言·microsoft
Wang's Blog12 分钟前
Java 项目实战: 交友项目 - MongoDB 入门:体系结构和命令行操作
java·mongodb·交友
小溪学编程12 分钟前
将 Bean 放入 Spring 容器中的方式有哪些?
java·后端·spring
java_nnnn16 分钟前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode
用户37215742613519 分钟前
Java 读取、修改和清空 PDF 文档属性(元数据)
java
SL_staff21 分钟前
从‘改一行代码等三天’到‘改完即生效’:实施工程师的逻辑引擎避坑手记
java·产品·全栈
小羊没烦恼!25 分钟前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
hhwyqwqhhwy26 分钟前
设备树之 provider 和 consumer
java·linux·前端
\光辉岁月/29 分钟前
9.日期时间类
java·date·localdatetime·日期类
wanderist.32 分钟前
202609CSP认证P3
算法