VictoriaMetrics 存储机制逻辑分析

VictoriaMetrics 是后出的一款面向可观测指标的时序数据存储和查询引擎, 传闻比prometheus 查询性能强,存储成本还低,我们也有做指标数据平台,特来学习下

以下分析基于 v1.45.x 版本

1. 核心结论

VictoriaMetrics 的存储设计可以概括为以下几点:

  • 使用 vminsertvmstoragevmselect 分离写入、存储与查询职责;
  • 采用 Prometheus 风格的单值时序模型,每个数据点由时间戳和浮点数值组成;
  • 根据指标名和标签生成时间序列标识 TSID
  • 将标签索引与原始时序数据分开存储;
  • 原始数据按 TSID 聚合,并将时间戳和值分别进行列式压缩;
  • 数据先写入内存,刷盘后形成不可变的 Part,再通过后台 Merge 合并;
  • 按月建立数据分区,通过 smallbig 两级目录平衡近期数据的读取速度与历史数据的压缩率;
  • 索引和数据均采用分层文件结构,通过元索引缩小读取范围;
  • 不依赖传统 WAL,在异常退出时可能丢失尚未刷盘的数据。

2. 集群架构

VictoriaMetrics 集群由三个核心组件组成。

组件 主要职责
vminsert 接收写入请求,通过一致性哈希将数据分发到不同的 vmstorage 节点
vmstorage 保存标签索引和时序数据,并执行本地检索
vmselect 接收查询请求,从一个或多个 vmstorage 节点读取数据并汇总结果

三个组件可以分别扩容。vmstorage 采用 Shared-Nothing 架构:节点之间不共享数据,也不需要互相通信。这种结构降低了节点间协调成本,使扩容和故障隔离更简单。


VictoriaMetrics 查询性能测试

Vectoria Metrics 查询性能测试用例

原本是测试某款产品和 vm的对比, 某款产品没有对比意义,已经移除相关对比数据, 只看vm的查询性能即可

测试结果

对比项 vectoria metircs
写入吞吐 886k/s(1.66m/s)
压缩率(源json) 0.22%
查询性能(1并发) 6.19
查询性能(10并发, 7个用例同时进行10、50、100个时间段查询,统计总用时) 34.18 s
查询性能(50并发) 560.47
查询性能(100并发) 2040.83
查询性能(50并发,47.5w/s写) 575.19

测试方案

单次查询脚本

  1. 双方准备测试用例sql
  2. 用例均通过双方的query api接口进行查询,均通过response中的执行耗时进行用例耗时统计
  3. 循环测试用例,每个用例连续执行3遍,新用例执行前清除系统cache
  4. 收集所有用例执行时间
  5. 分别统计 冷执行(用例第一次执行) 和 热执行(用例第二次和第三次执行)所耗时间
  6. 进行对比

VictoriaMetrics 写入吞吐和数据压缩比 测试

测试结果

对比项 X2.0 vectoria metircs score
写入吞吐 x 886k/s(1.66m/s)
压缩比(源json) x 0.22%

benchmark 数据源配置

pod 4c6G

数据生成速率

1
2
3
3000*1230 active time series
scrapeInterval 10秒,1230*3K/10 369k samples/sec
每10分钟 更新10% time series

数据协议,prometheus remote write

数据格式(转json 后)

1
2
3
{"metric":{"__name__":"node_scrape_collector_success","job":"node_exporter","instance":"host-2362","collector":"mdadm","replica":"0","revision":"r0","url_replica":"0"},"values":[1],"timestamps":[1724313603243]}
{"metric":{"__name__":"node_textfile_scrape_error","job":"node_exporter","instance":"host-1398","replica":"0","revision":"r5","url_replica":"0"},"values":[0],"timestamps":[1724313609919]}
{"metric":{"__name__":"node_textfile_scrape_error","job":"node_exporter","instance":"host-1902","replica":"0","revision":"r0","url_replica":"0"},"values":[0],"timestamps":[1724313604325]}

Column-Stores vs. Row-Stores

论文标题:Column-Stores vs. Row-Stores: How Different Are They Really?

论文: http://www.cs.umd.edu/~abadi/papers/abadi-sigmod08.pdf

概述

从论文的标题可以看出这篇论文不是陈述一种新的技术、架构,而更偏议论文一点,它主要的目的在于搞清楚对于分析类的查询为什么Column-Store比Row-Store好那么多?好在哪里?

一般认为原因是:

