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