VictoriaMetrics系列指南(一)
2026年4月6日 · 4938 字 · 更新 2026年4月7日
victoriaMetrics介绍
随着Kubernetes成为基础设施的标准,Prometheus凭借其出色的多维数据模型成为了监控的事实标准。然而当业务规模、指标基数爆炸式增长时,Prometheus的本地存储在大规模、长周期的数据保留面前显得力不从心。正是在这样的背景下,VictoriaMetrics作为一个高性能的长期存储方案应运而生。它不仅仅是为了兼容Prometheus,更是试图通过更纯粹的架构设计,重塑时序数据库在成本与性能之间的平衡。
victoriaMetrics集群版的架构如下:

可以看到其中包含了多个组件。每个组件分工不同,且都可以独立拓展。现在,让我们先聚焦于负责数据采集的轻量化组件vmagent,深度剖析它的工作机制与核心逻辑”。
vmagent
该组件可用于从多个不同来源收集指标。收集之后,可以对这些指标进行重新打标或执行流式聚合、去重等操作,然后再将它们发送到具体的存储位置(如prometheus、vmstorage)。
数据采集
vmagent数据采集支持两种模式:pull和push。在push模式下,数据源可以通过HTTP API向vmagent直接推送数据,在推送的过程中还可以对数据进行打标。
而在pull模式下,vmagent可以通过配置定期地从数据源抓取数据,抓取间隔可以在配置中的global字段中设置。如果没有设置全局或特定的抓取间隔,则默认间隔为1分钟。抓取的超时时间则默认为10秒,如果抓取间隔短于超时时间,则以超时时间为准。
config:
global:
scrape_interval: 10s
scrape_configs:
- job_name: "kubernetes-service-endpoints-slow"
scrape_interval: 5m
scrape_timeout: 30s
...当响应体过大时,vmagent会直接丢弃超过16 MB的数据(-promscrape.maxScrapeSize)。可以根据需要全局调整此限制,也可以只针对特定目标进行调整。vmagent自身还提供了一系列监控指标用于观察抓取链路的稳定性:
- vm_promscrape_scrape_requests_total:目前已发出多少次抓取请求
- vm_promscrape_scrapes_total200:成功的抓取次数
- vm_promscrape_max_scrape_size_exceeded_errors_total:是否有任何抓取响应超出大小限制
- vm_promscrape_scrapes_timed_out_total:抓取操作超时次数
- vm_promscrape_scrape_duration_seconds:每次抓取需要多长时间
- vm_promscrape_scrape_response_size_bytes:抓取响应的大小是多少
- vm_promscrape_scrapes_failed_total:失败的抓取次数
抓取到的数据会加载到内存中,对于如何处理这些数据,vmagent提供了两种模式:one-shot模式和流模式。对于较小的抓取响应,on-shot模式通常更高效,因为数据不需要拆分处理。而对于较大的响应,流模式可能更节省资源,数据会以64KB为单位进行顺序处理,而不是一次性处理所有数据。
流模式可以通过vmagent启动时使用-promscrape.streamParse标志全局开启。也可以在配置中为每个抓取目标单独开启:
scrape_configs:
- job_name: 'big-federate'
stream_parse: true
...即使没有手动启用流模式,当响应大小超过1MB时(参考-promscrape.minResponseSizeForStreamParse配置),vmagent也会自动切换到流模式以进行优化。但前提是没有设置过如下配置:
- scrape_configs[].sample_limit:限制可以抓取的样本数量
- promscrape.seriesLimitPerTarget:限制单个抓取目标中唯一时间序列的数量
这两个配置在到达限制时会丢弃数据,因此会导致断流。
重新打标
指标数据抓取后,可以通过配置中的metric_relabel_configs字段对数据进行重新打标:
scrape_configs:
- job_name: test
static_configs:
- targets: [host123]
metric_relabel_configs:
- if: '{job=~"my-app-.*",env!="dev"}'
target_label: foo
replacement: bar缓冲队列
指标数据处理完毕后,vmagent就可以将数据发送到远端存储了(-remoteWrite.url)。如果远端存储出现故障,默认情况下会启用持久队列来保护数据。这意味着数据会被发送到本地存储,以避免数据丢失。
这个队列是一个混合系统,它有一部分在内存,有一部分在磁盘。在数据进入这个队列之前,会先进行压缩。如果压缩后的数据块大小大于-remoteWrite.maxBlockSize标志所配置的限制,就会对该数据块进行递归切割,直到满足条件。在某些极端情况下(标签过长),如果递归切割后的数据块仍然不满足条件,该数据块就会被丢弃,并记录警告信息。
数据块最大大小为32MB
内存队列遵循FIFO先进先出原则。vmagent启动时会启用一定数量的工作进程,这些工作进程默认是可用CPU核心数的两倍(可通过-remoteWrite.queues标志进行设置),它们有5秒中的时间从内存队列中读取数据,如果未能及时读取,内存队列中的数据就会被刷新到基于磁盘的文件队列中。
而且内存队列中的数据块数量是有限的,如果超出限制,不管有没有到5秒,数据都会被直接刷新到磁盘。可以通过如下配置进行调整:
- -memory.allowedPercent:vmagent使用总内存的百分比,默认60%
- -memory.allowedBytes:按字节数设置,比百分比设置更精确,会覆盖百分比的设置
数据被刷新到磁盘时,会存储在-remoteWrite.tmpDataPath标志所指定的目录中。如果你不想将数据持久化到磁盘,也可以通过-remoteWrite.disableOnDiskQueue标志来禁用此功能。与内存队列一样,基于文件的队列也有限制,系统只会将一定量的数据存储在磁盘上,主要由以下标志控制:
- -remoteWrite.maxDiskUsagePerURL:表示要发往对应远端存储的数据最大能缓冲多少到本地。默认为0,也就是无限制。如果超过该限制,vmagent就会删除最老的未处理的数据块来腾出空间,防止溢出。
数据在写入磁盘时,会以数据块的形式存储。每个数据块文件的大小上限约为512MB,一旦达到此限制,vmagent就会为下一组数据块创建一个新的数据块文件。随着数据的不断流入,可能会创建多个数据块文件。数据块文件中的每个数据块都包含了两部分:
- Header:一个8字节的头部,存储数据块的大小信息,用于读取时定位
- Payload:压缩后的实际时间序列数据
为什么限制在512MB呢?这是基于最大的块大小 (32MB) 和每Chunk文件最多容纳16个块(32MB x 16 = 512MB)的逻辑计算出来的。这些数据块文件的存储位置可以通过-remoteWrite.tmpDataPath进行配置。目录结构如下:
/tmpData/persistent-queue
│
└── 1_B075D19130BC92D7
├── 0000000000000000 # 512 MB chunk file
├── 0000000002000000 # 512 MB chunk file
├── 0000000004000000 # 512 MB chunk file
├── 0000000006000000 # 512 MB chunk file
├── flock.lock
└── metainfo.json在向文件写入数据时,每次都直接将小块数据写入磁盘效率并不高。所以vmagent使用了内存缓冲,将较小的数据块收集到内存的缓冲区中,直到缓冲区写满再一次性将其全部写入磁盘,这样可以减少I/O操作,提高写入速度。当然,若数据块太大而无法放入缓冲区,vmagent则会跳过缓冲区,直接将数据写入磁盘。缓冲区的大小取决于以下配置:
- -memory.allowedPercent:vmagent使用总内存的百分比,默认为60%
- -memory.allowedBytes:按字节数设置,比百分比设置更精确,会覆盖百分比的设置
如果设置vmagent可使用的内存总大小为2GB,则缓冲区大小为250KB,这正好在4KB到512KB的范围内。数据写入到磁盘后,vmagent还会生成一个小型的元数据文件(metainfo.json),用于跟踪已处理的数据量、已写入的数据量以及其他重要信息。如果系统崩溃或重启,这些元数据可以帮助vmagent从上次中断的地方继续运行。那去重器和流聚合器中保存的数据呢?无论数据是否经过磁盘持久化,来自去重器和流聚合器的数据会按照其设定的时间间隔定期刷新到快速队列,以确保数据流的连续性和同步性。
flock.lock文件用于坐标并发访问的文件锁机制的一部分,主要用于实现进程间互斥。防止多个vmagent进程同时操作同一份队列数据,造成数据损坏或状态混乱
基数缩减
高基数指的是在监控系统中产生了非常多的唯一时间序列。例如你有一百万个用户,每个用户都有唯一的user_id,当使用user_id作为标签时,这些标签的值会有一百万个,时间序列也就有一百万个,这会降低写入和查询性能。为了避免这种情况,vmagent可以通过以下两个配置项控制某个时间窗口内最多允许出现多少个新的时间序列:
- -remoteWrite.maxHourlySeries:控制每小时最多接受多少个新的时间序列。默认为0,即不开启限制
- -remoteWrite.maxDailySeries:控制每天最多接受多少个新的时间序列。默认为0,即不开启限制 如果超过限制,新出现的时间序列就会被丢弃,但已经接受的时间序列的样本数据会继续发送
全局去重
vmagent支持去重功能,可以取出时间序列中任意额外的、不必要的数据点。其目的是在特定时间范围内仅保留最重要的一个样本,这可以有效减少高频数据的冗余,尤其是在scrape抖动或重复传输场景里非常有用。去重功能配置如下:
- -streamAggr.dedupInterval:设置全局去重间隔。假设将全局去重间隔设置为30秒,vmagent每30秒内只保留最新的样本,也就是时间戳最高的样本。如果样本的时间戳相同,则保留样本值最大的那个
流式聚合
流式聚合是指在写入前实时地将连续的指标数据进行汇总。假设现在正在以很高的频率抓取数据,例如每秒一次,如果每个数据点都存储下来就会占用大量空间并且还会降低查询速度。但你又不想因为去重而丢失数据,流式聚合可以解决这个问题,它允许你对更长时间段(如5分钟)的数据进行汇总:
- match: '{__name__=~".+_total"}'
interval: 5m
outputs: [total]如上,任何以_total结尾的指标每5分钟都将只存储一个数据点。如果原始指标是some_metric_total,那聚合后的版本就类似于some_metric_total:5m_total,虽然最终数据点总数会减少,但不会丢失任何信息。
启用流式聚合后,vmagent会将数据发送到聚合器,该聚合器拥有自己的内存缓存,并在后台将聚合后的数据分批刷新到远端存储。需要注意的是,去重是在流式聚合前执行的,也就是数据首先在去重时间间隔内被过滤,剩下的(去重后的)数据再刷新到聚合器,聚合器根据聚合规则,再将数据汇总后发送到远端存储。要正确使用去重和流式聚合,流式聚合的时间间隔必须大于去重的时间间隔,且必须是去重间隔的倍数。
例如,如果去重间隔设置为30s,那流式聚合间隔可以是60s、90s、5m等。当开启流式聚合后,聚合器默认会从主数据流中窃取并丢弃那些已经匹配了聚合规则并贡献了聚合结果的输入时间序列,可以以下通过两个标志位来调整此功能:
- -streamAggr.keepInput:是否同时保留匹配和未匹配的输入时间序列
- 默认为false。如果设置为true,则两者都保留
- -streamAggr.dropInput:是否删除所有的输入时间序列,还是仅删除匹配到的时间序列
- 当-streamAggr.keepInput为false时生效
- 默认为false。如果设置为true,所有输入时间序列(无论是否匹配规则)都会被丢弃
如果想要更精确地控制,还可以按照数据流设置去重,即在每条流式聚合规则中设置独立的去重配置:
- match: '{__name__=~".+_total"}'
interval: 1m # 流式聚合间隔
outputs: [total]
dedup_interval: 30s # 去重间隔在这种情况下,数据每分钟都会进行聚合,但在每个30秒的去重窗口期间,它只保留最新(或最高值)的样本。这样,既能减少噪音,又能保留关键数据。
数据分片和复制
经过去重和流式聚合的筛选后,vmagent会将筛选后的时间序列样本发送到一个或多个远程存储端。在这一过程中,vmagent提供了两种主要的数据分发策略:
- 复制:将相同的样本数据复制并发送到多个远端存储(-remoteWrite.url)。每个远程存储都会接收到完整数据副本,数据一致性强
- 分片:将样本数据分散发送到多个远端存储(-remoteWrite.shardByURL)。每个远程存储只接收部分数据,数据被水平拆分
对于分片,vmagent会将每个时间序列的标签集组合起来,并使用hash函数生成一串64位的哈希值。这个哈希值决定了该时间序列属于哪个分片,并将其路由到对应的远程存储端点。这样可以确保同一时间序列的样本数据(基于其标签集)总是被发送到同一个存储系统。你也可以通过以下配置来控制用于分片的标签:
- -remoteWrite.shardByURL.labels:指定时间序列中用来参与计算的标签
- -remoteWrite.shardByURL.ignoreLabels:忽略时间序列中的某些标签,不参与计算
默认情况下,分片只会创建一个副本,为了提高数据的可用性,你可以通过-remoteWrite.shardByURLReplicas标志指定每个分片应该发送到的存储系统数量,从而实现分片级别的复制。这意味着,如果一个存储系统发生故障或无法接收数据数据时,仍然存在于另一个分片中,因此不会丢失任何数据。
针对远端存储进行微调
在配置复制或分片后,每个时间序列就都有了目的地。但此时还没完全准备好将数据发送到远程存储端点。每个配置的远程存储端点URL都有一个独立的remoteWriteCtx来管理数据流。这种基于每个URL独立配置上下文的设计,允许你向不同的后端发送不同精度、不同维度的数据,赋予了监控系统极强的多租户和异构分发能力。此上下文会依次执行以下四个主要步骤来对标签和样本值进行修改和优化:
- -remoteWrite.urlRelabelConfig:局部重新打标,仅对当前这一个远程存储端点生效。可以针对特定的后端,剔除掉那些高基数但在该存储中不必要的标签
- -remoteWrite.streamAggr.dedupInterval:针对发往当前远程存储端点的数据进行去重
- -remoteWrite.streamAggr.config:针对发往当前远程存储端点的数据进行流式聚合。强烈建议不要将全局流聚合与按远程存储的流聚合混合使用
- -remoteWrite.label:强制固定标签。这些标签是强制添加的,无论之前Relabel怎么删减,这里定义的标签都会被强行注入
在完成了标签的重新打标后,vmagent还允许我们对样本值进行最后的微调。这不仅是为了让Grafana面板上的数字更易读,更核心的目的是提升数据的压缩率,节省磁盘空间。vmagent可以通过两种方式修改时间序列样本的值:
- -remoteWrite.significantFigures:有效数字舍入。从第一个非零数字开始保留N位精度,适用于量级跨度极大的指标(如网络流量、磁盘字节数)
- 例如当流量为1234字节时,保留2位有效数字变为1200
- -remoteWrite.roundDigits:小数位数舍入。固定保留小数点后的位数,适用于百分比或比率指标(如CPU使用率、错误率)
- 例如磁盘占用99.998765,保留2位小数变为100.00%
四舍五入后使数据更易读,且有助于数据压缩。默认情况下不会进行任何舍入操作
限速
正如前文所讲的,vmagent启动时,会启动多个工作进程,用于从缓冲队列中提取数据块,并对其进行处理,之后再将其发送到远程存储。数据的发送由另一组工作进程完成,其数量通常是可用CPU核心数的两倍(-remoteWrite.queues)。
工作进程从缓冲队列中取数据的流程是这样的:它首先检查缓冲队列中的内存队列,如果内存队列为空,则继续检查文件队列。如果两个队列都为空,则工作进程会等待新数据到达。
一旦获取到了有效的数据块,工作进程就会开始将其发送到远程存储,该操作是阻塞的,一直持续到接收到新的响应。如果vmagent在此阶段停止了(如重启或agent副本重新调度),它会优雅地停止(最多等5s,尽量把请求处理完)。这就是为什么vmagent需要等待一段时间才会真正停止。如果5秒后数据仍未得到响应,vmagent会强制将该数据块写回到缓冲队列中(内存或磁盘文件)。
在将数据块发送給远程存储时,vmagent会使用速率限制器控制发送速率,避免超过每秒设定的字节数。配置如下;
- -remoteWrite.rateLimit:控制发送速率。默认为0,表示无限制
这是应对“惊群效应”的终极武器。例如,当后端存储宕机1小时后突然恢复,积压了数GB数据的vmagent会瞬间爆发。如果没有限速,这种突发的请求洪峰极有可能再次打垮刚刚恢复的后端。
重试机制
如果发送失败会发生什么呢?当远程存储端点响应的状态码为409或400时,则被视为永久拒绝。这通常意味着由于请求错误或冲突等原因,该数据块无法被接受。在这种情况下,vmagent会记录该事件并完全跳过该区块。
而对于其他的返回不同状态码的故障,vmagent会使用指数退避算法来重新发送发送失败的数据块。这意味着每次失败后,重试间隔时间都会逐渐延长,但不会超过预设的最大重试间隔:
- -remoteWrite.retryMinInterval:初始重试间隔,默认为1秒
- -remoteWrite.maxRetryInterval:最大重试间隔,默认为60秒