分析类查询往往只查询一个表里面很少的几个字段,Column-Store只需要从磁盘读取用户查询的Column,而Row-Store读取每一条记录的时候你会把所有Column的数据读出来,在IO上Column-Store比Row-Store效率高很多,因此性能更好。

而本文的目的是要告诉你Column-Store在存储格式优势只是一方面,如果没有查询引擎上其它几个优化措施的配合,性能也不会太好的,这篇论文认为Column-Store在查询引擎层有以下几种大的优化手段:

  • 块遍历(Block Iteration)
  • 压缩(Compression)
  • 延迟物化(Late Materialization)
  • Invisible Join

其中前三点是前人就已经总结过的、在现有Column-Store上实现过了的,而最后一点是本论文的创新。下面我们一一看一下这几种优化手段的细节,最后再看看它们优化效果的对比。


什么是列式存储

什么是列式存储

在传统的行式数据库系统中,数据按如下顺序存储:

Row WatchID JavaEnable Title GoodEvent EventTime
#0 89354350662 1 Investor Relations 1 2016-05-18 05:19:20
#1 90329509958 0 Contact us 1 2016-05-18 08:10:20
#2 89953706054 1 Mission 1 2016-05-18 07:38:00
#N

处于同一行中的数据总是被物理的存储在一起。

常见的行式数据库系统有:MySQLPostgresMS SQL Server

在列式数据库系统中,数据按如下的顺序存储:

Row: #0 #1 #2 #N
WatchID: 89354350662 90329509958 89953706054
JavaEnable: 1 0 1
Title: Investor Relations Contact us Mission
GoodEvent: 1 1 1
EventTime: 2016-05-18 05:19:20 2016-05-18 08:10:20 2016-05-18 07:38:00

这些示例只显示了数据的排列顺序。来自不同列的值被单独存储,来自同一列的数据被存储在一起。


Kudu 删除表range分区

Kudu 删除表range分区

https://kudu.apache.org/docs/command_line_tools_reference.html#table-drop_range_partition

注意事项

  1. 一次只能删一个分区
  2. 分区条件必须前后精准匹配
  3. 可以通过kudu table describe查看表信息(包括分区信息):
1
2
# 查看表信息,包括列、分区信息
kudu table describe 10.128.2.162:7051,10.128.2.72:7051,10.128.2.172:7051 bn_op_1228

Kudu表复制(表数据迁移)

Kudu 表复制(表数据迁移)

https://kudu.apache.org/docs/command_line_tools_reference.html#table-copy

注意事项:

  1. 要求复制表于原表的列的结构一致(不要求分区和副本一致,所以可以调整表的副本数)
  2. 可以先只复制表结构
  3. predicates 预言 大小比值 只支持数值类型,时间戳类型不支持
  4. 注意超时设置
  5. write_type 参数有3种:
    1. 空字符 ,表示只复制表结构
    2. insert,直接插入
    3. upsert,更新插入

Presto执行计划 - 分发Statement到不同的QueryExecution

Presto执行计划 - 分发Statement到不同的QueryExecution

阅读源码时梳理逻辑用,无阅读价值

获取QueryExecution

queryexecution表示一次查询执行,用于启动、停止与管理一个查询,以及统计这个查询的相关信息。

在 经过Antlr 4语法解析起进行语法分析后,最终生成了一个Node,然后转成Statement,然后再包装成PreparedQuery

再看后续代码,在 dispatchQueryFactory.createDispatchQuery()方法中对不同类型的Statement 进行分发处理,其中对应的类为:QueryExecutionFactory

io.trino.dispatcher.DispatchManager.createQueryInternal()
1
2
3
4
5
6
7
8
 preparedQuery = queryPreparer.prepareQuery(session, query);
// ....
DispatchQuery dispatchQuery = dispatchQueryFactory.createDispatchQuery(
session,
query,
preparedQuery,
slug,
selectionContext.getResourceGroupId());

Presto查询计划

Presto执行计划-生成计划

阅读源码时梳理逻辑用,无阅读价值

在创建 dispatchQuery 的最后,返回了一个LocalDispatchQuery,其构造函数最后一个参数调用了 SqlQueryExecution 的start()方法


Presto执行计划-词法语法分析

Presto执行计划-词法语法分析

阅读源码时梳理逻辑用,无阅读价值

如果要跟可以从上一节提交查询过程中client 发送给coordinator 的提交查询API (/v1/statement)开始跟

client初始提交query后,coordinator 创建了一个Query,然后组装nextUri 后直接response了,这时候 query 尚未排队,只有当client 来请求query状态了(/v1/statement/queued/{queryId}/{slug}/{token}),这时才将其加入到调度队列中


Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×